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