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}