1package server
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "time"
8
9 cm "github.com/charmbracelet/soft-serve/server/cmd"
10 "github.com/charmbracelet/soft-serve/server/config"
11 "github.com/charmbracelet/soft-serve/server/git/daemon"
12 gm "github.com/charmbracelet/soft-serve/server/git/ssh"
13 "github.com/charmbracelet/wish"
14 bm "github.com/charmbracelet/wish/bubbletea"
15 lm "github.com/charmbracelet/wish/logging"
16 rm "github.com/charmbracelet/wish/recover"
17 "github.com/gliderlabs/ssh"
18 "github.com/muesli/termenv"
19 "golang.org/x/sync/errgroup"
20)
21
22// Server is the Soft Serve server.
23type Server struct {
24 SSHServer *ssh.Server
25 GitServer *daemon.Daemon
26 Config *config.Config
27}
28
29// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
30// server key-pair will be created if none exists. An initial admin SSH public
31// key can be provided with authKey. If authKey is provided, access will be
32// restricted to that key. If authKey is not provided, the server will be
33// publicly writable until configured otherwise by cloning the `config` repo.
34func NewServer(cfg *config.Config) *Server {
35 s := &Server{Config: cfg}
36 mw := []wish.Middleware{
37 rm.MiddlewareWithLogger(
38 log.Default(),
39 // BubbleTea middleware.
40 bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256),
41 // Command middleware must come after the git middleware.
42 cm.Middleware(cfg),
43 // Git middleware.
44 gm.Middleware(cfg.RepoPath(), cfg),
45 // Logging middleware must be last to be executed first.
46 lm.Middleware(),
47 ),
48 }
49
50 opts := []ssh.Option{
51 wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.SSH.Port)),
52 wish.WithPublicKeyAuth(cfg.PublicKeyHandler),
53 wish.WithMiddleware(mw...),
54 }
55 if cfg.SSH.AllowKeyless {
56 opts = append(opts, ssh.KeyboardInteractiveAuth(cfg.KeyboardInteractiveHandler))
57 }
58 if cfg.SSH.AllowPassword {
59 opts = append(opts, ssh.PasswordAuth(cfg.PasswordHandler))
60 }
61 if cfg.SSH.Key != "" {
62 opts = append(opts, wish.WithHostKeyPEM([]byte(cfg.SSH.Key)))
63 } else {
64 opts = append(opts, wish.WithHostKeyPath(cfg.PrivateKeyPath()))
65 }
66 opts = append(opts)
67 sh, err := wish.NewServer(opts...)
68 if err != nil {
69 log.Fatalln(err)
70 }
71 if cfg.SSH.MaxTimeout > 0 {
72 sh.MaxTimeout = time.Duration(cfg.SSH.MaxTimeout) * time.Second
73 }
74 if cfg.SSH.IdleTimeout > 0 {
75 sh.IdleTimeout = time.Duration(cfg.SSH.IdleTimeout) * time.Second
76 }
77 s.SSHServer = sh
78 if cfg.Git.Enabled {
79 d, err := daemon.NewDaemon(cfg)
80 if err != nil {
81 log.Fatalln(err)
82 }
83 s.GitServer = d
84 }
85 return s
86}
87
88// Reload reloads the server configuration.
89func (s *Server) Reload() error {
90 return nil
91 // return s.config.Reload()
92}
93
94// Start starts the SSH server.
95func (s *Server) Start() error {
96 var errg errgroup.Group
97 if s.Config.Git.Enabled {
98 errg.Go(func() error {
99 log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
100 if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
101 return err
102 }
103 return nil
104 })
105 }
106 errg.Go(func() error {
107 log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
108 if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
109 return err
110 }
111 return nil
112 })
113 return errg.Wait()
114}
115
116// Shutdown lets the server gracefully shutdown.
117func (s *Server) Shutdown(ctx context.Context) error {
118 var errg errgroup.Group
119 if s.Config.Git.Enabled {
120 errg.Go(func() error {
121 return s.GitServer.Shutdown(ctx)
122 })
123 }
124 errg.Go(func() error {
125 return s.SSHServer.Shutdown(ctx)
126 })
127 return errg.Wait()
128}
129
130// Close closes the SSH server.
131func (s *Server) Close() error {
132 var errg errgroup.Group
133 errg.Go(func() error {
134 return s.SSHServer.Close()
135 })
136 if s.Config.Git.Enabled {
137 errg.Go(func() error {
138 return s.GitServer.Close()
139 })
140 }
141 return errg.Wait()
142}