1package server
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "time"
8
9 appCfg "github.com/charmbracelet/soft-serve/config"
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 Config *config.Config
28 config *appCfg.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 ac, err := appCfg.NewConfig(cfg)
39 if err != nil {
40 log.Fatal(err)
41 }
42 mw := []wish.Middleware{
43 rm.MiddlewareWithLogger(
44 cfg.ErrorLog,
45 // BubbleTea middleware.
46 bm.MiddlewareWithProgramHandler(SessionHandler(ac), termenv.ANSI256),
47 // Command middleware must come after the git middleware.
48 cm.Middleware(cfg),
49 // Git middleware.
50 gm.Middleware(cfg.RepoPath(), ac),
51 // Logging middleware must be last to be executed first.
52 lm.Middleware(),
53 ),
54 }
55
56 opts := []ssh.Option{ssh.PublicKeyAuth(cfg.PublicKeyHandler)}
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 opts = append(opts,
64 wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.SSH.Port)),
65 wish.WithHostKeyPath(cfg.PrivateKeyPath()),
66 wish.WithMiddleware(mw...),
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 d, err := daemon.NewDaemon(cfg)
80 if err != nil {
81 log.Fatalln(err)
82 }
83 s.GitServer = d
84 return s
85}
86
87// Reload reloads the server configuration.
88func (s *Server) Reload() error {
89 return s.config.Reload()
90}
91
92// Start starts the SSH server.
93func (s *Server) Start() error {
94 var errg errgroup.Group
95 errg.Go(func() error {
96 log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
97 if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
98 return err
99 }
100 return nil
101 })
102 errg.Go(func() error {
103 log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
104 if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
105 return err
106 }
107 return nil
108 })
109 return errg.Wait()
110}
111
112// Shutdown lets the server gracefully shutdown.
113func (s *Server) Shutdown(ctx context.Context) error {
114 var errg errgroup.Group
115 errg.Go(func() error {
116 return s.SSHServer.Shutdown(ctx)
117 })
118 errg.Go(func() error {
119 return s.GitServer.Shutdown(ctx)
120 })
121 return errg.Wait()
122}
123
124// Close closes the SSH server.
125func (s *Server) Close() error {
126 var errg errgroup.Group
127 errg.Go(func() error {
128 return s.SSHServer.Close()
129 })
130 errg.Go(func() error {
131 return s.GitServer.Close()
132 })
133 return errg.Wait()
134}