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 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 Maintenance(cfg MaintenanceConfig) Middleware
- func MethodOverride() 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 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 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 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 http(s) URL or
// a path. 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) 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.