Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add automatic HTTPS with ACME

642e599466fe4f5066afdc9a24775694a52c9749 · Keenan Nemetz · 14d ago · parent 91e6a33a

server/server.go +60−9

@@ -9,6 +9,7 @@ import (
9 9 "net"
10 10 "net/http"
11 11 "strconv"
12 "strings"
12 13 "time"
13 14
14 15 "github.com/rodent-software/vole/ci/runs"
@@ -42,6 +43,7 @@ type Server struct {
42 43 Web *web.Web
43 44 Log *slog.Logger
44 45 mux *http.ServeMux
46 acme *acmeSetup
45 47 }
46 48
47 49 func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) {
@@ -60,6 +62,17 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error)
60 62 } else if n > 0 {
61 63 log.Info("hooks installed", "repos", n)
62 64 }
65 acme, err := newACME(cfg, inst.Repos.Public)
66 if err != nil {
67 inst.Close()
68 return nil, err
69 }
70 if acme != nil {
71 log.Info("automatic HTTPS enabled", "addr", cfg.HTTPSAddr, "hosts", strings.Join(acme.hostNames(), ", "), "cache", cfg.ACMECachePath())
72 if cfg.PagesDomain != "" {
73 log.Info("site certificates issued on demand", "domain", cfg.PagesDomain)
74 }
75 }
63 76 proxy := githttp.New(githttp.Config{
64 77 Root: cfg.ReposDir(),
65 78 Open: inst.Repos.Open,
@@ -108,7 +121,7 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error)
108 121
109 122 s := &Server{
110 123 Cfg: cfg, Repos: inst.Repos, Identity: inst.Identity, Sessions: inst.Sessions,
111 Runs: inst.Runs, Index: inst.Index,
124 Runs: inst.Runs, Index: inst.Index, acme: acme,
112 125 LFS: inst.LFS, Locks: inst.Locks, Proxy: proxy, SSH: ssh, Web: wp, Log: log,
113 126 }
114 127 wp.AllowCI = s.allowContributorCI
@@ -276,7 +289,9 @@ func (s *Server) Handler() http.Handler {
276 289 })
277 290 }
278 291
279 // Run starts both listeners and blocks until ctx is cancelled.
292 // Run starts the listeners and blocks until ctx is cancelled. With automatic
293 // HTTPS, the HTTP address only answers ACME challenges and redirects, and the
294 // forge is served over TLS on HTTPSAddr.
280 295 func (s *Server) Run(ctx context.Context) error {
281 296 sshLn, err := net.Listen("tcp", s.Cfg.SSHAddr)
282 297 if err != nil {
@@ -288,36 +303,72 @@ func (s *Server) Run(ctx context.Context) error {
288 303 return fmt.Errorf("http listen %s: %w", s.Cfg.HTTPAddr, err)
289 304 }
290 305
291 hs := &http.Server{
306 httpSrv := &http.Server{
292 307 Handler: s.Handler(),
293 308 ReadHeaderTimeout: 15 * time.Second,
294 309 IdleTimeout: 120 * time.Second,
295 310 }
296 errCh := make(chan error, 2)
311 var httpsLn net.Listener
312 var httpsSrv *http.Server
313 if s.acme != nil {
314 httpSrv.Handler = s.acme.httpHandler()
315 httpsLn, err = net.Listen("tcp", s.Cfg.HTTPSAddr)
316 if err != nil {
317 httpLn.Close()
318 sshLn.Close()
319 return fmt.Errorf("https listen %s: %w", s.Cfg.HTTPSAddr, err)
320 }
321 httpsSrv = &http.Server{
322 Handler: s.Handler(),
323 TLSConfig: s.acme.tlsConfig(),
324 ReadHeaderTimeout: 15 * time.Second,
325 IdleTimeout: 120 * time.Second,
326 }
327 }
328
329 errCh := make(chan error, 3)
297 330 go func() {
298 331 if err := s.SSH.Serve(sshLn); err != nil {
299 332 errCh <- fmt.Errorf("ssh serve: %w", err)
300 333 }
301 334 }()
302 335 go func() {
303 if err := hs.Serve(httpLn); err != nil && !errors.Is(err, http.ErrServerClosed) {
336 if err := httpSrv.Serve(httpLn); err != nil && !errors.Is(err, http.ErrServerClosed) {
304 337 errCh <- fmt.Errorf("http serve: %w", err)
305 338 }
306 339 }()
340 if httpsSrv != nil {
341 go func() {
342 if err := httpsSrv.ServeTLS(httpsLn, "", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
343 errCh <- fmt.Errorf("https serve: %w", err)
344 }
345 }()
346 }
307 347
308 s.Log.Info("listening", "ssh", sshLn.Addr().String(), "http", httpLn.Addr().String(),
309 "data", s.Cfg.DataDir, "users", s.Identity.Snapshot().Count)
348 if httpsSrv != nil {
349 s.Log.Info("listening", "ssh", sshLn.Addr().String(), "http", httpLn.Addr().String(),
350 "https", httpsLn.Addr().String(), "data", s.Cfg.DataDir, "users", s.Identity.Snapshot().Count)
351 } else {
352 s.Log.Info("listening", "ssh", sshLn.Addr().String(), "http", httpLn.Addr().String(),
353 "data", s.Cfg.DataDir, "users", s.Identity.Snapshot().Count)
354 }
310 355
311 356 select {
312 357 case <-ctx.Done():
313 358 sh, cancel := context.WithTimeout(context.Background(), 20*time.Second)
314 359 defer cancel()
315 360 s.SSH.Close()
316 hs.Shutdown(sh)
361 httpSrv.Shutdown(sh)
362 if httpsSrv != nil {
363 httpsSrv.Shutdown(sh)
364 }
317 365 return nil
318 366 case err := <-errCh:
319 367 s.SSH.Close()
320 hs.Close()
368 httpSrv.Close()
369 if httpsSrv != nil {
370 httpsSrv.Close()
371 }
321 372 return err
322 373 }
323 374 }