Documentation
¶
Overview ¶
Package middleware provides composable github.com/gorilla/mux middleware for building server-side-rendered web applications.
Each constructor returns a github.com/gorilla/mux.MiddlewareFunc (an http.Handler decorator), so they can be registered on a router with Use or chained by hand. Most ordering is flexible, but a few have prerequisites, noted on each function. A typical chain is:
Recovery → PopulateRequestID → PopulateTraceID → PopulateLogger → SecureHeaders → GzipResponse → RequireSession → HandleCSRF → PopulateTemplateVariables → InjectCurrentPath → ProcessLocale
Authentication is intentionally pluggable: implement the Authenticator interface (for example, an OIDC client) and install RequireAuthenticated. The framework itself stays free of any specific auth provider dependency.
Index ¶
- Constants
- func AddOperatingSystemFromUserAgent() mux.MiddlewareFunc
- func CheckSessionIdleNoAuth(idleTTL time.Duration, onIdle http.HandlerFunc) mux.MiddlewareFunc
- func ConfigureStaticAssets(devMode bool) mux.MiddlewareFunc
- func ContentSecurityPolicy(policy string) mux.MiddlewareFunc
- func GzipResponse() mux.MiddlewareFunc
- func HandleCSRF(h *render.Renderer) mux.MiddlewareFunc
- func InjectCurrentPath() mux.MiddlewareFunc
- func LogRequests() mux.MiddlewareFunc
- func MutateMethod() mux.MiddlewareFunc
- func OnlyIfEnabled(enabled bool, h *render.Renderer) mux.MiddlewareFunc
- func PopulateLogger(originalLogger *slog.Logger) mux.MiddlewareFunc
- func PopulateRequestID(h *render.Renderer) mux.MiddlewareFunc
- func PopulateTemplateVariables(cfg TemplateConfig) mux.MiddlewareFunc
- func PopulateTraceID() mux.MiddlewareFunc
- func ProcessDebug(buildID, buildTag string) mux.MiddlewareFunc
- func ProcessLocale(p LocaleProvider) mux.MiddlewareFunc
- func ProcessNonce() mux.MiddlewareFunc
- func Recovery(h *render.Renderer) mux.MiddlewareFunc
- func RequireAuthenticated(a Authenticator, h *render.Renderer) mux.MiddlewareFunc
- func RequireHeader(header string, h *render.Renderer) mux.MiddlewareFunc
- func RequireHeaderValues(header string, allowed []string, h *render.Renderer) mux.MiddlewareFunc
- func RequireHostHeader(allowed []string, h *render.Renderer, stripPort bool) mux.MiddlewareFunc
- func RequireNamedSession(store sessions.Store, name string, splitValues []any, h *render.Renderer) mux.MiddlewareFunc
- func RequireSession(store sessions.Store, splitValues []any, h *render.Renderer) mux.MiddlewareFunc
- func SecureHeaders(devMode bool, serverType ServerType) mux.MiddlewareFunc
- type Authenticator
- type Error
- type LocaleProvider
- type Path
- type ServerType
- type TemplateConfig
Examples ¶
Constants ¶
const ( // CSRFHeaderField is the header carrying the CSRF token. CSRFHeaderField = "X-CSRF-Token" // CSRFFormField is the form field carrying the CSRF token. CSRFFormField = "csrf_token" // CSRFFormFieldTemplate renders a hidden CSRF form input. CSRFFormFieldTemplate = `<input type="hidden" name="%s" value="%s" />` // CSRFMetaTagName is the meta tag name used by JavaScript to read the token. CSRFMetaTagName = "csrf-token" // CSRFMetaTagTemplate renders the CSRF meta tag. CSRFMetaTagTemplate = `<meta name="%s" content="%s">` // TokenLength is the length of the CSRF token, in bytes. TokenLength = 64 )
const ( ErrMissingExistingToken = Error("missing existing csrf token in session") ErrMissingIncomingToken = Error("missing csrf token in request") ErrInvalidToken = Error("invalid csrf token") )
CSRF-related sentinel errors.
const ( // HeaderDebug is the request header that, when present with any value, // triggers debug response headers. HeaderDebug = "x-debug" // HeaderDebugBuildID is the response header carrying the build ID. HeaderDebugBuildID = "x-build-id" // HeaderDebugBuildTag is the response header carrying the build tag. HeaderDebugBuildTag = "x-build-tag" )
const ( // HeaderAcceptLanguage is the standard content-negotiation language header. HeaderAcceptLanguage = "Accept-Language" // QueryKeyLanguage is the query parameter that overrides the language. QueryKeyLanguage = "lang" // LeftAlign and RightAlign are the text-direction values placed on the // template map for use in templates (e.g. the html dir attribute). LeftAlign = "ltr" RightAlign = "rtl" )
const CSPNoncePlaceholder = "{{nonce}}"
CSPNoncePlaceholder is the token in a Content-Security-Policy template that ContentSecurityPolicy replaces with the request's nonce.
const TraceHeader = "X-Cloud-Trace-Context"
TraceHeader is the request header carrying distributed-trace context. It matches the header injected by Google Cloud load balancers, but any upstream that sets it will be honored.
Variables ¶
This section is empty.
Functions ¶
func AddOperatingSystemFromUserAgent ¶
func AddOperatingSystemFromUserAgent() mux.MiddlewareFunc
AddOperatingSystemFromUserAgent inspects the request's User-Agent and stores the inferred client operating system (webctx.OSIOS, webctx.OSAndroid, or webctx.OSUnknown) on the context.
func CheckSessionIdleNoAuth ¶
func CheckSessionIdleNoAuth(idleTTL time.Duration, onIdle http.HandlerFunc) mux.MiddlewareFunc
CheckSessionIdleNoAuth enforces an idle-timeout on the session independent of authentication. If the time since the session's last activity exceeds idleTTL, onIdle is invoked (e.g. to redirect to a login or logout page, or to write a 401) and the request does not proceed. Use it on routes that have no other auth check; routes behind RequireAuthenticated can enforce idleness there instead. Install it after RequireSession.
func ConfigureStaticAssets ¶ added in v0.2.0
func ConfigureStaticAssets(devMode bool) mux.MiddlewareFunc
ConfigureStaticAssets prepares responses for static assets: it sets cache headers (never cached in dev mode so edits show up on reload; publicly cacheable for a week otherwise — safe because the renderer's asset tags append the build ID as a cache-busting query string, see github.com/mikehelmick/go-bananas/render.WithBuildID) and rejects directory requests (paths ending in "/") with a 404, so a wrapped http.FileServerFS never emits auto-generated directory listings.
Pair it with a file-serving handler. With templates and assets in an embed.FS (the layout the renderer expects), wire it as:
static := middleware.ConfigureStaticAssets(devMode)
r.PathPrefix("/static/").Handler(static(http.FileServerFS(assets)))
For defense in depth, consider serving an io/fs.Sub of just the static subtree so a routing mistake can never expose templates or other embedded files.
func ContentSecurityPolicy ¶ added in v0.2.0
func ContentSecurityPolicy(policy string) mux.MiddlewareFunc
ContentSecurityPolicy sets the Content-Security-Policy response header to policy on every request. Any occurrence of CSPNoncePlaceholder in the policy is replaced with the per-request nonce generated by ProcessNonce, so the header and templates (which read the nonce via webctx.NonceFromContext) share a single nonce:
r.Use(middleware.ProcessNonce())
r.Use(middleware.ContentSecurityPolicy(
"default-src 'self'; script-src 'self' 'nonce-{{nonce}}'; object-src 'none'"))
When the policy contains the placeholder, install this middleware after ProcessNonce. If no nonce is on the context, the nonce'd source entries are dropped from the emitted header (and a warning is logged) rather than emitting an empty 'nonce-' source.
Example ¶
ExampleContentSecurityPolicy sets a static policy. To include a per-request nonce, add the {{nonce}} placeholder and install ProcessNonce first: "script-src 'self' 'nonce-{{nonce}}'".
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/mikehelmick/go-bananas/middleware"
)
func main() {
h := middleware.ContentSecurityPolicy("default-src 'self'; object-src 'none'")(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
fmt.Println(w.Header().Get("Content-Security-Policy"))
}
Output: default-src 'self'; object-src 'none'
func GzipResponse ¶
func GzipResponse() mux.MiddlewareFunc
GzipResponse gzip-compresses responses for clients that advertise gzip support via the Accept-Encoding header.
func HandleCSRF ¶
func HandleCSRF(h *render.Renderer) mux.MiddlewareFunc
HandleCSRF manages per-request CSRF tokens. It reads (or generates and stores) a token on the session, exposes masked token helpers on the template map ("csrfToken", "csrfField", "csrfMeta", "csrfHeaderField", "csrfMetaTagName"), and, for mutating methods, verifies the incoming token. It must be installed after RequireSession.
func InjectCurrentPath ¶
func InjectCurrentPath() mux.MiddlewareFunc
InjectCurrentPath stores the request's path on the template map as a *Path under the key "currentPath", so templates can reason about the active route.
func LogRequests ¶ added in v0.2.0
func LogRequests() mux.MiddlewareFunc
LogRequests emits one structured Info log line per request with the method, path, response status, bytes written, and duration. It logs through logging.FromContext, so when installed after PopulateLogger each line is automatically tagged with the request (and trace) ID.
r.Use(middleware.PopulateLogger(logging.DefaultLogger())) r.Use(middleware.LogRequests())
func MutateMethod ¶
func MutateMethod() mux.MiddlewareFunc
MutateMethod lets HTML forms emulate verbs other than GET and POST by supplying a "_method" form value (e.g. PATCH or DELETE), which is then used as the request method before routing. It must be installed very early in the chain, before the router matches a route.
func OnlyIfEnabled ¶
func OnlyIfEnabled(enabled bool, h *render.Renderer) mux.MiddlewareFunc
OnlyIfEnabled hides routes behind a 404 when enabled is false, so a feature can be toggled off without removing its routes or revealing their existence.
func PopulateLogger ¶
func PopulateLogger(originalLogger *slog.Logger) mux.MiddlewareFunc
PopulateLogger stores a request-scoped logger on the context, enriched with the request ID (and trace ID, if present) so every log line within the request can be correlated. Install it after PopulateRequestID and PopulateTraceID.
If an upstream middleware already placed a non-default logger on the context, that logger is used as the base instead of originalLogger.
func PopulateRequestID ¶
func PopulateRequestID(h *render.Renderer) mux.MiddlewareFunc
PopulateRequestID assigns a random UUID request ID to the context if one is not already present, so it can be logged and surfaced in templates. Install it before PopulateLogger.
func PopulateTemplateVariables ¶
func PopulateTemplateVariables(cfg TemplateConfig) mux.MiddlewareFunc
PopulateTemplateVariables seeds the template map with common values (server name, endpoint, build identifiers, dev-mode flag, and any Extra values) and bootstraps the map so later middleware can add to it. Install it after the session middleware and before handlers render.
func PopulateTraceID ¶
func PopulateTraceID() mux.MiddlewareFunc
PopulateTraceID extracts the trace ID from the trace header (the portion before the first "/") and stores it on the context, if present, well-formed (alphanumeric/dash/underscore, at most 128 characters), and not already set. Install it before PopulateLogger.
func ProcessDebug ¶
func ProcessDebug(buildID, buildTag string) mux.MiddlewareFunc
ProcessDebug echoes the provided build identifiers in response headers when the request includes the "X-Debug" header with any value. This aids debugging without exposing build information to ordinary clients.
func ProcessLocale ¶
func ProcessLocale(p LocaleProvider) mux.MiddlewareFunc
ProcessLocale resolves the request locale via p and stores the translator on the context (for the "t"/"tDefault" template functions) along with template map values "locale", "acceptLanguage", "textLanguage", and "textDirection". Install it after the session/template middleware.
func ProcessNonce ¶
func ProcessNonce() mux.MiddlewareFunc
ProcessNonce generates a fresh, cryptographically-random Content-Security- Policy nonce per request and stores it on the context, where templates can read it (via webctx.NonceFromContext) to mark trusted inline scripts/styles.
The nonce is generated server-side; it is deliberately NOT read from a request header, because a client-controlled nonce would let an attacker predict it and defeat the CSP. To take effect, emit a Content-Security-Policy header that references the same nonce — ContentSecurityPolicy does this via its CSPNoncePlaceholder when installed after this middleware.
func Recovery ¶
func Recovery(h *render.Renderer) mux.MiddlewareFunc
Recovery recovers from panics in downstream handlers, logging the panic in a structured format and returning a 500 to the client so the server keeps running. It is typically the outermost middleware in the chain.
Example ¶
ExampleRecovery demonstrates assembling the recommended middleware chain on a gorilla/mux router. The order matters: Recovery is outermost, request/trace IDs and the logger come next, then security and session middleware.
package main
import (
"fmt"
"net/http"
"testing/fstest"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/mikehelmick/go-bananas/cookiestore"
"github.com/mikehelmick/go-bananas/logging"
"github.com/mikehelmick/go-bananas/middleware"
"github.com/mikehelmick/go-bananas/render"
)
func main() {
h, err := render.New(fstest.MapFS{
"500.html": &fstest.MapFile{Data: []byte(`{{define "500"}}error{{end}}`)},
})
if err != nil {
panic(err)
}
store := cookiestore.New(func() ([][]byte, error) {
return [][]byte{make([]byte, 64)}, nil
}, &sessions.Options{Path: "/"})
r := mux.NewRouter()
r.Use(middleware.Recovery(h))
r.Use(middleware.PopulateRequestID(h))
r.Use(middleware.PopulateTraceID())
r.Use(middleware.PopulateLogger(logging.DefaultLogger()))
r.Use(middleware.SecureHeaders(false, middleware.ServerTypeHTML))
r.Use(middleware.GzipResponse())
r.Use(middleware.RequireSession(store, nil, h))
r.Use(middleware.HandleCSRF(h))
r.Use(middleware.InjectCurrentPath())
r.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "hello")
})
fmt.Println("router configured")
}
Output: router configured
func RequireAuthenticated ¶
func RequireAuthenticated(a Authenticator, h *render.Renderer) mux.MiddlewareFunc
RequireAuthenticated rejects anonymous requests. It calls a's Authenticate: a non-nil error renders a 500; a nil principal renders a 401; otherwise the principal is stored on the context (see webctx.PrincipalFromContext) and the request proceeds.
The plumbing an OIDC flow needs is provided by other middleware in this package: RequireSession for token/claims storage, HandleCSRF, SecureHeaders, and CheckSessionIdleNoAuth for idle expiry.
func RequireHeader ¶
func RequireHeader(header string, h *render.Renderer) mux.MiddlewareFunc
RequireHeader requires that the request carry the named header with any non-empty value, returning 401 otherwise.
func RequireHeaderValues ¶
RequireHeaderValues requires that the request carry the named header with a value matching one of allowed, returning 401 otherwise.
func RequireHostHeader ¶
RequireHostHeader requires that the request's Host header match one of allowed (case-insensitively), returning 401 otherwise. When stripPort is true, any port in the Host header is ignored during comparison.
func RequireNamedSession ¶
func RequireNamedSession(store sessions.Store, name string, splitValues []any, h *render.Renderer) mux.MiddlewareFunc
RequireNamedSession is like RequireSession but uses a specific session (cookie) name instead of the default.
func RequireSession ¶
RequireSession loads or creates a session from store, stores it on the request context, and ensures the session's flash data is available to templates. Any handler that uses sessions, flash messages, or CSRF must be wrapped with this.
splitValues names session keys whose values are large enough to warrant their own cookie (browsers cap individual cookies at ~4KB); each listed key is split out into a separate companion cookie on save and rejoined on load.
func SecureHeaders ¶
func SecureHeaders(devMode bool, serverType ServerType) mux.MiddlewareFunc
SecureHeaders installs a sensible set of security-related response headers (HSTS, nosniff, referrer policy, and, for HTML servers, frame denial). When devMode is true, HTTPS redirects and HSTS are relaxed for local development.
It deliberately does NOT set a Content-Security-Policy: a useful CSP is highly application-specific. Add one with ContentSecurityPolicy, combined with ProcessNonce for per-request nonces on trusted inline scripts/styles.
Types ¶
type Authenticator ¶
type Authenticator interface {
// Authenticate returns the authenticated principal for the request, or
// (nil, nil) if the request is anonymous (no credentials presented). A
// non-nil error indicates the authentication attempt itself failed (for
// example, an identity provider was unreachable) and results in a 500.
Authenticate(r *http.Request) (principal any, err error)
}
Authenticator authenticates an HTTP request. It is the single seam through which applications plug in an identity provider (OIDC, an API key scheme, a signed token, etc.) without the framework depending on any of them.
Implementations typically read a session, cookie, or header populated by an earlier login flow. The returned principal is opaque to the framework: its concrete type is the application's own user/session model, retrievable later with webctx.PrincipalFromContext.
type LocaleProvider ¶
type LocaleProvider interface {
// Lookup returns the best translator for the given ordered hints (typically
// the "lang" query parameter followed by the Accept-Language header), along
// with its BCP-47 language tag (e.g. "en", "ar"). It may return a nil
// translator, in which case templates fall back to default strings.
Lookup(hints ...string) (t gotext.Translator, lang string)
}
LocaleProvider resolves the best gotext.Translator for a request. It is the pluggable seam for internationalization: the framework does not prescribe how translations are loaded, only how the chosen translator reaches templates.
type Path ¶
type Path struct {
// contains filtered or unexported fields
}
Path wraps the request URL and is placed on the template map under "currentPath" by InjectCurrentPath. Its methods make it easy for templates to highlight the active nav item.
type ServerType ¶
type ServerType string
ServerType describes the kind of responses a server emits, which tweaks a few secure-header defaults (for example, whether to deny framing).
const ( // ServerTypeHTML is for servers that render HTML pages; it enables // clickjacking protection (X-Frame-Options: DENY). ServerTypeHTML ServerType = "html" // ServerTypeAPI is for servers that emit machine-readable responses. ServerTypeAPI ServerType = "api" )
type TemplateConfig ¶
type TemplateConfig struct {
// ServerName is the application name; it is used as the default page title.
ServerName string
// ServerEndpoint is the canonical base URL of the server. If empty, it is
// derived from each request via [response.RealHostFromRequest].
ServerEndpoint string
// BuildID and BuildTag identify the running build.
BuildID string
BuildTag string
// DevMode indicates the server is running in development mode.
DevMode bool
// Extra holds any additional application-specific values to expose to every
// template (for example, feature flags). Keys here are copied onto the
// template map verbatim.
Extra map[string]any
}
TemplateConfig holds the common values seeded onto the template map by PopulateTemplateVariables.