1package server
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "path/filepath"
8 "strings"
9
10 "github.com/charmbracelet/log"
11
12 appCfg "github.com/charmbracelet/soft-serve/config"
13 "github.com/charmbracelet/soft-serve/server/config"
14 "github.com/charmbracelet/wish"
15 bm "github.com/charmbracelet/wish/bubbletea"
16 gm "github.com/charmbracelet/wish/git"
17 lm "github.com/charmbracelet/wish/logging"
18 rm "github.com/charmbracelet/wish/recover"
19 "github.com/gliderlabs/ssh"
20 "github.com/muesli/termenv"
21)
22
23// Server is the Soft Serve server.
24type Server struct {
25 SSHServer *ssh.Server
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 softMiddleware(ac),
44 bm.MiddlewareWithProgramHandler(SessionHandler(ac), termenv.ANSI256),
45 gm.Middleware(cfg.RepoPath, ac),
46 // Note: disable pushing to subdirectories as it can create
47 // conflicts with existing repos. This only affects the git
48 // middleware.
49 //
50 // This is related to
51 // https://github.com/charmbracelet/soft-serve/issues/120
52 // https://github.com/charmbracelet/wish/commit/8808de520d3ea21931f13113c6b0b6d0141272d4
53 func(sh ssh.Handler) ssh.Handler {
54 return func(s ssh.Session) {
55 cmds := s.Command()
56 if len(cmds) == 2 && strings.HasPrefix(cmds[0], "git") {
57 repo := strings.TrimSuffix(strings.TrimPrefix(cmds[1], "/"), "/")
58 repo = filepath.Clean(repo)
59 if n := strings.Count(repo, "/"); n != 0 {
60 wish.Fatalln(s, fmt.Errorf("invalid repo path: subdirectories not allowed"))
61 return
62 }
63 }
64 sh(s)
65 }
66 },
67 lm.MiddlewareWithLogger(log.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})),
68 ),
69 }
70 s, err := wish.NewServer(
71 ssh.PublicKeyAuth(ac.PublicKeyHandler),
72 ssh.KeyboardInteractiveAuth(ac.KeyboardInteractiveHandler),
73 wish.WithAddress(fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.Port)),
74 wish.WithHostKeyPath(cfg.KeyPath),
75 wish.WithMiddleware(mw...),
76 )
77 if err != nil {
78 log.Fatal(err)
79 }
80 return &Server{
81 SSHServer: s,
82 Config: cfg,
83 config: ac,
84 }
85}
86
87// Reload reloads the server configuration.
88func (srv *Server) Reload() error {
89 return srv.config.Reload()
90}
91
92// Start starts the SSH server.
93func (srv *Server) Start() error {
94 if err := srv.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
95 return err
96 }
97 return nil
98}
99
100// Serve serves the SSH server using the provided listener.
101func (srv *Server) Serve(l net.Listener) error {
102 if err := srv.SSHServer.Serve(l); err != ssh.ErrServerClosed {
103 return err
104 }
105 return nil
106}
107
108// Shutdown lets the server gracefully shutdown.
109func (srv *Server) Shutdown(ctx context.Context) error {
110 return srv.SSHServer.Shutdown(ctx)
111}
112
113// Close closes the SSH server.
114func (srv *Server) Close() error {
115 return srv.SSHServer.Close()
116}