middleware

package
v1.23.0 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: BSD-3-Clause Imports: 22 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. 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 and minimum size thresholds automatically. ETag suffixing (gzhttp.SuffixETag) is deliberately NOT configured: our ETags are weak (W/ prefix — semantic, not byte-for-byte, equivalence), so the same tag is correct for both the plain and gzipped representation and a --gzip suffix would only fragment client caches.

func Logging

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

Logging returns middleware that logs each HTTP request with method, path, query, status, duration, and trace context (trace_id, span_id) when available. It also attaches the query string to the active server span as url.query. url.path alone left /api traffic uninspectable on both surfaces — the caller's actual intent (filters, since, depth, pagination) lives in the query string — so it is recorded on the access log and the trace alike, covering every API surface from one place. Uses structured slog with LogAttrs for an 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.

Observability contract:

  • 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 RESTError added in v1.23.0

func RESTError(next http.Handler) http.Handler

RESTError wraps entrest error responses in RFC 9457 Problem Details format. It buffers non-2xx entrest bodies, then rewrites them as application/problem+json — preserving entrest's client-actionable error message (e.g. "per_page 0 is out of bounds, must be >= 1") as the problem Detail for 4xx responses. 5xx detail is dropped: those messages can carry SQL/driver internals that must not reach anonymous callers.

func RESTFieldRedact added in v1.23.0

func RESTFieldRedact(next http.Handler) http.Handler

RESTFieldRedact removes the `ixf_ixp_member_list_url` key from /rest/v1/ JSON responses when the caller's ctx tier does not admit the field (per internal/privfield.Redact).

entrest has no native per-field conditional-omission hook (verified against the lrstanley/entrest annotation reference and local behavior notes Finding #1). This middleware is the workaround: it buffers the response body, parses the JSON, and re-emits with the field deleted wherever privfield.Redact says omit.

Scope: ALL /rest/v1/ paths, with the gated object located by a recursive walk rather than by response shape. entrest eager-loads the ixlan edge unconditionally (ent/rest/eagerload.go), so ixlan objects appear nested under edges.ix_lans / edges.ix_lan on internet-exchange, ix-prefix, and network-ix-lan responses — scoping redaction to the flat /rest/v1/ix-lans* paths leaked the gated URL through every embedding endpoint (2026-06-10 audit). Non-JSON bodies pass through unchanged.

Single exemption: the exact path /rest/v1/openapi.json. The spec is a static document patched once at startup — it describes the `ixf_ixp_member_list_url` SCHEMA but can never contain the `_visible` companion as a data key, so the buffer+parse+walk over its ~1 MB body is pure overhead on every fetch. This is an exact-path skip of a provably-static document, NOT the entity path-scoping that caused the 2026-06-10 leak.

Ordering: this middleware MUST be wrapped INSIDE RESTError so that problem+json error bodies pass through without being mis-parsed as data payloads.

Required for privacy-redaction correctness.

func Readiness added in v1.23.0

func Readiness(sr SyncReadiness, next http.Handler) http.Handler

Readiness returns 503 for all routes except infrastructure paths until the first sync has completed. Browser requests receive a styled HTML syncing page instead of JSON.

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 an RFC 9457 problem-detail 500 to the client.

http.ErrAbortHandler is re-panicked, not swallowed: it is net/http's sanctioned way for a handler to abort a response (http.ServeContent uses it when a client disconnects mid-body), and the server suppresses its stack trace by contract. Recovering it would log a scary pseudo-panic for every torn-down client connection and attempt a 500 write on a dead ResponseWriter.

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 the project history. 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. The hash work was moved 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.

type SyncReadiness added in v1.23.0

type SyncReadiness interface {
	HasCompletedSync() bool
}

SyncReadiness reports whether at least one sync has completed.

Jump to

Keyboard shortcuts

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