middleware

package
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2026 License: Apache-2.0 Imports: 35 Imported by: 0

Documentation

Overview

Package middleware bundles common HTTP middleware ready to drop into any gogo App or Router. Each middleware is a thin layer over gogo.Middleware so they compose with the framework's existing chaining model (App.Use, Router.Use).

Logger records a one-line entry per request: method, URL, status, duration, client IP. The default format is text; pass a custom Format function to emit JSON or any other shape.

Limitations

Logger wraps the sync handler chain, so for synchronous routes (Get, Post, Any, Put, Patch, Delete, Options, Head) it captures the full request lifetime including the response write. For "fire-and-forget" Response.Async (a sync handler that spawns a goroutine and returns immediately), Logger sees only the sync portion — the duration will be roughly zero. PostAsync / AsyncMiddleware paths run inside their worker so they ARE timed correctly when the middleware is registered as AsyncMiddleware via app.Use instead of app.Use.

Index

Constants

View Source
const BasicAuthLocalKey = "gogo.basicauth.user"

BasicAuthLocalKey is the req.Local key carrying the authenticated username. Handlers retrieve it via:

user, _ := req.Local(middleware.BasicAuthLocalKey).(string)
View Source
const CSRFLocalKey = "gogo.csrf.token"

CSRFLocalKey is the req.Local key carrying the CSRF token for the current request. Handlers reading it can embed the value into HTML forms or pass it back to the client (e.g. via a JSON envelope) so subsequent unsafe requests can echo it.

View Source
const CompressNoMaxSize = -1

CompressNoMaxSize disables CompressOptions.MaxSize. Use sparingly: it lets the middleware buffer and compress arbitrarily large dynamic responses.

View Source
const JWTLocalKey = "gogo.jwt.claims"

JWTLocalKey is the req.Local key carrying the verified token's claim set as a map[string]any.

View Source
const NoBasicAuthCredentialLimit = -1

NoBasicAuthCredentialLimit disables the BasicAuth credentials payload cap. Use only behind an external header-size limit.

View Source
const NoCSRFTokenLimit = -1

NoCSRFTokenLimit disables the CSRF token length cap. Use only behind an external header-size limit.

View Source
const NoJWTTokenLimit = -1

NoJWTTokenLimit disables the compact JWT length cap. Use only behind an external header-size limit.

View Source
const NoRateLimitBucketLimit = -1

NoRateLimitBucketLimit disables the in-memory rate-limit bucket cap. This is intended for tests only; production deployments should keep the cap enabled or use a bounded external store such as Redis.

View Source
const NoSessionEntryLimit = -1

NoSessionEntryLimit disables the in-memory session entry cap. This is intended for tests only; production deployments should keep the cap enabled or use a bounded external store such as Redis.

View Source
const RequestIDLocalKey = "gogo.requestID"

RequestIDLocalKey is the key used to stash the request ID into req.SetLocal / req.Local. Handlers and downstream middleware read the ID via req.Local(middleware.RequestIDLocalKey).(string).

View Source
const SessionLocalKey = "gogo.session"

SessionLocalKey is the req.Local key carrying the *Session for the current request. Handlers read and mutate session data via:

sess, _ := req.Local(middleware.SessionLocalKey).(*middleware.Session)
sess.Set("user_id", 42)

Variables

This section is empty.

Functions

func Async

func Async(mw gogo.Middleware) mwhint.Hinted

Async wraps a Middleware so the framework runs it inside the worker goroutine that runs an async handler. Use this for any middleware that blocks on I/O — database queries, HTTP calls, file reads — because the framework's sync chain runs on the uWS event-loop thread, and a blocking call there would stall every other request in flight.

app.Use(middleware.Async(func(next gogo.Handler) gogo.Handler {
    return func(res *gogo.Response, req *gogo.Request) {
        user, err := db.LookupUser(req.Header("authorization"))
        if err != nil {
            res.Send(401, "text/plain", "bad token")
            return
        }
        req.SetLocal("user", user)
        next(res, req)
    }
}))

Async-wrapped middleware fires only on GetAsync / PostAsync routes; sync routes (Get / Post) do not see it. If you register an Async middleware that matches a sync route, the route will simply skip the middleware — register it on a Group of async-only routes, or move the route to GetAsync.

Non-blocking middleware (header setters, validators, in-memory state) does not need Async; pass the raw function to app.Use and the framework will route it through the optimal chain for each request type.

func BasicAuth

func BasicAuth(opt BasicAuthOptions) mwhint.Hinted

BasicAuth returns a Middleware that enforces HTTP Basic authentication (RFC 7617). Requests without a valid Authorization header receive 401 Unauthorized with a WWW-Authenticate challenge; authenticated requests have the username stashed at LocalKey for downstream handlers.

app.Use(middleware.BasicAuth(middleware.BasicAuthOptions{
    Users: map[string]string{"admin": "secret"},
}))

Always pair Basic auth with TLS — the credentials travel base64-encoded but otherwise in plaintext. The middleware does not enforce HTTPS; configure that at the edge (load balancer, reverse proxy) or via HSTS via Helmet.

func CORS

func CORS(opts ...CORSOptions) mwhint.Hinted

CORS returns a Middleware that sets the appropriate Access-Control-* headers and short-circuits OPTIONS preflight requests with a 204. Install it like any other middleware:

app.Use(middleware.CORS(middleware.CORSOptions{
    AllowOrigins: []string{"https://app.example.com"},
    AllowCredentials: true,
}))

Preflight requests (OPTIONS + Access-Control-Request-Method header) reach this middleware even when the path has no user-registered OPTIONS handler — gogo's App.Listen auto-registers a global catch-all route the moment any middleware is registered, so the middleware chain fires on every URL the way express / fiber users expect.

func CSRF

func CSRF(opt CSRFOptions) mwhint.Hinted

CSRF returns a Middleware that enforces the double-submit-cookie pattern: the server issues a signed random token in a non-HttpOnly cookie and on unsafe requests (POST/PUT/PATCH/DELETE) demands the same value in the X-CSRF-Token header. JavaScript on the trusted origin can read the cookie and echo it; cross-origin attackers cannot read the cookie thanks to the same-origin policy.

app.Use(middleware.CSRF(middleware.CSRFOptions{
    Secret: []byte(os.Getenv("CSRF_SECRET")),
    CookieSecure: true,
}))

Token format: <random>.<hmac(secret, random)>. The HMAC binds the token to the server secret, so an attacker who can set a cookie on a sibling subdomain still cannot forge a token that verifies.

Limitations: this middleware only checks the header, not form fields. SPA / JSON clients can echo the cookie via JavaScript; classic HTML form posts need to include the token in the X-CSRF- Token header via fetch or set it via a custom hidden-field flow the handler manages.

func Compress

func Compress(opts ...CompressOptions) mwhint.Hinted

Compress returns a Middleware that gzips response bodies for clients that advertise gzip / deflate support via Accept-Encoding. The middleware buffers the handler's Write / End / Send / JSON output via Response.SetBodyEncoder, then compresses on flush. Both sync and async handlers (Response.Async, PostAsync) are supported — when extra headers are present the async path transparently falls back from the zero-cgo shared-memory path to the defer-send-with-headers shim that carries Content-Encoding and Vary alongside the body.

app.Use(middleware.Compress())

Limitations

SendFile is not compressed — the static-file path streams the file directly to uWS without flowing through Write / End.

Brotli is not bundled: it requires a separate Go dependency (e.g. github.com/andybalholm/brotli) and is left for callers who want it to wire up via their own encoder. Use SetBodyEncoder directly from a custom middleware for that.

func DefaultFormat

func DefaultFormat(e LogEntry) string

DefaultFormat formats an entry as a single line of human-readable text. Stable enough for log scraping; switch to JSONFormat for structured ingest.

func FastRequestIDGenerator

func FastRequestIDGenerator() func() string

FastRequestIDGenerator returns a generator that produces 22-character base64url IDs from a ChaCha8 PRNG seeded once per pool slot from crypto/rand. The entropy matches the default generator's 16-byte (128-bit) payload while emitting a shorter 22-character base64url string instead of the default 32-character hex string.

Tracing identifiers don't need cryptographic strength — they need uniqueness with high probability. ChaCha8 has a 256-bit internal state so collisions in any realistic window are astronomically unlikely; the seed itself comes from crypto/rand so an attacker can't predict the sequence from the outside.

Use it via RequestIDOptions.Generator:

app.Use(middleware.RequestID(middleware.RequestIDOptions{
    Generator: middleware.FastRequestIDGenerator(),
}))

The returned function is safe for concurrent use — a sync.Pool serializes access to each ChaCha8 instance. A single shared instance under a mutex would tank throughput at high RPS; the pool lets each loop / goroutine pull its own ChaCha8 and put it back when done.

func Helmet

func Helmet(opts ...HelmetOptions) mwhint.Hinted

Helmet returns a Middleware that sets a curated bundle of HTTP security response headers on every request. The defaults follow OWASP / helmet.js guidance and are safe to drop into most apps; HSTS and CSP are the two that typically need explicit configuration before going to production.

The middleware does not inspect the request — every response gets the same headers. Pair with App.Use so the headers attach to every route, including 404s for unknown paths.

app.Use(middleware.Helmet())

app.Use(middleware.Helmet(middleware.HelmetOptions{
    ContentSecurityPolicy: "default-src 'self'",
    HSTS: "max-age=63072000; includeSubDomains; preload",
}))

func JSONFormat

func JSONFormat(e LogEntry) string

JSONFormat formats an entry as a single line of JSON with stable keys. Useful when piping logs into structured systems (loki, elasticsearch, datadog) — handwritten so there are no allocations for a json.Marshal reflection pass on the hot path.

func JWT

func JWT(opt JWTOptions) mwhint.Hinted

JWT returns a Middleware that authenticates requests carrying a JSON Web Token signed with HMAC, RSA, RSA-PSS, or ECDSA. On success the verified claims (as a map[string]any from json.Unmarshal) are stashed at req.Local(LocalKey) for the handler chain.

// HMAC
app.Use(middleware.JWT(middleware.JWTOptions{
    Secret: []byte(os.Getenv("JWT_SECRET")),
}))

// RSA
pub, _ := middleware.ParseRSAPublicKey([]byte(rsaPEM))
app.Use(middleware.JWT(middleware.JWTOptions{
    Algorithm: middleware.JWTRS256,
    Key:       pub,
}))

// ECDSA
pub, _ := middleware.ParseECPublicKey([]byte(ecPEM))
app.Use(middleware.JWT(middleware.JWTOptions{
    Algorithm: middleware.JWTES256,
    Key:       pub,
}))

app.Get("/me", func(res *gogo.Response, req *gogo.Request) {
    claims, _ := req.Local(middleware.JWTLocalKey).(map[string]any)
    res.JSON(200, claims)
})

JWKS-style key rotation (looking up the key per token via the header `kid`) is not built in. Wire it by setting TokenFunc to a custom verifier that resolves the key from your JWKS cache and returns the validated claims — or write a thin middleware that dispatches between multiple JWT instances keyed on `kid`.

func Logger

func Logger(opts ...LoggerOptions) mwhint.Hinted

Logger returns a Middleware that logs each request after its handler returns. Safe to share across goroutines — writes are serialized behind a sync.Mutex so concurrent log lines never interleave on the output writer.

func NewSession

func NewSession(opt SessionOptions) mwhint.Hinted

NewSession returns a Middleware that loads the session for the request, exposes a *Session at req.Local(LocalKey), and saves the session back to the store after the handler returns.

app.Use(middleware.NewSession(middleware.SessionOptions{
    Secret: []byte(os.Getenv("SESSION_SECRET")),
    CookieSecure: true,
}))

Session ids are signed with HMAC-SHA256: "<random>.<sig>". The random portion identifies the session in the store; the sig binds the cookie to the server secret so attackers cannot fabricate a valid id.

The factory keeps the "New" prefix to disambiguate from the per-request *Session struct that handlers manipulate; other middleware in this package follow the bare-noun convention (Helmet, CORS, …) but for sessions the struct name carries the weight since handlers reference it constantly.

func ParseECPublicKey

func ParseECPublicKey(pemBytes []byte) (*ecdsa.PublicKey, error)

ParseECPublicKey decodes a PEM-encoded EC public key in PKIX/SPKI format (the format `openssl ec -pubout` produces).

func ParseRSAPublicKey

func ParseRSAPublicKey(pemBytes []byte) (*rsa.PublicKey, error)

ParseRSAPublicKey decodes a PEM-encoded RSA public key. Accepts both PKCS1 (BEGIN RSA PUBLIC KEY) and PKIX/SPKI (BEGIN PUBLIC KEY) blocks — the two formats Go's stdlib emits via x509.MarshalPKCS1PublicKey and x509.MarshalPKIXPublicKey.

func RateLimit

func RateLimit(opt RateLimitOptions) mwhint.Hinted

RateLimit returns a middleware that enforces a fixed-window per-key quota. When a key exceeds Max requests within Window, subsequent requests are short-circuited with 429 Too Many Requests and a Retry-After header. The default key is the client IP; override KeyFunc to rate-limit by user, API key, etc.

The middleware also emits the conventional informational headers on every response:

X-RateLimit-Limit      — configured Max
X-RateLimit-Remaining  — requests left in the current window
X-RateLimit-Reset      — Unix timestamp when the window resets

In-memory backing is single-process. For multi-instance fleets supply a Store implementation that consults a shared backend.

func RequestID

func RequestID(opts ...RequestIDOptions) mwhint.Hinted

RequestID returns a middleware that ensures every request has an ID:

  • If the incoming Header value is non-empty and passes the configured length/format policy, that ID is reused.
  • Otherwise a new ID is generated via Options.Generator.

The ID is echoed on the response in the same header and stashed in req.Locals so handlers / downstream middleware can read it via

id, _ := req.Local(middleware.RequestIDLocalKey).(string)

Pair with Logger by extending LogEntry / Format to include the ID.

func SignJWT

func SignJWT(alg JWTAlgorithm, key any, claims map[string]any) (string, error)

SignJWT produces a compact JWT signed with the given algorithm and key. Intended for tests and small login flows — production auth servers usually mint tokens in dedicated identity-provider code with richer key management.

The key argument's type depends on Algorithm:

  • HS256/384/512: []byte
  • RS256/384/512 and PS256/384/512: *rsa.PrivateKey
  • ES256/384/512: *ecdsa.PrivateKey

func WebSocketAuth

func WebSocketAuth(opt WebSocketAuthOptions) func(*gogo.UpgradeContext)

WebSocketAuth returns an Upgrade callback that gates incoming WebSocket handshakes against an Origin allow-list and an optional app-supplied Verify check. With zero-value options it rejects every handshake; configure AllowedOrigins for browsers and set AllowMissingOrigin only for non-browser clients that omit Origin. Attach it to WebSocketBehavior.Upgrade:

app.WebSocket("/ws", gogo.WebSocketBehavior{
    Upgrade: middleware.WebSocketAuth(middleware.WebSocketAuthOptions{
        AllowedOrigins: []string{"https://app.example.com"},
        Verify: func(ctx *gogo.UpgradeContext) (any, bool, int, string) {
            user, err := verifyToken(ctx.QueryParam("token"))
            if err != nil {
                return nil, false, 401, "bad token"
            }
            return user, true, 0, ""
        },
    }),
    Open:    handleOpen,
    Message: handleMessage,
    Close:   handleClose,
})

CSWSH defense is the primary reason this helper exists. If you only need a hand-rolled Origin check it's a few lines of code — but the failure mode of "forgot to write the Upgrade callback at all" is catastrophic enough that having a documented defaults-on helper is worth the abstraction.

Types

type BasicAuthOptions

type BasicAuthOptions struct {
	// Users is a static {username: password} map. Verification
	// scans every entry with constant-time comparisons, so timing
	// reveals neither whether a username exists nor which entry
	// matched. Intended for small fleets and CI; production
	// credentials should live in Validator backed by a
	// hashed-password store.
	Users map[string]string

	// Validator authenticates a (user, pass) pair. Return true to
	// allow the request, false to reject with 401. Validator is
	// called once per request — keep it fast (bcrypt only on the
	// register / login path, not here).
	Validator func(user, pass string) bool

	// Realm appears in the WWW-Authenticate challenge header on a
	// 401 response. Default "Restricted". Browsers use the realm
	// string to namespace cached credentials.
	Realm string

	// LocalKey overrides the req.Local key used to stash the
	// authenticated username. Default BasicAuthLocalKey.
	LocalKey string

	// SkipFunc, when non-nil and returning true, bypasses
	// authentication for that request. Useful to expose /healthz
	// without credentials while protecting the rest of the app.
	SkipFunc func(*gogo.Request) bool

	// MaxCredentialBytes caps the base64 credentials payload before
	// decoding. Default 8 KiB. Set to NoBasicAuthCredentialLimit to
	// disable.
	MaxCredentialBytes int
}

BasicAuthOptions configures BasicAuth. Provide either Users or Validator (Validator wins if both are set).

type CORSOptions

type CORSOptions struct {
	// AllowOrigins is the list of origins that are allowed to make
	// cross-origin requests. "*" allows any origin (default). Exact
	// hostnames or full origins (https://app.example.com) are matched
	// case-insensitively. Wildcard suffixes like "https://*.example.com"
	// are supported — the literal "*" must appear right after "://".
	//
	// AllowCredentials cannot be combined with "*" — browsers reject it
	// per spec. Do not mix "*" with explicit origins; use only "*"
	// for public APIs, or list every trusted origin explicitly.
	AllowOrigins []string

	// AllowMethods is the list of HTTP methods returned in the preflight
	// Access-Control-Allow-Methods header. Defaults to the standard
	// safe-plus-write set: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.
	AllowMethods []string

	// AllowHeaders is the whitelist of request headers the server
	// permits on cross-origin requests. The preflight response's
	// Access-Control-Allow-Headers is computed as the case-insensitive
	// intersection of the client's Access-Control-Request-Headers and
	// this list — headers the client asks for that are NOT in this
	// list are silently dropped from the response, which causes the
	// browser to refuse to send them on the actual request.
	//
	// Default: Content-Type, Authorization. The literal "*" is
	// supported and means "any header" per the Fetch spec, but ONLY
	// when AllowCredentials is false; with credentials enabled the
	// wildcard is ignored and the configured list is enforced exactly.
	AllowHeaders []string

	// ExposeHeaders lists response headers the browser is allowed to
	// surface to client JavaScript. Empty by default (browsers expose
	// only the CORS-safelisted-response headers).
	ExposeHeaders []string

	// AllowCredentials sets Access-Control-Allow-Credentials: true.
	// Cannot be combined with AllowOrigins == []string{"*"}.
	AllowCredentials bool

	// MaxAge sets Access-Control-Max-Age in seconds. The browser caches
	// the preflight result for this long. 0 omits the header (browser
	// default, ~5 seconds).
	MaxAge int
}

CORSOptions configures the CORS middleware. Zero value is permissive (Access-Control-Allow-Origin: *, common methods, common headers); use the explicit struct for production deployments.

type CSRFOptions

type CSRFOptions struct {
	// Secret is the HMAC key used to sign tokens. Required; must be at
	// least 32 bytes of entropy. Rotating the secret invalidates
	// outstanding tokens, which is the intended behavior for
	// logout-everywhere flows.
	Secret []byte

	// CookieName is the Set-Cookie name carrying the token to the
	// browser. Default "csrf_token". The cookie is *not* HttpOnly
	// — JavaScript must be able to read it to echo into an
	// X-CSRF-Token header.
	CookieName string

	// HeaderName is the request header checked on unsafe methods.
	// Default "X-CSRF-Token".
	HeaderName string

	// CookiePath sets the Path attribute on the issued cookie.
	// Default "/".
	CookiePath string

	// CookieDomain sets the Domain attribute on the issued cookie.
	// Default empty (host-only cookie).
	CookieDomain string

	// CookieSecure sets the Secure attribute. Default false —
	// enable in production so the cookie never leaves HTTPS.
	CookieSecure bool

	// CookieSameSite sets the SameSite attribute. Default Lax,
	// which is the modern recommendation: prevents cross-site
	// POSTs from carrying the cookie while still allowing
	// top-level GET navigation.
	CookieSameSite gogo.SameSite

	// CookieMaxAge sets Max-Age in seconds. Default 86400 (1 day).
	CookieMaxAge int

	// MaxTokenBytes caps the CSRF token read from the Cookie and header
	// before signature verification or constant-time comparison. Zero uses
	// a conservative default; NoCSRFTokenLimit disables the cap.
	MaxTokenBytes int

	// SkipFunc, when non-nil and returning true, bypasses CSRF
	// entirely. Useful to allow specific routes (e.g. signed
	// webhook endpoints) to skip the check.
	SkipFunc func(*gogo.Request) bool

	// LocalKey overrides the req.Local key for the issued token.
	// Default CSRFLocalKey.
	LocalKey string
}

CSRFOptions configures the double-submit-cookie CSRF middleware.

type CompressOptions

type CompressOptions struct {
	// Level is the gzip / deflate compression level. Zero means
	// gzip.DefaultCompression so the zero-value options stay useful.
	// Explicit valid values are gzip.DefaultCompression,
	// gzip.HuffmanOnly, and 1..9. Values outside the stdlib range
	// panic at construction time.
	Level int

	// MinSize skips compression when the buffered body is smaller
	// than this many bytes — small payloads are not worth the CPU
	// and the gzip framing overhead often makes the result larger.
	// Default 1024.
	MinSize int

	// MaxSize caps both compression work and the middleware's staging
	// buffer. Once the response body would grow beyond this many bytes,
	// the encoder is bypassed and the response continues uncompressed
	// (no Content-Encoding header). This avoids retaining a whole large
	// dynamic response just to later decide it is too large to compress.
	//
	// Default 4 MiB. Set to CompressNoMaxSize to disable the cap
	// (matches the pre-cap behavior — every body of any size gets
	// compressed if it passes the other filters).
	MaxSize int

	// Filter, when non-nil, is consulted after the body is buffered
	// to decide whether the response is worth compressing. The
	// default accepts standard text-ish content types and skips
	// pre-compressed media (images, archives). Override to add
	// app-specific types.
	Filter func(contentType string) bool

	// SkipFunc, when non-nil and returning true, bypasses the
	// middleware entirely for that request — useful to spare CPU
	// for streaming endpoints (Server-Sent Events, file
	// downloads) where buffering the whole body defeats the point.
	SkipFunc func(*gogo.Request) bool
}

CompressOptions configures Compress. Zero value uses gzip.DefaultCompression with a 1 KiB MinSize threshold, a 4 MiB MaxSize cap, and the standard "compressible content-type" filter (text/*, application/json, application/javascript, application/xml, image/svg+xml).

type HelmetOptions

type HelmetOptions struct {
	// HSTS sets Strict-Transport-Security. Default
	// "max-age=15552000; includeSubDomains" (180 days). The header is
	// only meaningful over HTTPS; browsers ignore it on plain HTTP.
	// Set "off" to omit (useful for HTTP-only dev environments).
	HSTS string

	// ContentSecurityPolicy sets Content-Security-Policy. Empty by
	// default — CSP is highly app-specific and a wrong policy breaks
	// pages. Set "off" or leave empty to omit. Common starter:
	// "default-src 'self'".
	ContentSecurityPolicy string

	// FrameOptions sets X-Frame-Options. Default "SAMEORIGIN".
	// Standard values: DENY, SAMEORIGIN. "off" omits.
	FrameOptions string

	// ContentTypeOptions sets X-Content-Type-Options. Default
	// "nosniff". "off" omits.
	ContentTypeOptions string

	// ReferrerPolicy sets Referrer-Policy. Default "no-referrer".
	// "off" omits.
	ReferrerPolicy string

	// XSSProtection sets X-XSS-Protection. Default "0" — modern
	// guidance is to disable the legacy IE/old-Chrome filter, which
	// could be abused to introduce XSS in otherwise safe pages.
	// "off" omits.
	XSSProtection string

	// DNSPrefetchControl sets X-DNS-Prefetch-Control. Default "off"
	// (browsers will not pre-resolve hostnames found in the page).
	// "off" as a value still emits the header; pass "" to use the
	// default and pass the literal "" override using SetHeader-style
	// is N/A — use HelmetOptions{DNSPrefetchControl: "on"} to enable.
	// To suppress the header entirely set "omit".
	DNSPrefetchControl string

	// DownloadOptions sets X-Download-Options (IE legacy). Default
	// "noopen". "omit" suppresses.
	DownloadOptions string

	// PermittedCrossDomainPolicies sets
	// X-Permitted-Cross-Domain-Policies (Adobe legacy). Default
	// "none". "omit" suppresses.
	PermittedCrossDomainPolicies string

	// CrossOriginOpenerPolicy sets Cross-Origin-Opener-Policy.
	// Default "same-origin" — isolates the browsing context group
	// to mitigate Spectre / side-channel leaks. "omit" suppresses.
	CrossOriginOpenerPolicy string

	// CrossOriginResourcePolicy sets Cross-Origin-Resource-Policy.
	// Default "same-origin". "omit" suppresses.
	CrossOriginResourcePolicy string
}

HelmetOptions configures Helmet. Each field's zero value falls back to the OWASP-recommended default listed below. Set a field to the sentinel string "off" to suppress that header entirely.

type JWTAlgorithm

type JWTAlgorithm string

JWTAlgorithm selects the signing algorithm the middleware will accept. The middleware refuses to verify any token whose `alg` header disagrees with this value, blocking alg-confusion and `alg: "none"` attacks.

Supported families:

  • HS256/HS384/HS512 — HMAC. Key is a []byte secret.
  • RS256/RS384/RS512 — RSASSA-PKCS1-v1_5. Key is a *rsa.PublicKey.
  • PS256/PS384/PS512 — RSASSA-PSS. Key is a *rsa.PublicKey.
  • ES256/ES384/ES512 — ECDSA on P-256 / P-384 / P-521. Key is a *ecdsa.PublicKey.
const (
	JWTHS256 JWTAlgorithm = "HS256"
	JWTHS384 JWTAlgorithm = "HS384"
	JWTHS512 JWTAlgorithm = "HS512"
	JWTRS256 JWTAlgorithm = "RS256"
	JWTRS384 JWTAlgorithm = "RS384"
	JWTRS512 JWTAlgorithm = "RS512"
	JWTPS256 JWTAlgorithm = "PS256"
	JWTPS384 JWTAlgorithm = "PS384"
	JWTPS512 JWTAlgorithm = "PS512"
	JWTES256 JWTAlgorithm = "ES256"
	JWTES384 JWTAlgorithm = "ES384"
	JWTES512 JWTAlgorithm = "ES512"
)

type JWTOptions

type JWTOptions struct {
	// Secret is the HMAC verification key for HS256/384/512.
	// Required when Algorithm is an HMAC variant; must be at least
	// 32 bytes of entropy. Ignored for asymmetric algorithms.
	Secret []byte

	// Key is the public key for asymmetric algorithms (RS*, PS*,
	// ES*). Must match the algorithm family:
	//   - *rsa.PublicKey for RS256/384/512 and PS256/384/512
	//   - *ecdsa.PublicKey for ES256/384/512
	// The middleware panics at construction if Algorithm and Key
	// disagree. Ignored for HMAC algorithms (use Secret).
	Key crypto.PublicKey

	// Algorithm selects the expected signing algorithm. Default
	// HS256. Tokens whose `alg` header differs from this value
	// are rejected — preventing both the legacy "none" attack
	// and algorithm-confusion attacks (HMAC-vs-RSA, where an
	// attacker could sign an RS256 token using the public key as
	// an HMAC secret if the verifier blindly used the alg header).
	Algorithm JWTAlgorithm

	// Issuer, when non-empty, requires the token's `iss` claim to
	// exactly match this value. Default empty disables issuer validation.
	Issuer string

	// Audience, when non-empty, requires the token's `aud` claim to contain
	// this value. The middleware accepts either a string `aud` claim or an
	// array of string audiences. Default empty disables audience validation.
	Audience string

	// RequiredClaims lists claim names that must be present with non-null
	// values. It does not otherwise validate custom claim value semantics.
	// Default nil requires no additional claims.
	RequiredClaims []string

	// TokenFunc extracts the JWT string from the request. Default
	// reads "Authorization: Bearer <token>". Override to source
	// the token from a cookie, query parameter, etc.
	TokenFunc func(*gogo.Request) string

	// LocalKey overrides the req.Local key used to stash claims.
	// Default JWTLocalKey.
	LocalKey string

	// SkipFunc, when non-nil and returning true, bypasses JWT
	// verification for that request.
	SkipFunc func(*gogo.Request) bool

	// Optional, when true, lets requests without a token through
	// (handler can inspect req.Local(LocalKey) to detect the
	// anonymous case). Tokens that are present but invalid are
	// still rejected with 401 — Optional doesn't make malformed
	// tokens acceptable.
	Optional bool

	// Leeway is the clock-skew tolerance when validating `exp` and
	// `nbf` claims. Default 0 (strict).
	Leeway time.Duration

	// MaxTokenBytes caps the compact JWT string before any base64 decode
	// or JSON parsing. Default 16 KiB. Set to NoJWTTokenLimit to disable.
	MaxTokenBytes int
}

JWTOptions configures the JWT middleware.

type LogEntry

type LogEntry struct {
	Method    string
	URL       string
	Status    int
	Duration  time.Duration
	IP        string
	UserAgent string
}

LogEntry is the snapshot a Logger Format function receives.

type LoggerOptions

type LoggerOptions struct {
	// Output is where formatted log lines go. Default os.Stdout.
	Output io.Writer

	// Format produces the log line to write. Default: standard text
	// format "<method> <url> <status> <duration> <ip> <user-agent>".
	// Replace with a JSON-emitting function for structured logs.
	Format func(LogEntry) string

	// SkipPaths is a set of URLs to omit from logging — useful for
	// health-check spam (/healthz, /metrics, etc.).
	SkipPaths []string
}

LoggerOptions configures Logger. Zero value uses sensible defaults (stdout, text format).

type MemoryRateLimitStore

type MemoryRateLimitStore struct {
	// contains filtered or unexported fields
}

MemoryRateLimitStore is the default in-memory backend for RateLimit. Counters are kept in a map[string]*rateBucket protected by a single mutex; suitable for a single process at moderate QPS.

Memory is bounded by maxBuckets (set via RateLimitOptions.MaxBuckets, default 100_000): when the cap is reached Hit first sweeps expired buckets, then evicts the oldest-resetAt remaining bucket to make room — so the worst-case footprint stays predictable even under high-cardinality key spam (per-user-agent, per-token, etc.).

GC may also be called manually to reclaim space before the cap is hit; otherwise the lazy sweep inside Hit covers the common case.

Scale guidance

Every Hit acquires a single mutex. On a single host with one or two dozen cores this is fine well past 50 k RPS — the critical section is < 1 µs (map probe + count++) and Go's mutex is fair under moderate contention. Above ~100 k RPS the lock starts to show up in CPU profiles; above ~250 k RPS it dominates.

Beyond those thresholds, prefer:

  • A Redis-backed Store for shared counters across a fleet. The network round-trip costs more per call but parallelizes across cores, and you can swap atomic.Int64 in Redis or use INCR with EXPIRE for the same algorithm.
  • Shard your own map[string]*rateBucket across N independent stores keyed by hash(key) % N. Each shard has its own mutex. Useful when you must stay in-process but contention shows up in profiles.

func NewMemoryRateLimitStore

func NewMemoryRateLimitStore() *MemoryRateLimitStore

NewMemoryRateLimitStore returns an empty in-memory store ready for use as RateLimitOptions.Store. The store is unbounded by default when used directly; routing it through RateLimit applies the MaxBuckets cap (default 100_000) automatically.

func (*MemoryRateLimitStore) GC

func (s *MemoryRateLimitStore) GC() int

GC removes buckets whose window has already expired. Call periodically if your key cardinality is unbounded and you want to cap memory. Returns the number of buckets reclaimed.

func (*MemoryRateLimitStore) Hit

func (s *MemoryRateLimitStore) Hit(key string, window time.Duration) (int, time.Time)

Hit implements RateLimitStore. The window rolls forward to now+window the first time a key is seen, and on every reset crossing thereafter. When maxBuckets is configured and the store is at capacity for a brand-new key, expired buckets are reclaimed first; if that doesn't free space the oldest-resetAt bucket is evicted.

type MemorySessionStore

type MemorySessionStore struct {
	// contains filtered or unexported fields
}

MemorySessionStore is the default Store: a map[string]*sessionEntry guarded by a sync.Mutex. Expired entries are reclaimed lazily — on Load when an expired id is touched, and on Save when the cap (maxEntries) is reached. The manual GC method is still available for callers that want to force a full sweep.

Memory is bounded by maxEntries (set via SessionOptions.MaxEntries, default 100_000): on a fresh Save at the cap the store first sweeps expired entries, then evicts the oldest-expires entry remaining to make room. Worst-case memory therefore stays predictable even under high-cardinality session-id spam.

Scale guidance

Every Load and Save acquires the single mutex. The critical section is small (map probe + a few atomic loads) but for session-heavy workloads at high RPS the lock can show up as the dominant CPU cost in profiles — typically beyond ~50 k authenticated RPS per process.

Beyond that, prefer:

  • A Redis or other shared SessionStore for horizontal scale; the network round-trip is more expensive per call but parallelizes across cores and stops being a single-process bottleneck.
  • A sharded in-process implementation: N independent stores keyed by hash(id) % N. Each shard has its own mutex. Reasonable when sessions must stay process-local.

The pattern is identical to MemoryRateLimitStore — see its "Scale guidance" comment for the same trade-offs.

func NewMemorySessionStore

func NewMemorySessionStore() *MemorySessionStore

NewMemorySessionStore returns an empty MemorySessionStore ready for use as SessionOptions.Store. The store is unbounded by default when used directly; routing it through NewSession applies the MaxEntries cap (default 100_000) automatically.

func (*MemorySessionStore) Delete

func (s *MemorySessionStore) Delete(id string) error

func (*MemorySessionStore) GC

func (s *MemorySessionStore) GC() int

GC removes expired entries. Returns the number reclaimed.

func (*MemorySessionStore) Load

func (s *MemorySessionStore) Load(id string) (map[string]any, bool)

Load implements SessionStore. Expired entries are deleted from the map before reporting them absent — the doc claim that "expired entries are reclaimed lazily on Load" now holds. Without this delete, an attacker who never revisits a session-id leaves the entry pinned in the map forever, defeating the TTL.

func (*MemorySessionStore) Save

func (s *MemorySessionStore) Save(id string, data map[string]any, ttl time.Duration) error

Save implements SessionStore. New entries trigger eviction when the store is at maxEntries capacity (existing entries don't, so the steady-state hot path stays O(1)).

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics aggregates request statistics and exposes them as a Prometheus text exposition response. One instance lives for the lifetime of the App; Middleware and Handler return values captured against this instance.

func NewMetrics

func NewMetrics(opts ...MetricsOptions) *Metrics

NewMetrics builds a Metrics instance with the supplied options. Zero options is safe; defaults are applied.

func (*Metrics) Handler

func (m *Metrics) Handler() gogo.Handler

Handler returns a gogo.Handler that responds with the metrics encoded as Prometheus text exposition format (version 0.0.4 — the OpenMetrics-compatible flavor most scrapers default to). Register on whatever path your scrape config uses; "/metrics" is the convention.

app.Get("/metrics", m.Handler())

The handler reads counter / bucket / gauge values atomically; concurrent updates from in-flight requests are safe.

func (*Metrics) Middleware

func (m *Metrics) Middleware() mwhint.Hinted

Middleware returns the request-instrumenting middleware. Install it BEFORE the routes you want measured — the timer starts when the middleware fires and stops when control returns. Routes registered before this Use call won't be instrumented.

Placement is "both chains" so sync and async routes both get observed; the underlying counters are atomic and safe to share across the loop thread and worker goroutines.

func (*Metrics) ObserveBytesIn

func (m *Metrics) ObserveBytesIn(n int)

ObserveBytesIn records bytes read for the current request. Same usage shape as ObserveBytesOut — call where you know the payload size (e.g. inside a Body callback).

func (*Metrics) ObserveBytesOut

func (m *Metrics) ObserveBytesOut(n int)

ObserveBytesOut records bytes written for the current request. The middleware doesn't intercept Send / End automatically — users who want bytes-out tracking can call this from a handler or from a Logger-style middleware where they know the payload size.

func (*Metrics) Snapshot

func (m *Metrics) Snapshot() Snapshot

Snapshot returns the current counter values. Cheap atomic reads, no allocation past the per-snapshot maps. Useful for tests and for in-process callers that don't want to parse Prometheus text.

type MetricsOptions

type MetricsOptions struct {
	// Buckets are the histogram bucket upper bounds in seconds.
	// Sorted ascending; the implicit +Inf bucket is appended
	// internally so callers don't have to. If empty, defaults to
	// defaultMetricsBuckets — a Prometheus-standard set tuned for
	// the gogo latency range.
	Buckets []float64

	// Namespace prefixes every metric name (`<namespace>_<metric>`).
	// Default "http".
	Namespace string

	// Subsystem inserts a second prefix
	// (`<namespace>_<subsystem>_<metric>`). Empty by default — only
	// useful when an app exposes multiple metric groups under
	// distinct middlewares.
	Subsystem string

	// OnObservation fires after every request the middleware
	// instruments. It receives the same bounded method/status label
	// values used by the Prometheus output and never receives path or
	// route labels by default. Hook callers wire this to their tracer
	// of choice (OpenTelemetry, Datadog, etc.) without dragging the
	// SDK into gogo's dependency tree. The callback runs inline on
	// the handler goroutine — keep it non-blocking; spawn a
	// goroutine yourself if the export crosses the network.
	OnObservation func(method, status string, dur time.Duration)
}

MetricsOptions configures the Metrics middleware.

type RateLimitOptions

type RateLimitOptions struct {
	// Max is the number of requests allowed per Window per key.
	// Required; zero disables the middleware (it passes everything
	// through).
	Max int

	// Window is the fixed window length. Counters reset at each
	// boundary. Required.
	Window time.Duration

	// KeyFunc derives the bucket key from the request. Default req.IP(), which
	// is the immediate TCP peer. Behind trusted proxies, override this when you
	// want end-client limits, for example by selecting from req.IPs(). For
	// AsyncStore on async routes, req.IP() requires Config.CapturePeerIP unless
	// Config.TrustedProxies auto-enabled it; when the default key cannot read a
	// peer IP, the middleware rejects instead of using an empty or header-derived
	// key.
	KeyFunc func(*gogo.Request) string

	// SkipFunc, when non-nil and returning true, bypasses the limit
	// entirely. Useful for whitelisting health-check probes or
	// authenticated admin traffic.
	SkipFunc func(*gogo.Request) bool

	// OnLimit, when non-nil, is invoked instead of the default 429
	// Too Many Requests response when a key is over its limit. The
	// middleware has already attached the X-RateLimit-* and
	// Retry-After headers before calling OnLimit.
	OnLimit gogo.Handler

	// Store overrides the in-memory bucket store. Empty default is a
	// process-local map[string]*rateBucket guarded by a mutex.
	// Provide an implementation backed by Redis / Memcache for
	// distributed deployments.
	Store RateLimitStore

	// MaxBuckets caps the in-memory store's bucket count to bound
	// memory growth under high key cardinality (e.g. attacker spam
	// with a unique key per request). When the cap is hit the store
	// sweeps expired buckets first; if that still doesn't free space,
	// the OLDEST remaining bucket (by resetAt) is evicted to make
	// room. Zero (default) means 100_000 — enough for legitimate
	// fleets, low enough that worst-case memory stays under ~10 MiB.
	// NoRateLimitBucketLimit disables the cap (not recommended outside
	// tests).
	// Only consulted when Store is the default MemoryRateLimitStore.
	MaxBuckets int

	// AsyncStore places the middleware in the async chain only. Enable
	// this when Store.Hit may block on Redis, Memcache, SQL, or network
	// I/O so the hit check runs on the worker goroutine instead of the
	// uWS event-loop thread.
	//
	// Async-placed middleware fires only on GetAsync / PostAsync routes;
	// sync routes do not see it. Keep the default false for the built-in
	// in-memory store or for fast non-blocking custom stores where early
	// sync rejection is desired.
	AsyncStore bool
}

RateLimitOptions configures the in-memory fixed-window rate limiter. Zero value means "no limit" — at minimum Max and Window must be set.

type RateLimitStore

type RateLimitStore interface {
	// Hit increments the counter for key in the current window and
	// returns the new count and the time the window resets. If the
	// window has rolled over, the store should reset to 1.
	Hit(key string, window time.Duration) (count int, resetAt time.Time)
}

RateLimitStore abstracts the bucket backend so production deployments can swap in Redis / Memcache for shared counters across instances.

type RequestIDOptions

type RequestIDOptions struct {
	// Header is the request/response header name carrying the ID.
	// Default "X-Request-ID". Some shops prefer X-Correlation-ID or
	// Traceparent-style values — set this to whatever your edge proxy
	// already uses so the chain stays consistent.
	Header string

	// MaxLength caps an incoming request ID before it is reused and echoed
	// back. Overlong IDs are discarded and a fresh ID is generated instead.
	// Default 128. Negative disables the length cap.
	MaxLength int

	// Validator can reject incoming IDs that don't match your fleet's
	// format. When nil, the default accepts visible non-space ASCII only.
	// Rejected IDs are replaced with a generated ID.
	Validator func(string) bool

	// Generator produces a new ID when the request has no incoming
	// value. Default generates 32 random hex characters (16 bytes of
	// crypto/rand, 128 bits of entropy). Replace with uuid.NewString
	// or a ULID generator for systems with established conventions.
	Generator func() string
}

RequestIDOptions configures RequestID. Zero value reads / sets X-Request-ID and generates 32-hex-char (128-bit) IDs from crypto/rand.

type Session

type Session struct {
	ID string
	// contains filtered or unexported fields
}

Session is the per-request session handle handlers manipulate during a request. Mutations are persisted to the Store after the handler returns (via Response.OnFinish, which runs after any Response.Async goroutine completes); concurrent goroutines writing to the same handle race — sessions are intended to be used from the request goroutine only.

Handlers that need state to land mid-flight (before the response is fully written) can call Save explicitly.

func (*Session) Delete

func (s *Session) Delete(key string)

Delete removes a key from the session. The same "mutate before streaming" contract documented on Set applies here.

func (*Session) Destroy

func (s *Session) Destroy()

Destroy clears the session payload, asks the middleware to remove the store row after the handler returns, and expires the session cookie immediately when headers are still writable. If the response is already in flight, the next request rotates a stale signed cookie to a fresh session id rather than reusing the destroyed id.

func (*Session) Get

func (s *Session) Get(key string) any

Get returns the value at key, or nil when absent.

func (*Session) Save

func (s *Session) Save()

Save persists the current session state to the configured Store immediately, without waiting for the response to finish. Useful when a handler:

  • wants the session row to land before launching a background goroutine that depends on the persisted state
  • performs a Response.Async upgrade and writes session mutations from inside the goroutine but wants an intermediate checkpoint
  • emits a streaming response (SSE) and needs the auth payload committed before sending events

The middleware's deferred OnFinish callback still runs at the end of the request, so a Save followed by additional mutations followed by handler return all end up persisted — Save just adds a synchronous checkpoint.

Safe to call multiple times. Idempotent when nothing changed (dirty flag short-circuits the store write).

func (*Session) Set

func (s *Session) Set(key string, value any)

Set writes value at key and marks the session for save.

Call Set / Delete BEFORE the response begins streaming (i.e. before res.Send / Status / Write / Stream / SSE start emitting bytes). If the session was loaded from a stale signed cookie, the first write rotates the session id and the rotation cookie is queued onto the response via res.SetCookie — once the response headers are on the wire that cookie can no longer reach the client and the next request from this user will arrive with the now-orphaned old id. The framework cannot prevent this without breaking valid streaming patterns, so the contract is documented and enforced by convention: finish your session mutations early in the handler.

type SessionOptions

type SessionOptions struct {
	// Secret signs the session-id cookie with HMAC so it cannot be
	// forged by clients (and is bound to this server fleet). Required;
	// must be at least 32 bytes of entropy.
	Secret []byte

	// Store is the persistence backend. Default
	// NewMemorySessionStore() — single-process, lost on restart.
	Store SessionStore

	// CookieName is the Set-Cookie name carrying the session id.
	// Default "session".
	CookieName string

	// CookiePath / CookieDomain mirror the cookie attributes of
	// the same names. Path defaults to "/", Domain empty.
	CookiePath   string
	CookieDomain string

	// CookieSecure sets the Secure flag — should be true in
	// production so the session id never leaks over plain HTTP.
	CookieSecure bool

	// CookieSameSite sets SameSite. Default Lax.
	CookieSameSite gogo.SameSite

	// TTL is the session lifetime. Default 24 hours. Used for
	// both the cookie Max-Age and the Store's expiry hint.
	TTL time.Duration

	// SkipFunc, when non-nil and returning true, bypasses session
	// loading for that request — useful for completely
	// session-irrelevant routes (static assets) that don't want
	// to pay the store-read cost.
	SkipFunc func(*gogo.Request) bool

	// LocalKey overrides the req.Local key. Default
	// SessionLocalKey.
	LocalKey string

	// MaxEntries caps the in-memory store's bucket count to bound
	// memory growth under high session-id cardinality (long-running
	// servers with sustained signup traffic, or attackers spamming
	// new session cookies). When the cap is hit on a fresh Save,
	// the store sweeps expired entries first; if that still doesn't
	// free space, the OLDEST remaining entry (by expires) is
	// evicted to make room. Zero (default) means 100_000 — enough
	// for typical fleets, low enough that worst-case memory stays
	// under ~50 MiB even with rich session payloads.
	// NoSessionEntryLimit disables the cap (not recommended outside
	// tests).
	//
	// Only consulted when Store is the default MemorySessionStore.
	MaxEntries int

	// AsyncStore places the middleware in the async chain only. Enable
	// this when Store may block on Redis, SQL, disk, or network I/O so
	// Load / Save / Delete run on the worker goroutine instead of the
	// uWS event-loop thread.
	//
	// Async-placed middleware fires only on GetAsync / PostAsync routes;
	// sync routes do not see it. Keep the default false for the built-in
	// in-memory store or for fast non-blocking custom stores that must
	// protect sync routes too.
	AsyncStore bool
}

SessionOptions configures the Session middleware.

type SessionStore

type SessionStore interface {
	// Load returns the data map for id, or (nil, false) when the
	// id is unknown or expired. Implementations should treat
	// expired sessions as absent.
	Load(id string) (map[string]any, bool)

	// Save persists the data for id with the supplied TTL.
	// Implementations are free to round / truncate TTL.
	Save(id string, data map[string]any, ttl time.Duration) error

	// Delete removes the session.
	Delete(id string) error
}

SessionStore is the persistence backend. The default in-memory implementation is suitable for single-process deployments; supply a Redis / SQL-backed implementation for horizontally scaled apps.

Implementations must be safe for concurrent use from many goroutines.

type Snapshot

type Snapshot struct {
	TotalRequests int64
	InFlight      int64
	MeanLatency   time.Duration
	Status        map[string]int64
	Method        map[string]int64
	BucketLE      []float64
	BucketCounts  []int64 // len = len(BucketLE) + 1 (+Inf trailing)
	BytesIn       int64
	BytesOut      int64
}

Snapshot returns a copy of the current counter values for tests / programmatic readers that want to assert against the metrics without parsing the Prometheus exposition format.

type WebSocketAuthOptions

type WebSocketAuthOptions struct {
	// AllowedOrigins is the exact-match list of permitted Origin
	// header values. Empty means no browser Origin is allowed. A
	// handshake with neither Origin nor legacy Sec-WebSocket-Origin
	// is still rejected unless AllowMissingOrigin is true. To allow
	// browser clients from a specific site, list it explicitly:
	// []string{"https://app.example.com"}.
	//
	// Matching is case-insensitive. Trailing slashes are ignored.
	// The literal "*" is treated as "allow any origin" and is
	// intentionally not the zero-value default — opt in explicitly
	// when you understand the risk. Do not mix "*" with explicit
	// origins; the helper panics at construction time.
	//
	// Entries must be valid origins ("scheme://host[:port]"), "null",
	// or "*". Paths other than a single trailing slash, queries,
	// fragments, userinfo, empty entries, and control characters panic
	// at middleware construction.
	AllowedOrigins []string

	// AllowMissingOrigin permits handshakes that arrive with neither
	// Origin nor legacy Sec-WebSocket-Origin. Default false. Useful
	// for CLI tooling (websocat, curl --include) that doesn't set
	// Origin, but dangerous if any of your real clients are browsers
	// — browser requests always carry an Origin and a request without
	// one is either a non-browser or a stripped-down forgery.
	AllowMissingOrigin bool

	// Verify, when non-nil, runs after the Origin check passes and
	// is the place to plug in app-specific auth (bearer-token
	// verification, session-cookie lookup, etc.). Return a non-empty
	// userData to accept the connection with that value attached via
	// ctx.SetUserData; return nil userData with ok=true to accept
	// anonymously; return ok=false to reject with the supplied
	// status / message (defaults to 401 / "unauthorized" when zero).
	//
	// Run on the loop thread — keep it fast. For DB-backed
	// verification, look the token up at HTTP login time and stash
	// the verified bits in a signed cookie that Verify can decode
	// without I/O.
	Verify func(ctx *gogo.UpgradeContext) (userData any, ok bool, rejectStatus int, rejectMsg string)

	// AllowedSubprotocols, when non-empty, restricts the
	// Sec-WebSocket-Protocol negotiation: the helper picks the
	// first protocol from this list that the client also offered
	// in ctx.Protocols(). If the client offered no overlap the
	// connection is rejected with 400. When AllowedSubprotocols is
	// nil the helper accepts without negotiating a protocol. Entries
	// must be valid WebSocket subprotocol tokens; empty or malformed
	// entries panic at middleware construction.
	AllowedSubprotocols []string
}

WebSocketAuthOptions configures the WebSocketAuth helper. Every field is optional; the zero value rejects every handshake until the application opts into either explicit browser origins or missing-Origin CLI/service clients. That fail-closed default is the minimum defense against Cross-Site WebSocket Hijacking (CSWSH).

CSWSH is the WebSocket equivalent of CSRF: a third-party page the user has open can open ws://yoursite/... from the browser and ride the user's session cookies / bearer headers (browsers attach ambient credentials to WebSocket handshakes the same way they attach them to <img> tags). The HTTP CORS middleware does NOT cover WebSocket — the upgrade goes through a separate code path in the framework. Without an Origin check at the handshake there's nothing stopping the attacker page.

The helper wraps a user-supplied Verify callback so applications can layer their own auth (verify a bearer, look up a session row) while still benefiting from the standard Origin gate.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL