Documentation
¶
Overview ¶
Package middleware provides Vukem's built-in net/http middleware. Every constructor returns a func(http.Handler) http.Handler, so they compose with any other middleware. Errors are rendered through web.Fail, which uses the router's error handler (problem+json, HTML or htmx fragments).
The set covers Recover, RequestID, Logger, Timeout, BodyLimit, MethodOverride, JSONOnly, SecureHeaders (CSP with nonces, HSTS), CORS, Gzip, Static and Maintenance. vukem.DefaultMiddleware installs the secure web stack; vukem.APIMiddleware the API one.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/http.md.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/AlexGalhardo/vukem/middleware"
)
func main() {
hello := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "hello")
})
// Middleware are plain func(http.Handler) http.Handler values, so they
// compose by hand or through router.Use.
h := middleware.Recover()(
middleware.RequestID()(
middleware.SecureHeaders(middleware.SecureHeadersConfig{})(hello),
),
)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
fmt.Println(rec.Code, rec.Body.String())
fmt.Println(rec.Header().Get("X-Frame-Options"), rec.Header().Get("X-Content-Type-Options"))
fmt.Println(rec.Header().Get("X-Request-Id") != "")
}
Output: 200 hello DENY nosniff true
Index ¶
- Constants
- func CSPNonce(ctx context.Context) string
- func WriteMaintenance(file string, m MaintenanceState) error
- type CORSConfig
- type LoggerOptions
- type MaintenanceConfig
- type MaintenanceState
- type Middleware
- func BodyLimit(n int64) Middleware
- func CORS(cfg CORSConfig) (Middleware, error)
- func Gzip() Middleware
- func JSONOnly() Middleware
- func Logger(log *slog.Logger) Middleware
- func LoggerWith(log *slog.Logger, o LoggerOptions) Middleware
- func Maintenance(cfg MaintenanceConfig) Middleware
- func MethodOverride() Middleware
- func NoIndex() Middleware
- func Recover() Middleware
- func RequestID() Middleware
- func SecureHeaders(cfg SecureHeadersConfig) Middleware
- func Static(root *os.Root, cfg StaticConfig) Middleware
- func StaticFS(fsys fs.FS, cfg StaticConfig) Middleware
- func Timeout(d time.Duration) Middleware
- type SecureHeadersConfig
- type StaticConfig
Examples ¶
Constants ¶
const DefaultCSP = "default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'nonce-{nonce}'; " +
"img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; " +
"frame-ancestors 'none'; form-action 'self'"
DefaultCSP is a strict policy suitable for server-rendered pages with nonce'd scripts and styles.
const DefaultMaintenanceFile = "storage/framework/down"
DefaultMaintenanceFile is where `vukem down` writes the maintenance state.
const MaintenanceCookie = "vukem_maintenance"
MaintenanceCookie carries the bypass granted by visiting /<secret>.
const RequestIDHeader = "X-Request-ID"
RequestIDHeader is the header carrying the request id.
Variables ¶
This section is empty.
Functions ¶
func CSPNonce ¶
CSPNonce returns the per-request CSP nonce, or "" when SecureHeaders did not run. Templates expose it as {{ cspNonce }}.
func WriteMaintenance ¶ added in v0.18.0
func WriteMaintenance(file string, m MaintenanceState) error
WriteMaintenance puts the app in maintenance mode (vukem down).
Types ¶
type CORSConfig ¶
type CORSConfig struct {
// AllowedOrigins lists exact origins ("https://app.example.com") or "*".
AllowedOrigins []string
// AllowedMethods defaults to GET, HEAD, POST, PUT, PATCH, DELETE.
AllowedMethods []string
// AllowedHeaders defaults to Accept, Authorization, Content-Type,
// X-Requested-With, X-CSRF-Token.
AllowedHeaders []string
// ExposedHeaders are readable by the browser.
ExposedHeaders []string
// AllowCredentials allows cookies/Authorization; incompatible with "*".
AllowCredentials bool
// MaxAge caches preflight responses.
MaxAge time.Duration
}
CORSConfig configures cross-origin resource sharing. The zero value allows no cross-origin requests (deny by default).
type LoggerOptions ¶ added in v0.34.0
type LoggerOptions struct {
// TruncateIP logs the /24 network of IPv4 addresses and the /48 of
// IPv6 ones in peer and ip (203.0.113.0, 2001:db8:1234::) instead of
// the address, for data-minimising logs (LOG_TRUNCATE_IP). Rate limits
// and c.IP() keep the full address.
TruncateIP bool
}
LoggerOptions configures LoggerWith.
type MaintenanceConfig ¶ added in v0.18.0
type MaintenanceConfig struct {
// File is the state file (default DefaultMaintenanceFile).
File string
// Key signs bypass cookies (derive it from APP_KEY). Without a key the
// secret bypass is disabled: an unkeyed HMAC would be forgeable.
Key []byte
// PreviousKeys still accept bypass cookies signed before an APP_KEY
// rotation (derive them from APP_PREVIOUS_KEYS); such a cookie is
// re-issued under Key.
PreviousKeys [][]byte
// Except lists paths always served (default "/up").
Except []string
// Secure marks the bypass cookie Secure (production, HTTPS).
Secure bool
}
MaintenanceConfig configures Maintenance.
type MaintenanceState ¶ added in v0.18.0
type MaintenanceState struct {
// Secret, when set, lets visitors of /<secret> through (stored hashed).
Secret string `json:"-"`
// Retry is sent as Retry-After.
Retry time.Duration `json:"-"`
SecretHash string `json:"secret_hash,omitempty"`
RetryAfter int `json:"retry_after,omitempty"`
Since time.Time `json:"since"`
}
MaintenanceState is the state written by `vukem down`.
type Middleware ¶
Middleware wraps an http.Handler.
func BodyLimit ¶
func BodyLimit(n int64) Middleware
BodyLimit caps request bodies at n bytes; reading a larger body fails with *http.MaxBytesError, which the web helpers (Input, BindJSON, Parse, uploads) answer with 413. A declared Content-Length over the limit fails on the first read, before any byte is consumed.
A later BodyLimit, web.LimitBody or upload.Rules.MaxSize on the same request replaces the limit instead of nesting under it, so the router-wide default set by vukem.DefaultMiddleware can be raised or lowered per route. The check runs when the body is read, against the innermost limit: a handler that never reads the body gets no 413.
Example ¶
package main
import (
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"github.com/AlexGalhardo/vukem/middleware"
"github.com/AlexGalhardo/vukem/web"
)
func main() {
h := middleware.BodyLimit(16)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c := web.NewCtx(w, r, &web.Services{Log: slog.New(slog.DiscardHandler)})
var v map[string]any
if err := c.BindJSON(&v); err != nil {
c.HandleError(err)
return
}
_ = c.NoContent()
}))
// Reading a body over the limit fails with 413. A declared
// Content-Length over it fails on the first read, before any byte is
// consumed; undeclared bodies are cut off while reading.
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"a":"`+strings.Repeat("x", 64)+`"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Println(rec.Code)
}
Output: 413
func CORS ¶
func CORS(cfg CORSConfig) (Middleware, error)
CORS returns CORS middleware. Register it as server middleware so preflight requests are answered before routing. It returns an error for insecure configurations ("*" with credentials), so misconfiguration fails at boot (A02).
func Gzip ¶
func Gzip() Middleware
Gzip compresses text-like responses for clients that accept gzip. Bodies smaller than 1 KiB, already-encoded responses, ranges and event streams are passed through.
func JSONOnly ¶
func JSONOnly() Middleware
JSONOnly makes a route group JSON-only: write requests must send JSON (415 otherwise) and errors render as problem+json.
func Logger ¶
func Logger(log *slog.Logger) Middleware
Logger logs one structured line per request (method, path, status, duration, size, request id, peer address and client IP). peer is the direct connection (RemoteAddr host); ip is the client resolved through web.Services.TrustedProxies, as c.IP() returns it. 5xx log at ERROR, 4xx at WARN.
func LoggerWith ¶ added in v0.34.0
func LoggerWith(log *slog.Logger, o LoggerOptions) Middleware
LoggerWith is Logger with options.
func Maintenance ¶ added in v0.18.0
func Maintenance(cfg MaintenanceConfig) Middleware
Maintenance answers 503 (Retry-After, the "errors/503" view or problem+json) while the state file exists. Visiting /<secret> sets a signed bypass cookie. A file that cannot be read keeps the app down.
func MethodOverride ¶
func MethodOverride() Middleware
MethodOverride lets HTML forms send PUT, PATCH and DELETE through a hidden "_method" field on POST. Multipart forms put it in the action's query string instead ("?_method=PUT"), so their body is never buffered. Register it as server middleware so routing sees the overridden method.
func NoIndex ¶ added in v0.37.0
func NoIndex() Middleware
NoIndex sets X-Robots-Tag: noindex, nofollow on every response, so search engines index nothing the handlers serve (a staging site).
func Recover ¶
func Recover() Middleware
Recover converts panics into 500 responses rendered by the error handler and logs the stack trace server-side only (A10). http.ErrAbortHandler is re-raised so net/http can abort the connection.
func RequestID ¶
func RequestID() Middleware
RequestID reuses a well-formed incoming X-Request-ID or generates one, stores it in the context (web.RequestID) and echoes it in the response.
func SecureHeaders ¶
func SecureHeaders(cfg SecureHeadersConfig) Middleware
SecureHeaders sets security headers on every response (A02).
func Static ¶
func Static(root *os.Root, cfg StaticConfig) Middleware
Static serves files from root when they exist and passes other requests on. Files are opened through os.Root, so ".." and symlinks cannot escape the directory (A01); dotfiles and directory listings are never served. The caller owns root and closes it on shutdown.
Files under ImmutablePrefixes ("/build/" by default) and fingerprinted names get a one-year immutable cache; every other file must revalidate. A name is fingerprinted when its last "-" or "." segment before the extension is 8+ lowercase hex characters with a digit and a letter (app-3f9a1c2b.js, app.4f2a9c1e.css) or 8 base64url characters with a digit, an upper and a lower case letter (Vite's index-BkS3pZ8_.js); hero-mountains.jpg or report-20240101.pdf are not. Revalidation uses Last-Modified, or an ETag (a content hash) for files without a modification time, such as embed.FS files.
func StaticFS ¶
func StaticFS(fsys fs.FS, cfg StaticConfig) Middleware
StaticFS is Static over any fs.FS (e.g. an embed.FS in production).
func Timeout ¶
func Timeout(d time.Duration) Middleware
Timeout sets a deadline on the request context. Handlers and queries that respect the context stop when it expires.
type SecureHeadersConfig ¶
type SecureHeadersConfig struct {
// CSP is the Content-Security-Policy; "{nonce}" is replaced with a
// per-request nonce. Empty uses DefaultCSP; "-" disables the header.
CSP string
// HSTSMaxAge enables Strict-Transport-Security on HTTPS requests.
// Zero uses two years; negative disables it.
HSTSMaxAge time.Duration
// HSTSForce sends Strict-Transport-Security on every response that
// reaches SecureHeaders, also on requests that look like plain HTTP
// (middleware installed before it, such as Maintenance's 503, answers
// without it). Use it only when every public
// request reaches a TLS-terminating proxy whose range cannot be
// trusted (so TrustForwardedProto cannot be used): it asserts the
// public origin is HTTPS-only instead of checking each request.
// Browsers ignore HSTS received over plain HTTP (RFC 6797 section 8.1).
HSTSForce bool
// FrameOptions defaults to DENY; "-" disables it.
FrameOptions string
// ReferrerPolicy defaults to strict-origin-when-cross-origin.
ReferrerPolicy string
// PermissionsPolicy defaults to camera=(), microphone=(), geolocation=().
PermissionsPolicy string
// TrustForwardedProto treats X-Forwarded-Proto: https as HTTPS (set it
// only behind a trusted proxy).
TrustForwardedProto bool
// TrustedProxies, when non-empty, limits TrustForwardedProto to
// requests whose direct peer is in one of these networks, so clients
// that reach the app around the proxy cannot claim HTTPS.
TrustedProxies []*net.IPNet
// DevServer returns the origin of a running asset dev server (the Vite
// hot URL) or "". When set, the origin is allowed in script-src,
// style-src, img-src, font-src and connect-src (plus its ws:// form for
// hot reload). Wire it only outside production.
DevServer func() string
// CSPSources adds sources to directives of the policy, e.g. an
// analytics origin in script-src and connect-src, or the 'sha256-...'
// hash of a style a library injects. A "-src" directive missing from
// the policy starts from the default-src sources. Unsafe sources
// ('unsafe-*' keywords, "*", nonces, scheme-only sources in script
// directives) are refused: write a full CSP to opt into them.
CSPSources map[string][]string
// ReportURI receives CSP violation reports: an absolute https URL or
// a path (plain http is refused at boot). It adds report-uri and report-to directives and a
// Reporting-Endpoints header naming the "csp-endpoint" group.
ReportURI string
}
SecureHeadersConfig configures SecureHeaders. Zero values use the secure defaults documented in docs/security.md.
func (*SecureHeadersConfig) AddCSPSources ¶ added in v0.37.0
func (cfg *SecureHeadersConfig) AddCSPSources(directive string, sources ...string)
AddCSPSources adds sources to directive in CSPSources, keeping the sources already there (the ones Vukem's integrations put in) and skipping duplicates. Prefer it to assigning CSPSources, which drops them.
func (SecureHeadersConfig) MissingCSPSources ¶ added in v0.37.0
func (cfg SecureHeadersConfig) MissingCSPSources(want map[string][]string) map[string][]string
MissingCSPSources returns the sources of want, by directive, that the policy SecureHeaders would send does not allow (a "-src" directive the policy lacks is checked against its fallback, such as default-src), or nil when every one is allowed or the CSP is disabled. Pass an integration's CSPSources() to check it was not dropped.
func (SecureHeadersConfig) Validate ¶ added in v0.24.0
func (cfg SecureHeadersConfig) Validate() error
Validate reports TrustForwardedProto without TrustedProxies, HSTSForce with HSTS disabled, and CSPSources and ReportURI values that SecureHeaders would drop. Call it at boot: SecureHeaders never fails, it serves the policy without the invalid extras (stricter, never weaker).
type StaticConfig ¶
type StaticConfig struct {
// Prefix is the URL prefix ("/" serves files at the root).
Prefix string
// ImmutablePrefixes get a one-year immutable cache (hashed build output).
// Elsewhere only fingerprinted names (see Static) are immutable.
ImmutablePrefixes []string
}
StaticConfig configures Static.