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