server.go

  1package server
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log"
  7	"net/http"
  8	"time"
  9
 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	HTTPServer *http.Server
 28	Config     *config.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	mw := []wish.Middleware{
 39		rm.MiddlewareWithLogger(
 40			log.Default(),
 41			// BubbleTea middleware.
 42			bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256),
 43			// Command middleware must come after the git middleware.
 44			cm.Middleware(cfg),
 45			// Git middleware.
 46			gm.Middleware(cfg.RepoPath(), cfg),
 47			// Logging middleware must be last to be executed first.
 48			lm.Middleware(),
 49		),
 50	}
 51
 52	opts := []ssh.Option{
 53		wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.SSH.Port)),
 54		wish.WithPublicKeyAuth(cfg.PublicKeyHandler),
 55		wish.WithMiddleware(mw...),
 56	}
 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	if cfg.SSH.Key != "" {
 64		opts = append(opts, wish.WithHostKeyPEM([]byte(cfg.SSH.Key)))
 65	} else {
 66		opts = append(opts, wish.WithHostKeyPath(cfg.PrivateKeyPath()))
 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	if cfg.Git.Enabled {
 80		d, err := daemon.NewDaemon(cfg)
 81		if err != nil {
 82			log.Fatalln(err)
 83		}
 84		s.GitServer = d
 85	}
 86	if cfg.HTTP.Enabled {
 87		s.HTTPServer = newHTTPServer(cfg)
 88	}
 89	return s
 90}
 91
 92// Start starts the SSH server.
 93func (s *Server) Start() error {
 94	var errg errgroup.Group
 95	if s.Config.Git.Enabled {
 96		errg.Go(func() error {
 97			log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
 98			if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
 99				return err
100			}
101			return nil
102		})
103	}
104	if s.Config.HTTP.Enabled {
105		errg.Go(func() error {
106			log.Printf("Starting HTTP server on %s:%d", s.Config.Host, s.Config.HTTP.Port)
107			if err := s.HTTPServer.ListenAndServe(); err != http.ErrServerClosed {
108				return err
109			}
110			return nil
111		})
112	}
113	errg.Go(func() error {
114		log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
115		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
116			return err
117		}
118		return nil
119	})
120	return errg.Wait()
121}
122
123// Shutdown lets the server gracefully shutdown.
124func (s *Server) Shutdown(ctx context.Context) error {
125	var errg errgroup.Group
126	if s.Config.Git.Enabled {
127		errg.Go(func() error {
128			return s.GitServer.Shutdown(ctx)
129		})
130	}
131	if s.Config.HTTP.Enabled {
132		errg.Go(func() error {
133			return s.HTTPServer.Shutdown(ctx)
134		})
135	}
136	errg.Go(func() error {
137		return s.SSHServer.Shutdown(ctx)
138	})
139	return errg.Wait()
140}
141
142// Close closes the SSH server.
143func (s *Server) Close() error {
144	var errg errgroup.Group
145	errg.Go(func() error {
146		return s.SSHServer.Close()
147	})
148	if s.Config.Git.Enabled {
149		errg.Go(func() error {
150			return s.GitServer.Close()
151		})
152	}
153	if s.Config.HTTP.Enabled {
154		errg.Go(func() error {
155			return s.HTTPServer.Close()
156		})
157	}
158	return errg.Wait()
159}