Documentation
¶
Overview ¶
Package middleware provides reusable HTTP middleware for production hardening.
Components include:
- ClientIPExtractor: Extracts real client IPs behind trusted reverse proxies
- RateLimiter: Global and per-key rate limiting with configurable key functions
- ConnLimiter: Concurrent connection limiting for WebSocket endpoints
- OriginChecker: Origin allowlist for WebSocket upgrade requests
- CORS: Origin-aware CORS header middleware
- Recovery: Panic recovery with structured logging
- RequestLogger: Structured HTTP request logging
- Guard: Composable middleware chain
All components are nil-safe: a nil component acts as a no-op passthrough. No application-specific imports — designed for embedding in any Go HTTP server.
Index ¶
- func CORS(checker *OriginChecker) func(http.Handler) http.Handler
- func ClientIP(r *http.Request) string
- func Recovery(next http.Handler) http.Handler
- func RequestLogger(skipPaths ...string) func(http.Handler) http.Handler
- func SetTrustedProxies(cidrs []string)
- type ClientIPExtractor
- type ConnLimiter
- type Guard
- type OriginChecker
- type RateLimitConfig
- type RateLimiter
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CORS ¶
func CORS(checker *OriginChecker) func(http.Handler) http.Handler
CORS returns middleware that sets CORS headers based on an OriginChecker.
Behavior:
- If checker is nil (no allowlist): Access-Control-Allow-Origin: *
- If checker is set and the request Origin matches: reflect the origin back
- If checker is set and the request Origin doesn't match: no CORS headers (browser will block the response)
- OPTIONS preflight requests are handled and return 204
This replaces the naive "Access-Control-Allow-Origin: *" pattern with origin validation. The same OriginChecker used for WebSocket origin checks is reused here for consistency.
func ClientIP ¶
ClientIP extracts the real client IP using the default extractor. Configure trusted proxies via SetTrustedProxies.
func Recovery ¶
Recovery returns middleware that recovers from panics in downstream handlers. On panic, it logs the error and stack trace, then returns 500 to the client. Without this, a single bad request can crash the entire server.
func RequestLogger ¶
RequestLogger logs HTTP requests with method, path, status, duration, and client IP. Skips logging for specified paths (e.g. /health for noisy liveness probes).
func SetTrustedProxies ¶
func SetTrustedProxies(cidrs []string)
SetTrustedProxies configures the default (package-level) extractor. This is a convenience for simple use cases; prefer NewClientIPExtractor for instance isolation.
Types ¶
type ClientIPExtractor ¶
type ClientIPExtractor struct {
// contains filtered or unexported fields
}
ClientIPExtractor extracts the real client IP from requests, honoring X-Forwarded-For and X-Real-IP headers only when the direct connection comes from a trusted proxy CIDR.
Usage:
// Trust Caddy on localhost and Docker bridge network
extractor := NewClientIPExtractor([]string{"127.0.0.1/32", "172.17.0.0/16", "::1/128"})
ip := extractor.ClientIP(r)
// Trust all proxies (suitable for single-proxy deployments)
extractor := NewClientIPExtractor(nil)
func NewClientIPExtractor ¶
func NewClientIPExtractor(cidrs []string) *ClientIPExtractor
NewClientIPExtractor creates an extractor with the given trusted proxy CIDRs. When cidrs is nil/empty, all proxies are trusted (backwards-compatible default).
func (*ClientIPExtractor) ExtractClientIP ¶
func (e *ClientIPExtractor) ExtractClientIP(r *http.Request) string
ExtractClientIP extracts the real client IP from the request.
If trusted proxies are configured, X-Forwarded-For is only honored when the direct connection (RemoteAddr) comes from a trusted proxy CIDR. Otherwise, the direct RemoteAddr is used.
If no trusted proxies are configured (default), X-Forwarded-For is always trusted (backwards-compatible for deployments behind a proxy).
type ConnLimiter ¶
type ConnLimiter struct {
// contains filtered or unexported fields
}
ConnLimiter limits the number of concurrent active requests to a handler. Designed for WebSocket endpoints where each connection holds resources for the lifetime of the session.
When the limit is reached, new requests receive 503 Service Unavailable. The counter decrements when the request handler returns (connection closes).
func NewConnLimiter ¶
func NewConnLimiter(max int64) *ConnLimiter
NewConnLimiter creates a limiter with the given max concurrent connections. Pass 0 for unlimited (returns nil — caller should skip the middleware).
func (*ConnLimiter) Active ¶
func (c *ConnLimiter) Active() int64
Active returns the current number of active connections.
func (*ConnLimiter) Middleware ¶
func (c *ConnLimiter) Middleware(next http.Handler) http.Handler
Middleware returns an HTTP middleware that enforces the connection limit.
type Guard ¶
type Guard struct {
// contains filtered or unexported fields
}
Guard composes multiple middleware into a single wrapper. Middleware are applied in the order they are added via Use — the first middleware added is the outermost (runs first on request, last on response).
Usage:
g := &Guard{}
g.Use(originChecker.Middleware, rateLimiter.Middleware(nil), auth.Middleware, connLimiter.Middleware)
http.Handle("/ws", g.Wrap(wsHandler))
type OriginChecker ¶
type OriginChecker struct {
// contains filtered or unexported fields
}
OriginChecker rejects requests whose Origin header doesn't match an allowlist. Designed for WebSocket endpoints where CORS headers alone don't prevent cross-origin connections.
Matching rules:
- Exact match on scheme+host (port-insensitive for 80/443)
- Wildcard subdomain: "*.example.com" matches "foo.example.com"
- "localhost" matches any localhost origin regardless of port
- Empty allowlist = allow all (no-op)
- Missing Origin header = blocked (unless allowlist is empty)
func NewOriginChecker ¶
func NewOriginChecker(origins []string) *OriginChecker
NewOriginChecker creates a checker from a list of allowed origins. Accepts formats: "https://example.com", "*.example.com", "localhost". Returns nil if the list is empty (caller should skip the check).
func (*OriginChecker) Check ¶
func (c *OriginChecker) Check(origin string) bool
Check returns true if the origin is allowed.
func (*OriginChecker) Middleware ¶
func (c *OriginChecker) Middleware(next http.Handler) http.Handler
Middleware returns an HTTP middleware that rejects disallowed origins. Only applies to WebSocket upgrade requests (Connection: Upgrade). Non-upgrade requests pass through unchanged.
type RateLimitConfig ¶
type RateLimitConfig struct {
GlobalPerSec float64 // max requests/sec globally (0 = unlimited)
PerKeyPerSec float64 // max requests/sec per key (0 = unlimited)
PerKeyBurst int // burst allowance per key
KeyLimiterTTL time.Duration // cleanup interval for stale per-key limiters
}
RateLimitConfig controls rate limiting.
func DefaultRateLimitConfig ¶
func DefaultRateLimitConfig() RateLimitConfig
DefaultRateLimitConfig returns sensible defaults.
type RateLimiter ¶
type RateLimiter struct {
Config RateLimitConfig
// OnRejected is called when a request is rate-limited.
// The key argument is the rate limit key (e.g., IP address, subject ID).
OnRejected func(key string)
// contains filtered or unexported fields
}
RateLimiter enforces global and per-key rate limits.
func NewRateLimiter ¶
func NewRateLimiter(cfg RateLimitConfig) *RateLimiter
NewRateLimiter creates a rate limiter. Returns nil if both limits are 0.
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow(key string) bool
Allow checks both global and per-key rate limits. Returns false if rejected.
func (*RateLimiter) Middleware ¶
func (rl *RateLimiter) Middleware(keyFunc func(*http.Request) string) func(http.Handler) http.Handler
Middleware returns an HTTP middleware that enforces rate limits. keyFunc extracts the rate limit key from the request. If nil, defaults to ClientIP.