server.go

  1package server
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log"
  7	"time"
  8
  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}
 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	s := &Server{Config: cfg}
 36	mw := []wish.Middleware{
 37		rm.MiddlewareWithLogger(
 38			log.Default(),
 39			// BubbleTea middleware.
 40			bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256),
 41			// Command middleware must come after the git middleware.
 42			cm.Middleware(cfg),
 43			// Git middleware.
 44			gm.Middleware(cfg.RepoPath(), cfg),
 45			// Logging middleware must be last to be executed first.
 46			lm.Middleware(),
 47		),
 48	}
 49
 50	opts := []ssh.Option{
 51		wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.SSH.Port)),
 52		wish.WithPublicKeyAuth(cfg.PublicKeyHandler),
 53		wish.WithMiddleware(mw...),
 54	}
 55	if cfg.SSH.AllowKeyless {
 56		opts = append(opts, ssh.KeyboardInteractiveAuth(cfg.KeyboardInteractiveHandler))
 57	}
 58	if cfg.SSH.AllowPassword {
 59		opts = append(opts, ssh.PasswordAuth(cfg.PasswordHandler))
 60	}
 61	if cfg.SSH.Key != "" {
 62		opts = append(opts, wish.WithHostKeyPEM([]byte(cfg.SSH.Key)))
 63	} else {
 64		opts = append(opts, wish.WithHostKeyPath(cfg.PrivateKeyPath()))
 65	}
 66	opts = append(opts)
 67	sh, err := wish.NewServer(opts...)
 68	if err != nil {
 69		log.Fatalln(err)
 70	}
 71	if cfg.SSH.MaxTimeout > 0 {
 72		sh.MaxTimeout = time.Duration(cfg.SSH.MaxTimeout) * time.Second
 73	}
 74	if cfg.SSH.IdleTimeout > 0 {
 75		sh.IdleTimeout = time.Duration(cfg.SSH.IdleTimeout) * time.Second
 76	}
 77	s.SSHServer = sh
 78	if cfg.Git.Enabled {
 79		d, err := daemon.NewDaemon(cfg)
 80		if err != nil {
 81			log.Fatalln(err)
 82		}
 83		s.GitServer = d
 84	}
 85	return s
 86}
 87
 88// Reload reloads the server configuration.
 89func (s *Server) Reload() error {
 90	return nil
 91	// return s.config.Reload()
 92}
 93
 94// Start starts the SSH server.
 95func (s *Server) Start() error {
 96	var errg errgroup.Group
 97	if s.Config.Git.Enabled {
 98		errg.Go(func() error {
 99			log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
100			if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
101				return err
102			}
103			return nil
104		})
105	}
106	errg.Go(func() error {
107		log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
108		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
109			return err
110		}
111		return nil
112	})
113	return errg.Wait()
114}
115
116// Shutdown lets the server gracefully shutdown.
117func (s *Server) Shutdown(ctx context.Context) error {
118	var errg errgroup.Group
119	if s.Config.Git.Enabled {
120		errg.Go(func() error {
121			return s.GitServer.Shutdown(ctx)
122		})
123	}
124	errg.Go(func() error {
125		return s.SSHServer.Shutdown(ctx)
126	})
127	return errg.Wait()
128}
129
130// Close closes the SSH server.
131func (s *Server) Close() error {
132	var errg errgroup.Group
133	errg.Go(func() error {
134		return s.SSHServer.Close()
135	})
136	if s.Config.Git.Enabled {
137		errg.Go(func() error {
138			return s.GitServer.Close()
139		})
140	}
141	return errg.Wait()
142}