middleware

package
v1.18.14 Latest Latest
Warning

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

Go to latest
Published: May 3, 2026 License: BSD-3-Clause Imports: 15 Imported by: 0

Documentation

Overview

Package middleware provides HTTP middleware for the PeeringDB Plus server.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CORS

func CORS(in CORSInput) func(http.Handler) http.Handler

CORS returns middleware that adds CORS headers per OPS-06. Origins are configured via the AllowedOrigins field.

func CSP added in v1.12.0

func CSP(in CSPInput) func(http.Handler) http.Handler

CSP returns middleware that sets a Content-Security-Policy header based on the request path. Web UI routes get the tighter UIPolicy; GraphQL gets the more permissive GraphQLPolicy (for the GraphiQL playground). Non-browser routes (/api/, /rest/, ConnectRPC) receive no CSP header.

The header name is chosen by in.EnforcingMode: true → "Content-Security-Policy" (enforcing), false → "Content-Security-Policy-Report-Only" (report-only monitoring without blocking). The policy strings are identical in both modes.

func Compression added in v1.12.0

func Compression() func(http.Handler) http.Handler

Compression returns middleware that gzip-compresses HTTP responses when the client advertises Accept-Encoding: gzip. gRPC content types are excluded because ConnectRPC manages its own compression.

Uses klauspost/compress/gzhttp which handles Content-Encoding headers, ETag suffixing (appends --gzip), and minimum size thresholds automatically.

func Logging

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

Logging returns middleware that logs each HTTP request with method, path, status, duration, and trace context (trace_id, span_id) when available. Uses structured slog per OBS-1, OBS-5 with LogAttrs for attribute-based API.

func MaxBytesBody added in v1.14.0

func MaxBytesBody(in MaxBytesBodyInput) func(http.Handler) http.Handler

MaxBytesBody returns middleware that wraps r.Body with http.MaxBytesReader for every non-gRPC request. gRPC and ConnectRPC paths bypass entirely so streaming RPCs are not truncated.

Per-route wraps at cmd/peeringdb-plus/main.go (/sync, /graphql) remain as tighter belt-and-suspenders — innermost wins, which is harmless here because the global cap and the per-route caps share the same maxRequestBodySize constant (1 MB).

func PrivacyTier added in v1.14.0

func PrivacyTier(in PrivacyTierInput) func(http.Handler) http.Handler

PrivacyTier returns middleware that stamps every inbound request context with the configured tier via privctx.WithTier AND emits an OTel attribute pdbplus.privacy.tier on the current span (the inbound HTTP server span created by otelhttp.NewMiddleware, which sits just outside this middleware in the chain).

The tier and its string form are resolved once at construction; there is zero per-request env read, zero per-request string alloc, and the only per-request cost is context.WithValue + SetAttributes on the active span. When the chain does not carry an active span (unit tests without a tracer), trace.SpanFromContext returns a noop span and SetAttributes is a zero-cost no-op — fail-safe-closed.

Per 61-CONTEXT.md D-07/D-08/D-09 (OBS-03):

  • attribute key is the literal "pdbplus.privacy.tier" (pdbplus.* namespace, matches pdbplus.sync.*, pdbplus.data.*).
  • cardinality is 2: "public" or "users". A future TierAdmin addition must force a compile error here (exhaustive switch with no default) before shipping.

Downstream ent/sql spans inherit the attribute via parent-span context propagation — there is intentionally no redundant stamping further down the chain.

The returned middleware does not modify the response (headers, status, body) — it is a pure context stamper. Callers composing chains can assume it has no effect on output.

func Recovery

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

Recovery returns middleware that recovers from panics in downstream handlers. Logs the panic with stack trace via slog and returns 500 to the client.

func SecurityHeaders added in v1.14.0

func SecurityHeaders(in SecurityHeadersInput) func(http.Handler) http.Handler

SecurityHeaders returns middleware that sets HSTS, X-Frame-Options, and X-Content-Type-Options response headers. HSTS and XCTO apply to every response; XFO is scoped to browser paths because JSON APIs and gRPC do not render in frames.

The HSTS header emits only max-age and includeSubDomains — the preload directive is intentionally omitted because Fly.io .fly.dev is a shared- suffix domain. See .planning/research/FEATURES.md §SEC-7. Negative locked by TestMiddleware_SecurityHeaders_NoPreload.

Types

type CORSInput

type CORSInput struct {
	// AllowedOrigins is a comma-separated list of allowed origins. Use "*" for all origins.
	AllowedOrigins string
}

CORSInput holds configuration for the CORS middleware.

type CSPInput added in v1.12.0

type CSPInput struct {
	// UIPolicy is the CSP directive string applied to /ui/ routes.
	UIPolicy string

	// GraphQLPolicy is the CSP directive string applied to /graphql routes.
	// Typically more permissive than UIPolicy (e.g. allows unsafe-eval for GraphiQL).
	GraphQLPolicy string

	// EnforcingMode selects the CSP header name. When true, the middleware
	// sets "Content-Security-Policy" (enforcing). When false, it sets
	// "Content-Security-Policy-Report-Only" (report-only). The policy
	// strings themselves are unchanged across modes.
	EnforcingMode bool
}

CSPInput holds configuration for the Content-Security-Policy middleware.

type CachingState added in v1.14.0

type CachingState struct {
	// contains filtered or unexported fields
}

CachingState holds the HTTP caching middleware's mutable ETag value behind an atomic pointer so the hot request path can read it without locks or per-request SHA-256 computation. PERF-07 (Plan 55-02) moved the hash work out of the request path; the SHA-256 now runs exactly once per sync completion inside UpdateETag, driven by the sync worker's OnSyncComplete callback. Construct via NewCachingState.

A zero-value etag pointer (never called UpdateETag, i.e. pre-sync startup) is the documented pre-sync state: the Middleware skips Cache-Control and ETag headers and passes the request straight to the inner handler. This preserves the v1.0-v1.12 "no caching headers before first successful sync" semantic verbatim.

func NewCachingState added in v1.14.0

func NewCachingState(syncInterval time.Duration, skipPaths ...string) *CachingState

NewCachingState returns a CachingState with a nil ETag pointer (pre-sync) and the given sync interval. Call UpdateETag once the first sync completes; until then, Middleware skips caching headers.

Any additional skipPaths are treated as exact-match r.URL.Path opt-outs: matching requests receive Cache-Control: no-store, bypass the 304 short-circuit, and always reach the inner handler. Use for pages that contain wall-clock-relative rendering (e.g. "5 minutes ago") which would freeze at cache-creation time under the sync-time-keyed ETag and mislead users for up to a full sync interval.

func (*CachingState) Middleware added in v1.14.0

func (s *CachingState) Middleware() func(http.Handler) http.Handler

Middleware returns the HTTP caching middleware factory. The returned factory captures syncInterval and the pre-computed Cache-Control header value once, then on every request issues exactly one atomic load of the current ETag — no SHA-256, no time formatting, no DB access. If the ETag pointer is nil (pre-sync state), caching headers are skipped and the request passes through unchanged.

Only GET and HEAD requests receive caching treatment; mutation methods pass through untouched. Conditional requests with a matching If-None-Match (or the "*" wildcard) return 304 Not Modified with an empty body.

Paths registered via NewCachingState's skipPaths argument are opted out entirely: they receive Cache-Control: no-store, bypass the 304 short-circuit, and always reach the inner handler.

func (*CachingState) UpdateETag added in v1.14.0

func (s *CachingState) UpdateETag(syncTime time.Time)

UpdateETag computes a fresh weak ETag from syncTime and stores it atomically. Safe to call concurrently with any number of Middleware reads. Intended to be called from the sync worker's OnSyncComplete callback exactly once per successful sync (i.e. once per hour at default settings), not on the request path. The SHA-256 cost thus moves from O(requests) to O(syncs).

type MaxBytesBodyInput added in v1.14.0

type MaxBytesBodyInput struct {
	// MaxBytes is the per-request body size cap in bytes. When a request
	// body exceeds this limit, the next Read call returns *http.MaxBytesError
	// and the ResponseWriter writes 413 Request Entity Too Large.
	MaxBytes int64
}

MaxBytesBodyInput configures the MaxBytesBody middleware.

type PrivacyTierInput added in v1.14.0

type PrivacyTierInput struct {
	// DefaultTier is the visibility tier stamped onto every request
	// context. It is captured by PrivacyTier at construction and never
	// re-read per request; callers mutating the input struct afterwards
	// have no effect.
	DefaultTier privctx.Tier
}

PrivacyTierInput configures the PrivacyTier middleware. DefaultTier is the startup-resolved value from PDBPLUS_PUBLIC_TIER (parsed by internal/config.parsePublicTier).

type SecurityHeadersInput added in v1.14.0

type SecurityHeadersInput struct {
	// HSTSMaxAge is the Strict-Transport-Security max-age duration.
	// Zero disables the HSTS header entirely.
	//
	// Production default in cmd/peeringdb-plus is 365 days.
	// Keep this field explicit so tests and non-production deployments
	// can tune or disable HSTS intentionally.
	HSTSMaxAge time.Duration

	// HSTSIncludeSubDomains appends the includeSubDomains directive.
	// See the SecurityHeaders doc comment for the Fly.io shared-suffix
	// caveat on the HSTS rendering.
	HSTSIncludeSubDomains bool

	// FrameOptions is the X-Frame-Options header value, applied ONLY to
	// browser paths (/, /ui/*, /graphql). Empty disables the header.
	// Recommended: "DENY".
	FrameOptions string

	// ContentTypeOptions sets X-Content-Type-Options: nosniff on ALL
	// responses when true. Important on text/plain error responses.
	ContentTypeOptions bool

	// ReferrerPolicy sets the Referrer-Policy response header when non-empty.
	ReferrerPolicy string

	// CrossOriginOpenerPolicy sets Cross-Origin-Opener-Policy when non-empty.
	CrossOriginOpenerPolicy string

	// CrossOriginResourcePolicy sets Cross-Origin-Resource-Policy when non-empty.
	CrossOriginResourcePolicy string
}

SecurityHeadersInput configures the SecurityHeaders middleware.

Jump to

Keyboard shortcuts

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