1package server
  2
  3import (
  4	"context"
  5	"errors"
  6	"net/http"
  7
  8	"github.com/charmbracelet/keygen"
  9	"github.com/charmbracelet/log"
 10
 11	"github.com/charmbracelet/soft-serve/server/backend"
 12	"github.com/charmbracelet/soft-serve/server/backend/sqlite"
 13	"github.com/charmbracelet/soft-serve/server/config"
 14	"github.com/charmbracelet/soft-serve/server/cron"
 15	"github.com/charmbracelet/soft-serve/server/daemon"
 16	sshsrv "github.com/charmbracelet/soft-serve/server/ssh"
 17	"github.com/charmbracelet/soft-serve/server/stats"
 18	"github.com/charmbracelet/soft-serve/server/web"
 19	"github.com/charmbracelet/ssh"
 20	"golang.org/x/sync/errgroup"
 21)
 22
 23var (
 24	logger = log.WithPrefix("server")
 25)
 26
 27// Server is the Soft Serve server.
 28type Server struct {
 29	SSHServer   *sshsrv.SSHServer
 30	GitDaemon   *daemon.GitDaemon
 31	HTTPServer  *web.HTTPServer
 32	StatsServer *stats.StatsServer
 33	Cron        *cron.CronScheduler
 34	Config      *config.Config
 35	Backend     backend.Backend
 36	ctx         context.Context
 37}
 38
 39// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
 40// server key-pair will be created if none exists. An initial admin SSH public
 41// key can be provided with authKey. If authKey is provided, access will be
 42// restricted to that key. If authKey is not provided, the server will be
 43// publicly writable until configured otherwise by cloning the `config` repo.
 44func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
 45	var err error
 46	if cfg.Backend == nil {
 47		sb, err := sqlite.NewSqliteBackend(ctx, cfg)
 48		if err != nil {
 49			logger.Fatal(err)
 50		}
 51
 52		cfg = cfg.WithBackend(sb)
 53
 54		// Create internal key.
 55		ikp, err := keygen.New(
 56			cfg.SSH.InternalKeyPath,
 57			keygen.WithKeyType(keygen.Ed25519),
 58			keygen.WithWrite(),
 59		)
 60		if err != nil {
 61			return nil, err
 62		}
 63		cfg.InternalPublicKey = ikp.AuthorizedKey()
 64
 65		// Create client key.
 66		ckp, err := keygen.New(
 67			cfg.SSH.ClientKeyPath,
 68			keygen.WithKeyType(keygen.Ed25519),
 69			keygen.WithWrite(),
 70		)
 71		if err != nil {
 72			return nil, err
 73		}
 74		cfg.ClientPublicKey = ckp.AuthorizedKey()
 75	}
 76
 77	srv := &Server{
 78		Cron:    cron.NewCronScheduler(ctx),
 79		Config:  cfg,
 80		Backend: cfg.Backend,
 81		ctx:     ctx,
 82	}
 83
 84	// Add cron jobs.
 85	srv.Cron.AddFunc(jobSpecs["mirror"], mirrorJob(cfg))
 86
 87	srv.SSHServer, err = sshsrv.NewSSHServer(cfg, srv)
 88	if err != nil {
 89		return nil, err
 90	}
 91
 92	srv.GitDaemon, err = daemon.NewGitDaemon(cfg)
 93	if err != nil {
 94		return nil, err
 95	}
 96
 97	srv.HTTPServer, err = web.NewHTTPServer(cfg)
 98	if err != nil {
 99		return nil, err
100	}
101
102	srv.StatsServer, err = stats.NewStatsServer(cfg)
103	if err != nil {
104		return nil, err
105	}
106
107	return srv, nil
108}
109
110func start(ctx context.Context, fn func() error) error {
111	errc := make(chan error, 1)
112	go func() {
113		errc <- fn()
114	}()
115
116	select {
117	case err := <-errc:
118		return err
119	case <-ctx.Done():
120		return ctx.Err()
121	}
122}
123
124// Start starts the SSH server.
125func (s *Server) Start() error {
126	logger := log.FromContext(s.ctx).WithPrefix("server")
127	errg, ctx := errgroup.WithContext(s.ctx)
128	errg.Go(func() error {
129		logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
130		if err := start(ctx, s.GitDaemon.Start); !errors.Is(err, daemon.ErrServerClosed) {
131			return err
132		}
133		return nil
134	})
135	errg.Go(func() error {
136		logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
137		if err := start(ctx, s.HTTPServer.ListenAndServe); !errors.Is(err, http.ErrServerClosed) {
138			return err
139		}
140		return nil
141	})
142	errg.Go(func() error {
143		logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
144		if err := start(ctx, s.SSHServer.ListenAndServe); !errors.Is(err, ssh.ErrServerClosed) {
145			return err
146		}
147		return nil
148	})
149	errg.Go(func() error {
150		logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
151		if err := start(ctx, s.StatsServer.ListenAndServe); !errors.Is(err, http.ErrServerClosed) {
152			return err
153		}
154		return nil
155	})
156	errg.Go(func() error {
157		logger.Print("Starting cron scheduler")
158		s.Cron.Start()
159		return nil
160	})
161	return errg.Wait()
162}
163
164// Shutdown lets the server gracefully shutdown.
165func (s *Server) Shutdown(ctx context.Context) error {
166	var errg errgroup.Group
167	errg.Go(func() error {
168		return s.GitDaemon.Shutdown(ctx)
169	})
170	errg.Go(func() error {
171		return s.HTTPServer.Shutdown(ctx)
172	})
173	errg.Go(func() error {
174		return s.SSHServer.Shutdown(ctx)
175	})
176	errg.Go(func() error {
177		return s.StatsServer.Shutdown(ctx)
178	})
179	return errg.Wait()
180}
181
182// Close closes the SSH server.
183func (s *Server) Close() error {
184	var errg errgroup.Group
185	errg.Go(s.GitDaemon.Close)
186	errg.Go(s.HTTPServer.Close)
187	errg.Go(s.SSHServer.Close)
188	errg.Go(s.StatsServer.Close)
189	return errg.Wait()
190}