1package server
2
3import (
4 "context"
5 "fmt"
6 "log"
7
8 appCfg "github.com/charmbracelet/soft-serve/config"
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 config *appCfg.Config
28}
29
30// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
31// server key-pair will be created if none exists. An initial admin SSH public
32// key can be provided with authKey. If authKey is provided, access will be
33// restricted to that key. If authKey is not provided, the server will be
34// publicly writable until configured otherwise by cloning the `config` repo.
35func NewServer(cfg *config.Config) *Server {
36 ac, err := appCfg.NewConfig(cfg)
37 if err != nil {
38 log.Fatal(err)
39 }
40 mw := []wish.Middleware{
41 rm.MiddlewareWithLogger(
42 cfg.ErrorLog,
43 // BubbleTea middleware.
44 bm.MiddlewareWithProgramHandler(SessionHandler(ac), termenv.ANSI256),
45 // Command middleware must come after the git middleware.
46 cm.Middleware(ac),
47 // Git middleware.
48 gm.Middleware(cfg.RepoPath, ac),
49 // Logging middleware must be last to be executed first.
50 lm.Middleware(),
51 ),
52 }
53 s, err := wish.NewServer(
54 ssh.PublicKeyAuth(ac.PublicKeyHandler),
55 ssh.KeyboardInteractiveAuth(ac.KeyboardInteractiveHandler),
56 wish.WithAddress(fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.Port)),
57 wish.WithHostKeyPath(cfg.KeyPath),
58 wish.WithMiddleware(mw...),
59 )
60 if err != nil {
61 log.Fatalln(err)
62 }
63 d, err := daemon.NewDaemon(cfg, ac)
64 if err != nil {
65 log.Fatalln(err)
66 }
67 return &Server{
68 SSHServer: s,
69 GitServer: d,
70 Config: cfg,
71 config: ac,
72 }
73}
74
75// Reload reloads the server configuration.
76func (s *Server) Reload() error {
77 return s.config.Reload()
78}
79
80// Start starts the SSH server.
81func (s *Server) Start() error {
82 var errg errgroup.Group
83 errg.Go(func() error {
84 log.Printf("Starting Git server on %s:%d", s.Config.BindAddr, s.Config.GitPort)
85 if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
86 return err
87 }
88 return nil
89 })
90 errg.Go(func() error {
91 log.Printf("Starting SSH server on %s:%d", s.Config.BindAddr, s.Config.Port)
92 if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
93 return err
94 }
95 return nil
96 })
97 return errg.Wait()
98}
99
100// Shutdown lets the server gracefully shutdown.
101func (s *Server) Shutdown(ctx context.Context) error {
102 var errg errgroup.Group
103 errg.Go(func() error {
104 return s.SSHServer.Shutdown(ctx)
105 })
106 errg.Go(func() error {
107 return s.GitServer.Shutdown(ctx)
108 })
109 return errg.Wait()
110}
111
112// Close closes the SSH server.
113func (s *Server) Close() error {
114 var errg errgroup.Group
115 errg.Go(func() error {
116 return s.SSHServer.Close()
117 })
118 errg.Go(func() error {
119 return s.GitServer.Close()
120 })
121 return errg.Wait()
122}