~bigbes/sr-ht-compare

ref: 4e7c6329e40c58e790e98774d456af93c2e56bc5 sr-ht-compare/cmd/comparesrht/main.go -rw-r--r-- 6.6 KiB
4e7c6329 — bigbes web: full-width split diff view with a rendered file tree 30 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// Command comparesrht is the compare.sr.ht daemon: a stateless HTTP service
// that renders git ref-to-ref diffs and single-commit views for a self-hosted
// SourceHut instance.
//
// It reuses core-go's server.New assembly so it shares the standard SourceHut
// daemon lifecycle (crypto initialization, -b/-m/-p flags, warm shutdown on
// SIGINT) with the rest of the fleet, but it deliberately does NOT call
// WithDefaultMiddleware: compare.sr.ht owns no Postgres or Redis and must serve
// anonymous viewers, so it installs its own lightweight middleware group on the
// anonymous router instead (see the web package documentation for the exact
// chain).
//
// Flags (parsed by core-go's server.New):
//
//	-b addr   bind address (repeatable); default 127.0.0.1:5090
//	-d        debug (verbose request logging in core-go)
//	-m addr   Prometheus metrics bind (default random port)
//	-p addr   pprof bind (default random localhost port)
//
// Configuration is loaded from the shared SourceHut config.ini via core-go's
// fixed search path (./config.ini, ../config.ini, /etc/sr.ht/config.ini,
// /etc/sr.ht/*.ini). All required keys are validated up front with clear
// messages so a misconfiguration fails loudly at startup rather than as a deep
// panic on the first request.
package main

import (
	"fmt"
	"os"
	"strings"
	"time"

	"sourcecraft.dev/bigbes/sr-ht-core/config"
	coreserver "sourcecraft.dev/bigbes/sr-ht-core/server"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/sirupsen/logrus"
	"github.com/vaughan0/go-ini"

	"sourcecraft.dev/bigbes/sr-ht-compare/authz"
	"sourcecraft.dev/bigbes/sr-ht-compare/web"
)

const (
	service     = "compare.sr.ht"
	defaultBind = "127.0.0.1:5090"

	// authzTTL is how long the GraphQL authorizer memoizes positive and
	// not-found repository lookups, sparing git.sr.ht a round trip per page.
	authzTTL = 60 * time.Second
)

func main() {
	logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})

	// LoadConfig never panics on a missing file (it returns a nil ini.File);
	// validateConfig turns any absent required key into a single clear fatal.
	conf := config.LoadConfig()
	apiOrigin := validateConfig(conf)

	// server.New parses -b/-d/-m/-p from the argument vector and runs
	// crypto.InitCrypto(conf) — the network-key and webhook key validated above
	// are exactly what it needs, so this cannot fatal after validateConfig.
	// It expects the full os.Args (core-go's getopt skips argv[0] as the
	// program name, exactly as every upstream SourceHut daemon calls it).
	srv := coreserver.New(service, defaultBind, conf, os.Args)

	authorizer := authz.NewAuthorizer(authzTTL)
	app, err := web.New(conf, authorizer)
	if err != nil {
		logrus.Fatalf("initialize web server: %v", err)
	}

	// Middleware chain per the web package contract (outermost first). This is
	// the hand-rolled substitute for WithDefaultMiddleware: no database, no
	// redis, and authz.Middleware never issues a 401 so anonymous browsing
	// works. config.Middleware must be present because the GraphQL authorizer
	// resolves git.sr.ht's API origin from config.ForContext at request time.
	//
	// server.New already froze the anonymous router for direct middleware
	// registration (it built inline sub-routers during construction), so the
	// group + middleware + routes are installed together inside a Group, which
	// chi permits on a fresh inline mux sharing the same routing tree.
	srv.AnonRouter().Group(func(r chi.Router) {
		r.Use(middleware.RealIP)
		r.Use(middleware.Recoverer)
		r.Use(middleware.Logger)
		r.Use(config.Middleware(conf, service))
		r.Use(authz.Middleware())
		app.Register(r)
	})

	reposRoot, _ := conf.Get("git.sr.ht", "repos")
	logrus.WithFields(logrus.Fields{
		"bind":          resolveBind(os.Args[1:]),
		"repos":         reposRoot,
		"git.sr.ht-api": apiOrigin,
	}).Info("compare.sr.ht starting")

	// Run blocks until SIGINT, then performs a warm shutdown. systemd should
	// stop this unit with KillSignal=SIGINT (see contrib/compare-srht.service).
	srv.Run()
}

// validateConfig verifies every configuration key compare.sr.ht needs before it
// can serve or authorize a request. It reports all missing keys at once via a
// single logrus.Fatal so operators fix the config in one pass instead of
// discovering each gap on a separate restart. It returns the git.sr.ht internal
// API origin that GraphQL authorization will use (also logged at startup).
//
// This runs BEFORE anything can reach config.GetAPI, which panics when no
// origin candidate is configured, and before crypto.InitCrypto (invoked by
// server.New), which would otherwise fatal with a terse message on a missing
// network-key or webhook key.
func validateConfig(conf ini.File) string {
	var missing []string
	require := func(section, key string) {
		if v, ok := conf.Get(section, key); !ok || strings.TrimSpace(v) == "" {
			missing = append(missing, fmt.Sprintf("[%s] %s", section, key))
		}
	}

	require("sr.ht", "network-key")    // crypto: unified-login cookie fernet key
	require("webhooks", "private-key") // crypto: webhook signing key (shared)
	require("git.sr.ht", "repos")      // bare repository root on disk
	require("meta.sr.ht", "origin")    // login/logout links in the nav
	require("compare.sr.ht", "origin") // our own external origin

	// git.sr.ht needs at least one internal API origin candidate; without it
	// config.GetAPI panics on the first authorization request.
	apiOrigin := firstConfigured(conf, "git.sr.ht",
		"api-internal-origin", "internal-origin", "api-origin", "origin")
	if apiOrigin == "" {
		missing = append(missing,
			"[git.sr.ht] one of api-internal-origin, internal-origin, api-origin, origin")
	}

	if len(missing) > 0 {
		logrus.Fatalf("incomplete configuration; missing required keys:\n\t%s",
			strings.Join(missing, "\n\t"))
	}
	return apiOrigin
}

// firstConfigured returns the value of the first present, non-empty key in
// section, or "" if none are set.
func firstConfigured(conf ini.File, section string, keys ...string) string {
	for _, k := range keys {
		if v, ok := conf.Get(section, k); ok && strings.TrimSpace(v) != "" {
			return v
		}
	}
	return ""
}

// resolveBind reconstructs the primary bind address server.New will use, for
// logging only. server.New owns the authoritative parse; this mirrors its -b
// handling (last -b wins here; the real server binds every -b given) and falls
// back to the same default.
func resolveBind(args []string) string {
	bind := defaultBind
	for i := 0; i < len(args); i++ {
		switch a := args[i]; {
		case a == "-b" && i+1 < len(args):
			bind = args[i+1]
			i++
		case strings.HasPrefix(a, "-b") && len(a) > 2:
			bind = a[2:]
		}
	}
	return bind
}