detached 973e682

server.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package httpserver is the HTTP entry of ermite: the router, the security
4// headers, the request log and the graceful shutdown. Routes are added here
5// as the other adapters arrive: /-/health and the routes given in Config.
6package httpserver
7
8import (
9	"context"
10	"errors"
11	"fmt"
12	"net"
13	"net/http"
14	"os"
15	"time"
16
17	"github.com/bnema/zerowrap"
18)
19
20// ShutdownTimeout is how long Run waits for requests in flight after the
21// context ends.
22const ShutdownTimeout = 10 * time.Second
23
24// Check reports whether a dependency works. A nil result means healthy.
25type Check func(ctx context.Context) error
26
27// Route mounts a handler on the router.
28type Route struct {
29	// Pattern is a net/http.ServeMux pattern, such as "GET /{repo}/info/refs".
30	// It must not overlap the /-/ routes of the server.
31	Pattern string
32	Handler http.Handler
33}
34
35// Config configures a Server.
36type Config struct {
37	// Addr is the listen address, "host:port".
38	Addr string
39	// PublicURL is the public base URL of the server. When it is https://,
40	// every response carries Strict-Transport-Security.
41	PublicURL string
42	// HealthChecks run on GET /-/health, at most once per healthCacheTTL
43	// (the answer is cached, the endpoint being unauthenticated); one
44	// failure answers 503.
45	HealthChecks []Check
46	// Routes are added to the router next to /-/health.
47	Routes []Route
48	// API serves the ConnectRPC API under /-/api/ (the prefix is stripped, so
49	// it sees "/ermite.RepoService/ListRepos"). Nil serves none.
50	API http.Handler
51	// OnShutdown functions run, each in its own goroutine, when the graceful
52	// shutdown begins. Use them to end long-lived responses such as event
53	// streams, which the shutdown would otherwise wait for.
54	OnShutdown []func()
55}
56
57// APIPrefix is where the API is mounted.
58const APIPrefix = "/-/api"
59
60// Server is the HTTP server of ermite.
61type Server struct {
62	addr       string
63	handler    http.Handler
64	onShutdown []func()
65}
66
67// New builds the server and its routes.
68func New(cfg Config) *Server {
69	mux := http.NewServeMux()
70	// "GET" patterns also match HEAD; other methods get 405 with Allow, and
71	// unknown paths get 404.
72	mux.HandleFunc("GET /-/health", healthHandler(cfg.HealthChecks))
73	if cfg.API != nil {
74		mux.Handle(APIPrefix+"/", http.StripPrefix(APIPrefix, cfg.API))
75	}
76	for _, r := range cfg.Routes {
77		mux.Handle(r.Pattern, r.Handler)
78	}
79	return &Server{addr: cfg.Addr, handler: withRequestLog(withSecurityHeaders(mux, isHTTPS(cfg.PublicURL))), onShutdown: cfg.OnShutdown}
80}
81
82// Handler returns the root handler, for tests.
83func (s *Server) Handler() http.Handler { return s.handler }
84
85// Run listens on the configured address and serves until ctx ends, then
86// shuts down gracefully within ShutdownTimeout. It returns nil after a clean
87// shutdown. The logger of ctx is used for the server and its requests.
88func (s *Server) Run(ctx context.Context) error {
89	ln, err := net.Listen("tcp", s.addr)
90	if err != nil {
91		return fmt.Errorf("listen on %s: %w", s.addr, err)
92	}
93	return s.Serve(ctx, ln)
94}
95
96// Serve is Run on an existing listener, which it closes.
97func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
98	log := zerowrap.FromCtx(ctx)
99	srv := &http.Server{
100		Handler: s.handler,
101		// Requests keep the logger of ctx but outlive its cancellation, so
102		// that a graceful shutdown lets them finish.
103		BaseContext:       func(net.Listener) context.Context { return context.WithoutCancel(ctx) },
104		ReadHeaderTimeout: 10 * time.Second,
105		IdleTimeout:       2 * time.Minute,
106		// No read or write timeout: Git clones and pushes can be long.
107	}
108
109	for _, f := range s.onShutdown {
110		srv.RegisterOnShutdown(f)
111	}
112
113	errc := make(chan error, 1)
114	go func() { errc <- srv.Serve(ln) }()
115	log.Info().Str("addr", ln.Addr().String()).Msg("listening")
116
117	select {
118	case err := <-errc:
119		// Serve returns before Shutdown only on a failure.
120		return fmt.Errorf("serve: %w", err)
121	case <-ctx.Done():
122	}
123
124	log.Info().Dur("timeout", ShutdownTimeout).Msg("shutting down")
125	sctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), ShutdownTimeout)
126	defer cancel()
127	if err := srv.Shutdown(sctx); err != nil {
128		_ = srv.Close()
129		return fmt.Errorf("shutdown: %w", err)
130	}
131	if err := <-errc; err != nil && !errors.Is(err, http.ErrServerClosed) {
132		return fmt.Errorf("serve: %w", err)
133	}
134	log.Info().Msg("http server stopped")
135	return nil
136}
137
138// DirWritable returns a Check that creates and removes a file in dir.
139func DirWritable(dir string) Check {
140	return func(context.Context) error {
141		f, err := os.CreateTemp(dir, ".health-*")
142		if err != nil {
143			return fmt.Errorf("data directory is not writable: %w", err)
144		}
145		name := f.Name()
146		cerr := f.Close()
147		rerr := os.Remove(name)
148		if cerr != nil {
149			return fmt.Errorf("data directory is not writable: %w", cerr)
150		}
151		if rerr != nil {
152			return fmt.Errorf("data directory is not writable: %w", rerr)
153		}
154		return nil
155	}
156}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close