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{ssh.PublicKeyAuth(cfg.PublicKeyHandler)}
 51	if cfg.SSH.AllowKeyless {
 52		opts = append(opts, ssh.KeyboardInteractiveAuth(cfg.KeyboardInteractiveHandler))
 53	}
 54	if cfg.SSH.AllowPassword {
 55		opts = append(opts, ssh.PasswordAuth(cfg.PasswordHandler))
 56	}
 57	opts = append(opts,
 58		wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.SSH.Port)),
 59		wish.WithHostKeyPath(cfg.PrivateKeyPath()),
 60		wish.WithMiddleware(mw...),
 61	)
 62	sh, err := wish.NewServer(opts...)
 63	if err != nil {
 64		log.Fatalln(err)
 65	}
 66	if cfg.SSH.MaxTimeout > 0 {
 67		sh.MaxTimeout = time.Duration(cfg.SSH.MaxTimeout) * time.Second
 68	}
 69	if cfg.SSH.IdleTimeout > 0 {
 70		sh.IdleTimeout = time.Duration(cfg.SSH.IdleTimeout) * time.Second
 71	}
 72	s.SSHServer = sh
 73	d, err := daemon.NewDaemon(cfg)
 74	if err != nil {
 75		log.Fatalln(err)
 76	}
 77	s.GitServer = d
 78	return s
 79}
 80
 81// Reload reloads the server configuration.
 82func (s *Server) Reload() error {
 83	return nil
 84	// return s.config.Reload()
 85}
 86
 87// Start starts the SSH server.
 88func (s *Server) Start() error {
 89	var errg errgroup.Group
 90	errg.Go(func() error {
 91		log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
 92		if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
 93			return err
 94		}
 95		return nil
 96	})
 97	errg.Go(func() error {
 98		log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
 99		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
100			return err
101		}
102		return nil
103	})
104	return errg.Wait()
105}
106
107// Shutdown lets the server gracefully shutdown.
108func (s *Server) Shutdown(ctx context.Context) error {
109	var errg errgroup.Group
110	errg.Go(func() error {
111		return s.SSHServer.Shutdown(ctx)
112	})
113	errg.Go(func() error {
114		return s.GitServer.Shutdown(ctx)
115	})
116	return errg.Wait()
117}
118
119// Close closes the SSH server.
120func (s *Server) Close() error {
121	var errg errgroup.Group
122	errg.Go(func() error {
123		return s.SSHServer.Close()
124	})
125	errg.Go(func() error {
126		return s.GitServer.Close()
127	})
128	return errg.Wait()
129}