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