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