server.go

  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}