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