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}