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 | } | |