server.go

  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.Host, cfg.SSH.Port)),
 57		wish.WithHostKeyPath(cfg.PrivateKeyPath()),
 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.Host, s.Config.Git.Port)
 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.Host, s.Config.SSH.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}