Documentation
¶
Overview ¶
Package middleware holds the request pipeline every Līdza app runs: panic recovery, request ids, request logging, per-request deadlines, body limits, CORS and secure headers. Each middleware is a plain func(http.Handler) http.Handler with no state beyond its options.
Index ¶
- Constants
- func AddCSP(policy, directive string, sources ...string) string
- func Chain(h http.Handler, mw ...Middleware) http.Handler
- func ClientIP(r *http.Request) string
- func GetRequestID(ctx context.Context) string
- type CORSOptions
- type Middleware
- func CORS(o CORSOptions) Middleware
- func ClientIdentity(p *Proxies) Middleware
- func Logger(log *slog.Logger) Middleware
- func MaxBody(n int64) Middleware
- func RateLimit(o RateLimitOptions) Middleware
- func Recover(log *slog.Logger) Middleware
- func RequestID() Middleware
- func SecureHeaders(o SecureHeadersOptions) Middleware
- func StreamTimeout(d, stream time.Duration) Middleware
- func Timeout(d time.Duration) Middleware
- type Proxies
- type RateLimitOptions
- type SecureHeadersOptions
Constants ¶
const ( // DefaultPermissionsPolicy turns off the camera, the microphone and // location for the page and every frame in it. DefaultPermissionsPolicy = "camera=(), microphone=(), geolocation=()" // AllowGeolocation lets the app's own pages ask for the visitor's // location (navigator.geolocation); frames from other origins still // cannot. Camera and microphone stay off. AllowGeolocation = "camera=(), microphone=(), geolocation=(self)" )
Permissions-Policy values for SecureHeadersOptions.PermissionsPolicy and lidza.App.PermissionsPolicy.
const ( // DefaultCSP is the policy an app sends outside dev mode when it sets // none: scripts, styles, fonts and connections from the app's own // origin only (WebSockets to it included), images also from data: and // blob: URLs, no plugins, no <base> elsewhere, forms posting only to // the app, and no framing. The built templates hold under it: their // scripts and styles are files, and the JSON <script> blocks a page // carries (JSON-LD, the i18n catalog) are data the policy does not // govern. An app that loads from another origin extends it with // AddCSP. DefaultCSP = "" /* 200-byte string literal not displayed */ // NoCSP sends no Content-Security-Policy at all. NoCSP = "off" )
Content-Security-Policy values for SecureHeadersOptions.CSP and lidza.App.CSP.
const ( // DefaultHSTS is what an app whose APP_URL is https:// sends outside // dev mode: browsers use HTTPS for its host for a year. Subdomains // are left out: they may be other services, so including them is the // domain owner's choice (HSTSSubdomains). DefaultHSTS = "max-age=31536000" // HSTSSubdomains extends the year to every subdomain of the host. HSTSSubdomains = "max-age=31536000; includeSubDomains" // NoHSTS sends no Strict-Transport-Security header. NoHSTS = "off" )
Strict-Transport-Security values for lidza.App.HSTS.
const EnvTrustedProxies = "LIDZA_TRUSTED_PROXIES"
EnvTrustedProxies names the proxies whose forwarded headers count: IPs and CIDRs, comma-separated, plus "loopback" (127.0.0.0/8, ::1) and "private" (10/8, 172.16/12, 192.168/16, fc00::/7). Empty trusts none.
Variables ¶
This section is empty.
Functions ¶
func AddCSP ¶
AddCSP returns policy with sources added to one directive. A fetch directive (script-src, media-src, ...) that policy lacks is created with default-src's sources first, so adding to it keeps what it fell back to. Browsers keep only the first of a repeated directive, so a policy is extended here rather than by appending "; img-src ...":
CSP: middleware.AddCSP(middleware.DefaultCSP, "script-src", "https://js.example.com")
func Chain ¶
func Chain(h http.Handler, mw ...Middleware) http.Handler
Chain applies middlewares so that the first one listed is the outermost.
func ClientIP ¶
ClientIP is the request's client address: the one ClientIdentity resolved (through the trusted proxies), else the connection's. It is the default rate-limit key; never read X-Forwarded-For by hand.
func GetRequestID ¶
GetRequestID returns the request id set by RequestID, or "".
Types ¶
type CORSOptions ¶
type CORSOptions struct {
// Origins are the allowed origins; "*" allows any (not with credentials).
Origins []string
Methods []string // default GET, POST, PUT, PATCH, DELETE
Headers []string // default Content-Type, Authorization, X-Request-ID
// Credentials allows cookies and Authorization on cross-origin calls.
Credentials bool
// MaxAge is how long browsers cache the preflight, default 10 minutes.
MaxAge time.Duration
}
CORSOptions configures CORS. Same-origin apps (the default: the binary serves both the frontend and /api) need none.
type Middleware ¶
Middleware wraps a handler.
func CORS ¶
func CORS(o CORSOptions) Middleware
CORS answers preflights and sets the allow headers for the listed origins. Requests from other origins pass through without CORS headers, which the browser then blocks.
func ClientIdentity ¶
func ClientIdentity(p *Proxies) Middleware
ClientIdentity resolves each request's client address once (Resolve) and puts it in the context, where ClientIP, the rate limiter, the auth throttles and the request log read it. lidza.App installs it with LIDZA_TRUSTED_PROXIES; with none trusted it is the connection's address.
func Logger ¶
func Logger(log *slog.Logger) Middleware
Logger writes one structured line per request: method, path, status, bytes, duration, request id and client_ip (ClientIP: the resolved client, never a raw forwarded header). The path is the URL path only, cut at maxLogPath bytes; the query, headers, cookies and body are never logged.
func MaxBody ¶
func MaxBody(n int64) Middleware
MaxBody caps the request body; reading past n fails with a *http.MaxBytesError, which the typed router reports as 400.
func RateLimit ¶
func RateLimit(o RateLimitOptions) Middleware
RateLimit is a token bucket per key. Over the limit replies 429 with Retry-After. The bucket table is bounded, so a flood of new clients cannot grow memory without limit.
func Recover ¶
func Recover(log *slog.Logger) Middleware
Recover turns a panic into a 500 JSON error and logs the stack, so one bad request never takes the process down.
func RequestID ¶
func RequestID() Middleware
RequestID reads X-Request-ID from the request or generates one, stores it in the context and echoes it in the response.
func SecureHeaders ¶
func SecureHeaders(o SecureHeadersOptions) Middleware
SecureHeaders sets the response headers every app should send.
func StreamTimeout ¶
func StreamTimeout(d, stream time.Duration) Middleware
StreamTimeout is Timeout with a second deadline, stream, for requests that accept only server-sent events (Accept: text/event-stream, as the generated clients send for a router.Stream operation).
func Timeout ¶
func Timeout(d time.Duration) Middleware
Timeout gives every request a deadline through its context. Handlers and the calls they make (database, HTTP, Rust pool) stop at the deadline; the response is not buffered, so streaming still works.
type Proxies ¶
type Proxies struct {
// contains filtered or unexported fields
}
Proxies is the set of trusted proxies ClientIdentity resolves through.
func ParseProxies ¶
ParseProxies reads LIDZA_TRUSTED_PROXIES' format. An entry that is neither an address, a CIDR nor a shorthand is an error, so a typo is not silently ignored.
func (*Proxies) Resolve ¶
Resolve returns the client's address. A peer that is not a trusted proxy is the client, whatever its headers say. From a trusted peer the forwarded addresses are read nearest first: trusted proxies are skipped, and the first address that is not one is the client. A malformed entry stops the walk at the trusted hop before it; when every hop is trusted, the farthest is the client.
type RateLimitOptions ¶
type RateLimitOptions struct {
// RPS is the sustained rate per key; Burst the bucket size.
RPS float64
Burst int
// Key groups requests; default is ClientIP, the client's address
// through the trusted proxies (ClientIdentity, LIDZA_TRUSTED_PROXIES).
Key func(r *http.Request) string
// MaxKeys bounds the table of buckets; when reached, buckets idle for
// the longest are evicted. Default 100000.
MaxKeys int
}
RateLimitOptions configures RateLimit.
type SecureHeadersOptions ¶
type SecureHeadersOptions struct {
// CSP is the Content-Security-Policy value; empty or NoCSP sends
// none. Vite's dev server injects inline scripts, so a strict policy
// is for production builds; lidza.App sends DefaultCSP there unless
// it sets its own.
CSP string
// HSTS enables Strict-Transport-Security: HSTSPolicy, or a year with
// the subdomains when that is empty. Only for an app served over
// HTTPS (browsers ignore it over HTTP).
HSTS bool
// HSTSPolicy is the Strict-Transport-Security value when HSTS is set.
HSTSPolicy string
// PermissionsPolicy is the Permissions-Policy value; empty sends
// DefaultPermissionsPolicy, which turns off the camera, the
// microphone and location. An app that asks for one of them names it
// for its own origin: AllowGeolocation for location.
PermissionsPolicy string
}
SecureHeadersOptions configures SecureHeaders.