server.go

  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}