// Package web is the HTTP layer of compare.sr.ht. It ports the SourceHut chrome // (nav/service-switcher, login block, environment banner) to Go html/templates, // renders the repository landing, compare (base...head) and single-commit pages // server-side, and embeds a compact JSON payload plus the vendored esbuild // bundle so the browser renders the diff with @pierre/diffs and @pierre/trees. // // The package owns no state of its own: identity comes from the authz cookie // middleware, authorization from an authz.Authorizer (git.sr.ht GraphQL), and // git data from gitx over bare repositories on disk. Every request that touches // a repository authorizes first (a not-found or forbidden repo is a 404, never // a 403, so private-repo existence never leaks) and only then reads the disk. // // # What the cmd layer must wire // // Register only installs routes; it assumes the following middleware is already // applied to the router it is handed, in this order (outermost first): // // chi middleware.RealIP // chi middleware.Recoverer // chi middleware.Logger (optional, but recommended) // config.Middleware(conf, "compare.sr.ht") // required: authz + gitx read it // authz.Middleware() // required: never 401s; sets the viewer // // config.Middleware must run before authz.Middleware is irrelevant to authz // itself (it only reads the cookie), but the GraphQL authorizer invoked inside // handlers needs config.ForContext(ctx) to resolve git.sr.ht's API origin, so // config.Middleware is mandatory on every request that reaches a handler. package web import ( "fmt" "io/fs" "net/http" "path" "regexp" "git.sr.ht/~sircmpwn/core-go/config" "github.com/vaughan0/go-ini" "go.bigb.es/sourcehut-compare/authz" ) // hashedCSSRe matches the content-addressed stylesheet name so it can be served // with an immutable cache lifetime (the hash changes whenever the bytes do). var hashedCSSRe = regexp.MustCompile(`^main\.min\.[0-9a-f]{6,}\.css$`) // Server holds the immutable configuration a request handler needs. It is built // once at startup and is safe for concurrent use. type Server struct { authorizer authz.Authorizer reposRoot string conf ini.File siteName string environment string metaOrigin string compareOrigin string hubOrigin string cssHref string nav []navItem staticFileServer http.Handler } // New assembles a Server from the shared SourceHut config. It reads // [git.sr.ht] repos, [meta.sr.ht] origin and [compare.sr.ht] origin (all // required), the [sr.ht] site-name/environment display values, and resolves the // hashed stylesheet name by globbing the embedded static FS. A missing required // key is a clear error, not a panic, so the cmd layer can fail startup loudly. func New(conf ini.File, authorizer authz.Authorizer) (*Server, error) { reposRoot, ok := conf.Get("git.sr.ht", "repos") if !ok || reposRoot == "" { return nil, fmt.Errorf("web: [git.sr.ht] repos is required") } metaOrigin := config.GetOrigin(conf, "meta.sr.ht", true) if metaOrigin == "" { return nil, fmt.Errorf("web: [meta.sr.ht] origin is required") } compareOrigin := config.GetOrigin(conf, "compare.sr.ht", true) if compareOrigin == "" { return nil, fmt.Errorf("web: [compare.sr.ht] origin is required") } cssHref, err := resolveCSSHref() if err != nil { return nil, err } staticSub, err := fs.Sub(staticFS, "static") if err != nil { return nil, fmt.Errorf("web: sub static FS: %w", err) } return &Server{ authorizer: authorizer, reposRoot: reposRoot, conf: conf, siteName: config.GetString(conf, "sr.ht", "site-name", "sourcehut"), environment: config.GetString(conf, "sr.ht", "environment", "production"), metaOrigin: metaOrigin, compareOrigin: compareOrigin, hubOrigin: config.GetOrigin(conf, "hub.sr.ht", true), cssHref: cssHref, nav: buildNav(conf), staticFileServer: http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))), }, nil } // resolveCSSHref globs the embedded static FS for the content-addressed // stylesheet and returns its site-absolute URL. func resolveCSSHref() (string, error) { matches, err := fs.Glob(staticFS, "static/main.min.*.css") if err != nil { return "", fmt.Errorf("web: glob stylesheet: %w", err) } if len(matches) == 0 { return "", fmt.Errorf("web: no main.min.*.css in embedded static assets") } return "/static/" + path.Base(matches[0]), nil }