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}