server.go

  1package server
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log"
  7	"time"
  8
  9	appCfg "github.com/charmbracelet/soft-serve/config"
 10	cm "github.com/charmbracelet/soft-serve/server/cmd"
 11	"github.com/charmbracelet/soft-serve/server/config"
 12	"github.com/charmbracelet/soft-serve/server/git/daemon"
 13	gm "github.com/charmbracelet/soft-serve/server/git/ssh"
 14	"github.com/charmbracelet/wish"
 15	bm "github.com/charmbracelet/wish/bubbletea"
 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	"golang.org/x/sync/errgroup"
 21)
 22
 23// Server is the Soft Serve server.
 24type Server struct {
 25	SSHServer *ssh.Server
 26	GitServer *daemon.Daemon
 27	Config    *config.Config
 28	config    *appCfg.Config
 29}
 30
 31// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
 32// server key-pair will be created if none exists. An initial admin SSH public
 33// key can be provided with authKey. If authKey is provided, access will be
 34// restricted to that key. If authKey is not provided, the server will be
 35// publicly writable until configured otherwise by cloning the `config` repo.
 36func NewServer(cfg *config.Config) *Server {
 37	s := &Server{Config: cfg}
 38	ac, err := appCfg.NewConfig(cfg)
 39	if err != nil {
 40		log.Fatal(err)
 41	}
 42	mw := []wish.Middleware{
 43		rm.MiddlewareWithLogger(
 44			cfg.ErrorLog,
 45			// BubbleTea middleware.
 46			bm.MiddlewareWithProgramHandler(SessionHandler(ac), termenv.ANSI256),
 47			// Command middleware must come after the git middleware.
 48			cm.Middleware(cfg),
 49			// Git middleware.
 50			gm.Middleware(cfg.RepoPath(), ac),
 51			// Logging middleware must be last to be executed first.
 52			lm.Middleware(),
 53		),
 54	}
 55
 56	opts := []ssh.Option{ssh.PublicKeyAuth(cfg.PublicKeyHandler)}
 57	if cfg.SSH.AllowKeyless {
 58		opts = append(opts, ssh.KeyboardInteractiveAuth(cfg.KeyboardInteractiveHandler))
 59	}
 60	if cfg.SSH.AllowPassword {
 61		opts = append(opts, ssh.PasswordAuth(cfg.PasswordHandler))
 62	}
 63	opts = append(opts,
 64		wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.SSH.Port)),
 65		wish.WithHostKeyPath(cfg.PrivateKeyPath()),
 66		wish.WithMiddleware(mw...),
 67	)
 68	sh, err := wish.NewServer(opts...)
 69	if err != nil {
 70		log.Fatalln(err)
 71	}
 72	if cfg.SSH.MaxTimeout > 0 {
 73		sh.MaxTimeout = time.Duration(cfg.SSH.MaxTimeout) * time.Second
 74	}
 75	if cfg.SSH.IdleTimeout > 0 {
 76		sh.IdleTimeout = time.Duration(cfg.SSH.IdleTimeout) * time.Second
 77	}
 78	s.SSHServer = sh
 79	d, err := daemon.NewDaemon(cfg)
 80	if err != nil {
 81		log.Fatalln(err)
 82	}
 83	s.GitServer = d
 84	return s
 85}
 86
 87// Reload reloads the server configuration.
 88func (s *Server) Reload() error {
 89	return s.config.Reload()
 90}
 91
 92// Start starts the SSH server.
 93func (s *Server) Start() error {
 94	var errg errgroup.Group
 95	errg.Go(func() error {
 96		log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
 97		if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
 98			return err
 99		}
100		return nil
101	})
102	errg.Go(func() error {
103		log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
104		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
105			return err
106		}
107		return nil
108	})
109	return errg.Wait()
110}
111
112// Shutdown lets the server gracefully shutdown.
113func (s *Server) Shutdown(ctx context.Context) error {
114	var errg errgroup.Group
115	errg.Go(func() error {
116		return s.SSHServer.Shutdown(ctx)
117	})
118	errg.Go(func() error {
119		return s.GitServer.Shutdown(ctx)
120	})
121	return errg.Wait()
122}
123
124// Close closes the SSH server.
125func (s *Server) Close() error {
126	var errg errgroup.Group
127	errg.Go(func() error {
128		return s.SSHServer.Close()
129	})
130	errg.Go(func() error {
131		return s.GitServer.Close()
132	})
133	return errg.Wait()
134}