middleware

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package middleware provides shithub's HTTP middleware stack. Composable with chi/net.http; each middleware exposes its own constructor.

Index

Constants

View Source
const RequestIDHeader = "X-Request-Id"

RequestIDHeader is the canonical incoming/outgoing request-ID header.

Variables

This section is empty.

Functions

func AccessLog

func AccessLog(logger *slog.Logger) func(http.Handler) http.Handler

AccessLog returns middleware that emits one structured log line per request after the response is written. Includes request_id when present.

func CORS

func CORS(cfg CORSConfig) func(http.Handler) http.Handler

CORS returns middleware that enforces the policy. Requests with an `Origin` header are checked against AllowedOrigins; OPTIONS preflights short-circuit with the appropriate response headers.

func CSRF

func CSRF(cfg CSRFConfig) func(http.Handler) http.Handler

CSRF returns middleware that enforces nosurf's double-submit-cookie protection. State-changing methods without a valid token are rejected.

func CSRFTokenForRequest

func CSRFTokenForRequest(r *http.Request) string

CSRFTokenForRequest exposes the per-request token to templates.

func Compress

func Compress(next http.Handler) http.Handler

Compress returns middleware that gzip-encodes responses for clients that advertise gzip support, bypassing already-compressed responses (images, pre-compressed git pack data, etc.). The middleware is intentionally modest — chi/Negroni-grade compression suites are overkill here.

Routes that stream (git protocol in S12) must skip this middleware via a dedicated route group, since wrapping ResponseWriter breaks Flusher guarantees the streaming pack relies on.

func CountQueries

func CountQueries() func(http.Handler) http.Handler

CountQueries returns middleware that wraps every request context with db.WithCounter so the per-request query count is observable downstream.

Production: install at the router root in test builds only — it adds one context.WithValue per request; cheap, but conceptually reserved for the test surface.

Test usage:

r.Use(middleware.CountQueries())
rec := httptest.NewRecorder()
req := httptest.NewRequest(...)
r.ServeHTTP(rec, req)
if got := middleware.QueriesFor(req); got > 8 {
    t.Fatalf("issuesList ran %d queries; threshold 8", got)
}

The threshold is per-route; callers document it in the test.

func MaxBodySize

func MaxBodySize(maxBytes int64) func(http.Handler) http.Handler

MaxBodySize returns middleware that caps r.Body at maxBytes via http.MaxBytesReader. Use this on auth POST endpoints (signup, login, password reset) so a misbehaving client can't ship a 10 MB password to weaponize the argon2 hashing path.

func Metrics

func Metrics(next http.Handler) http.Handler

Metrics returns middleware that records HTTP-level metrics via the project-wide Prometheus registry.

Route labels are extracted from chi when available so we get "/owner/{repo}" instead of the per-repo concrete path — keeping cardinality bounded.

func OptionalUser

func OptionalUser(lookup UserLookup) func(http.Handler) http.Handler

OptionalUser populates CurrentUser into context from the loaded session when present. Does not redirect or reject — pages that don't need a user (homepage, public repo views) just ignore an empty CurrentUser.

When lookup returns successfully and the user's current session_epoch does NOT match the session's recorded epoch, the binding is skipped so the request looks anonymous. The stale cookie itself isn't actively cleared — RequireUser will redirect the next protected hit to /login, at which point a fresh session is minted with the current epoch.

Pass nil to skip the lookup entirely (CurrentUser.Username will be empty and epoch checks are bypassed).

func PATAuthMiddleware

func PATAuthMiddleware(cfg PATConfig) func(http.Handler) http.Handler

PATAuthMiddleware returns middleware that resolves a `Authorization: token ...`, `Authorization: Bearer ...`, or HTTP Basic credential into a populated PATAuth on the request context.

Behavior:

  • Missing Authorization header → next handler runs with an empty PATAuth (the route may still allow session auth).
  • Malformed credentials, unknown hash, revoked, or expired token → 401 with WWW-Authenticate set; chain stops here.
  • On success → PATAuth populated on context, last-used updated (debounced), chain proceeds.

func PolicyCache

func PolicyCache() func(http.Handler) http.Handler

PolicyCache attaches a per-request memo to ctx so the policy package can de-duplicate (actor, repo) → role lookups across the handler chain. Wire this once near the top of the chain, after the request has been bound to a session.

Mutations that change collaborator state mid-request must call policy.InvalidateRepo(ctx, repoID) before re-checking, or the cache will return a stale role.

func QueriesFor

func QueriesFor(r *http.Request) int64

QueriesFor reports the number of queries observed against the request's context. Returns 0 when CountQueries wasn't installed.

Reads the COUNTER from the request's context after the handler has returned; the counter is mutated by goroutines spawned during the request, but those should have synchronized before the handler returns.

func RealIP

func RealIP(cfg RealIPConfig) func(http.Handler) http.Handler

RealIP returns middleware that resolves the client IP by walking the X-Forwarded-For header rightwards, accepting only entries that come from a configured trusted-proxy network. The resolved IP is stored in context for later middleware (access log, rate limit) to consume.

func RealIPFromContext

func RealIPFromContext(ctx context.Context, r *http.Request) string

RealIPFromContext returns the real IP set by the middleware, falling back to the request's RemoteAddr (host part) when no value has been resolved.

func Recover

func Recover(logger *slog.Logger, handler PanicHandler) func(http.Handler) http.Handler

Recover returns middleware that catches panics, logs them with the request_id, and delegates rendering to handler. If handler is nil a plain "internal server error" body is written.

func RequestID

func RequestID(next http.Handler) http.Handler

RequestID returns middleware that ensures every request has a request_id. If the inbound request carries an X-Request-Id header it's reused (capped to a sane length); otherwise we generate a 16-byte random hex string. The id is propagated via context and echoed in the response headers so a caller can correlate logs with their own traces.

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request_id, or "" if none is set.

func RequireScope

func RequireScope(required pat.Scope) func(http.Handler) http.Handler

RequireScope rejects with 403 if the request was authenticated via PAT and the token's scopes don't include required. Pure-session callers (PATAuth zero) pass through — sessions have implicit full scope.

func RequireSiteAdmin

func RequireSiteAdmin(notFound http.Handler) func(http.Handler) http.Handler

RequireSiteAdmin gates routes behind users.is_site_admin. Compose AFTER RequireUser so an anonymous request hits /login first; this middleware then guards the elevated surface.

Non-admin viewers receive 404 (NOT 403) so the existence of /admin isn't disclosed by the response shape. The same pattern matches every other "shouldn't-know-it-exists" surface across the app.

Impersonating admins lose their admin powers for the duration of the impersonation (CurrentUser.IsSiteAdmin is forced false in that path), so this gate transparently locks them out of /admin until they end the impersonation. Documented in docs/internal/admin.md.

func RequireUser

func RequireUser(next http.Handler) http.Handler

RequireUser is a stricter wrapper: redirects to /login (preserving the requested path in the `next` query param) when there is no user bound to the session. Pages that demand a user (settings, dashboard) compose this on top of OptionalUser.

func SecureHeaders

func SecureHeaders(cfg SecureHeadersConfig) func(http.Handler) http.Handler

SecureHeaders returns middleware that stamps the configured security headers on every response. HSTS is only set when the request reached us over TLS or via a trusted X-Forwarded-Proto=https proxy header.

func SessionFromContext

func SessionFromContext(ctx context.Context) *session.Session

SessionFromContext returns the loaded session, or an empty Session if the loader middleware didn't run (which would itself be a wiring bug).

func SessionLoader

func SessionLoader(store session.Store, logger *slog.Logger) func(http.Handler) http.Handler

SessionLoader returns middleware that loads the session into request context before downstream handlers run. Save happens via the store — handlers that mutate the session call store.Save explicitly.

func SessionStoreFromContext

func SessionStoreFromContext(ctx context.Context) session.Store

SessionStoreFromContext returns the session store associated with the request. Handlers that need to Save / Clear pull it out via this helper.

func Timeout

func Timeout(d time.Duration) func(http.Handler) http.Handler

Timeout returns middleware that enforces a per-request timeout via context cancellation. Handlers must respect the request context.

Streaming routes (git protocol in S12) MUST be exempt from this middleware via a separate route group; the timeout-aware ResponseWriter breaks Flusher and would corrupt streaming pack data.

func WithCurrentUserForTest

func WithCurrentUserForTest(ctx context.Context, u CurrentUser) context.Context

WithCurrentUserForTest binds u onto ctx the same way the OptionalUser middleware does. Test-only — production code paths reach a request through OptionalUser/RequireUser and never need this. Lives here so the context key stays unexported.

Types

type CORSConfig

type CORSConfig struct {
	AllowedOrigins []string
	AllowedMethods []string
	AllowedHeaders []string
	MaxAge         int
}

CORSConfig holds the strict CORS policy. By default no cross-origin requests are permitted; route groups that need CORS register an explicit whitelist (typically just the API surface in S08).

type CSRFConfig

type CSRFConfig struct {
	// CookieDomain is optional.
	CookieDomain string
	// Secure marks the CSRF cookie Secure (set when serving over TLS).
	Secure bool
	// FailureHandler is invoked when CSRF validation fails. If nil, a
	// minimal 403 plain-text response is written.
	FailureHandler http.Handler
	// ExemptFunc returns true for requests that should bypass CSRF
	// validation. State-changing endpoints authenticated by PAT (S08) and
	// the git protocol (S12/S13) register exemptions here.
	ExemptFunc func(*http.Request) bool
}

CSRFConfig configures the CSRF middleware.

type CurrentUser

type CurrentUser struct {
	ID          int64
	Username    string
	IsSuspended bool
	IsSiteAdmin bool
	// ImpersonatedUserID, when non-zero, identifies the user this admin
	// is impersonating. ID/Username/IsSuspended above reflect the
	// IMPERSONATED user (so policy checks behave as that user); the
	// real admin's ID is preserved in RealActorID for audit rows.
	ImpersonatedUserID int64
	RealActorID        int64
	ImpersonateWriteOK bool
}

CurrentUser carries the loaded user identity in request context. Handlers pull this out via CurrentUserFromContext. Anonymous requests have ID == 0.

IsSuspended is sourced from users.suspended_at and is the canonical input to policy.UserActor for web requests. Without it, every handler that constructs an actor with a hard-coded `false` lets a suspended account keep writing — the audit found this gap (S00-S25 audit, finding C1).

func CurrentUserFromContext

func CurrentUserFromContext(ctx context.Context) CurrentUser

CurrentUserFromContext returns the user bound to ctx by OptionalUser / RequireUser. Returns the zero value (anonymous) when no user is bound.

func (CurrentUser) IsAnonymous

func (u CurrentUser) IsAnonymous() bool

IsAnonymous reports whether this is an unauthenticated request.

type PATAuth

type PATAuth struct {
	UserID  int64
	TokenID int64
	Scopes  []string
}

PATAuth carries the resolved-token state for downstream handlers. When the auth check passed via PAT, `Token != nil` and Scopes is the parsed scope list. Pure session callers see the zero value.

func PATAuthFromContext

func PATAuthFromContext(ctx context.Context) PATAuth

PATAuthFromContext returns the resolved PAT auth state, or the zero value when the request was authenticated some other way (or anonymous).

type PATConfig

type PATConfig struct {
	Pool      *pgxpool.Pool
	Debouncer *pat.Debouncer // optional; one is created if nil
	Logger    *slog.Logger
	// Realm is the WWW-Authenticate realm string written on 401.
	Realm string
}

PATConfig configures the PAT auth middleware.

type PanicHandler

type PanicHandler interface {
	HandlePanic(w http.ResponseWriter, r *http.Request, requestID string, recovered any)
}

PanicHandler renders a styled error response when the request handler panics. The error page receives the request_id so users can quote it for support.

type RealIPConfig

type RealIPConfig struct {
	// TrustedProxies is the list of CIDR blocks whose X-Forwarded-For values
	// are honored. If empty, the middleware is a no-op (RemoteAddr wins).
	TrustedProxies []*net.IPNet
}

RealIPConfig configures the real-IP middleware. Trust nothing by default; the operator opts into specific proxy networks.

type SecureHeadersConfig

type SecureHeadersConfig struct {
	CSP             string // Content-Security-Policy
	HSTS            string // Strict-Transport-Security; only set when TLS
	ReferrerPolicy  string
	PermissionsPol  string
	FrameOptions    string
	ContentTypeOpts string
	COOP            string
	CORP            string
}

SecureHeadersConfig lets the operator override defaults. Values left as the zero string fall back to the documented defaults.

func DefaultSecureHeaders

func DefaultSecureHeaders() SecureHeadersConfig

DefaultSecureHeaders returns the policy applied to web responses. S35 will lock these down further; S02's defaults are the practical baseline.

type UserLookup

type UserLookup func(ctx context.Context, userID int64) (UserLookupResult, error)

UserLookup resolves a user_id into the data the auth middleware needs. SessionEpoch is the users.session_epoch column; the middleware compares it to the session's recorded epoch on every request so "log out everywhere" (which bumps the column) invalidates stale cookies on the next hit.

type UserLookupResult

type UserLookupResult struct {
	Username     string
	SessionEpoch int32
	IsSuspended  bool
	IsSiteAdmin  bool
}

UserLookupResult is what UserLookup returns. It's a struct so future fields (e.g. is_admin once S34 lands) don't keep widening the signature and forcing every callsite to update.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL