htpx

package
v0.19.4 Latest Latest
Warning

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

Go to latest
Published: Sep 20, 2026 License: Apache-2.0, Apache-2.0 Imports: 67 Imported by: 0

README

htpx — HTTP Client & Proxy (Go) + API Server + (Planned) Forward/Forward-Forward Spec Inference

This repository contains:

  1. htpx library: composable HTTP client + reverse proxy primitives.
  2. API server skeleton (Gin + optional OIDC).
  3. Planned forward/forward-forward proxy to infer API specs from traffic (OpenAPI/WS/GQL/gRPC).

Module path: devnw.dev/apic/pkg/htpx (this package lives inside the devnw.dev/apic module; it is not a standalone Go module and is not published separately). See VENDORED.md for how this package relates to api/ and to pkg/httpx. Licensed under Apache-2.0, same as the rest of the repository — see the root LICENSE file. Every source file in this package carries the standard SPDX/copyright header block; none are empty.

Install

This package is imported as part of the devnw.dev/apic module:

go get devnw.dev/apic
import "devnw.dev/apic/pkg/htpx"

Quick Start — Client

package main

import (
  "context"
  "encoding/json"
  "fmt"
  "net/url"
  "os"

  "devnw.dev/apic/pkg/htpx"
)

type User struct {
  ID   string `json:"id"`
  Name string `json:"name"`
}

func main() {
  c, _ := htpx.NewClient(context.Background(), htpx.WithRoot("https://api.example.com"))
  resp, _ := c.Get("/v1/users", url.Values{"q": {"alice"}})
  users, _ := htpx.To[[]User](c, resp)
  enc := json.NewEncoder(os.Stdout); enc.SetIndent("", "  ")
  _ = enc.Encode(users)
}

Proxy (library)

ln, _ := net.Listen("tcp", "127.0.0.1:8443")
ctx, cancel := context.WithCancel(context.Background()); defer cancel()
_ = htpx.Proxy(ctx, ln,
  htpx.WithTLSConfig(&tls.Config{MinVersion: tls.VersionTLS13}),
  htpx.WithCertificate("", ""), // self-signed fallback
)

Upcoming changes: preserve request method/body, propagate multi-value headers, and define CONNECT behavior.

API Server

  • TLS listener, CORS, OpenAPI validation, optional OIDC JWT.
  • Only install JWT middleware when OIDC config is set to avoid nil middleware.

Forward/Forward-Forward Proxy (Spec Inference)

  • Privacy-first capture (header names + body structure by default).
  • Planned modules: Recorder → Inference → Exporters → CLI/UI (deterministic). See requirements.md for acceptance tests.
CLI (apimap)

Run a capture proxy (passthrough mode):

go run ./cmd/apimap proxy run \
  -addr 127.0.0.1:8080 \
  -mode passthrough \
  -sample 1.0 \
  -capture-bodies=true \
  -capture-body-values=false \
  -redact-headers Authorization,Cookie,Set-Cookie,X-Api-Key \
  -redact-json-fields email,password,token

Key flags:

  • -sample: floating (0..1) sample rate; unsampled requests are proxied, not recorded.
  • -capture-bodies: enable body shape capture (structure only unless -capture-body-values).
  • -capture-body-values: include literal JSON primitive types/values (replaces placeholders) – higher privacy impact.
  • -max-body-bytes: hard cap on bytes read per body (default 1MB) for capture.
  • -redact-headers: comma list of header names that will be marked redacted (names still listed; values never stored).
  • -redact-json-fields: comma list of top-level JSON response fields to replace with "redacted" marker.
  • -hash-json-fields: comma list of JSON fields (top-level or dot.paths) whose values are replaced by a stable truncated SHA-256 hash stored under hashed.
  • -hash-headers: comma list of header names whose values are hashed (request as header:<Name>, response as header-resp:<Name>) and never stored raw.
  • -privacy: preset shorthand for grouped privacy behavior: strict (structure only, force value redaction); balanced (default); open (captures JSON primitive values too).
  • -ws-max-frames: limit WebSocket frames captured per connection.
  • -mitm / -root-ca: enable TLS MITM (inspect mode) with generated or provided root CA.
Network exposure (SEC-0073 / SEC-0074)

apimap proxy run is an unauthenticated forward+CONNECT proxy (and, with -mitm, a TLS-terminating one). It defaults to loopback-only and refuses to bind anywhere else without an explicit opt-in:

  • -addr defaults to 127.0.0.1:8080 (was :8080 — all interfaces — prior to SEC-0073/SEC-0074).
  • -metrics-addr defaults to 127.0.0.1:9090 for the same reason.
  • -allow-insecure-network: required before -addr or -metrics-addr may bind a non-loopback interface. Without it, apimap refuses to start (exit code 1) rather than silently listening on every interface.
  • -allow-host (repeatable): allow-lists an upstream host that would otherwise be refused because it resolves to a loopback, link-local (including the 169.254.169.254 cloud metadata address), or private IP range. Applies to every path that dials an operator-unconfirmed host: a CONNECT tunnel, the per-request MITM reverse proxy, and the plain (non-CONNECT) forward-proxy path (an absolute-URI GET/POST/etc., or a WebSocket upgrade) — all three share one allowlist gate. Resolution happens before dialing, so a hostname that merely looks public but resolves to a private address is caught too, not just an obviously numeric one. Accepts an exact host or a shell-style glob (e.g. -allow-host '*.internal.example.com'). Ordinary public hosts are always reachable; this flag only widens the private ranges. A refused request gets 403 Forbidden and is logged via slog.Warn.

Root CA material (-root-ca, used with -mitm) is split into two files: the certificate (rootCA.pem, mode 0644) and the private key (a sibling rootCA-key.pem, mode 0600). Both writes refuse to follow a pre-planted symlink at the final path component. The generated CA carries a bounded one-year validity window, a random 128-bit serial, MaxPathLen: 0, and a ServerAuth ExtKeyUsage — a pre-existing combined rootCA.pem (cert + key in one file, from before this change) still loads for one release, with a slog.Warn recommending the split layout. That combined-file fallback is scheduled for removal after v1.0 (tracked against SEC-0073/#327); new and re-provisioned deployments always get the split layout from ensureRootCA, so only an untouched pre-upgrade CA still depends on it.

Metrics (when compiled with prom build tag) are exposed via the dedicated metrics listener if -metrics is true (default) at -metrics-addr (default 127.0.0.1:9090) with Prometheus format.

Example scrape stanza:

- job_name: apimap
  static_configs:
    - targets: ['localhost:9090']

### Privacy Modes Example

Given a JSON response: `{ "email": "alice@example.com", "token": "s3cr3t", "count": 5 }`

Flags & outcomes (body shape capture enabled):

1. Default (balanced):
  - `-capture-body-values=false`
  - Output shape: `email: string`, `token: string`, `count: integer`
2. With redaction: `-redact-json-fields token`
  - `token` -> `redacted`
3. With hashing: `-hash-json-fields email`
  - `hashed.email=<stable 16 hex>`; no raw email stored
4. Open mode: `-privacy open` (or `-capture-body-values=true`)
  - Literals preserved (`email=alice@example.com`, `count=5`)
5. Combined: `-redact-json-fields token -hash-json-fields email -privacy open`
  - `token` redacted, `email` hashed (still no raw email), other literals visible

Header hashing works similarly via `-hash-headers Authorization,Set-Cookie` producing `hashed.header:Authorization` entries without raw values.

Dev

go test ./... -v

Documentation

Overview

Package htpx is the HTTP application toolkit that backs apic-generated services. It wires a Gin-based TLS server (driven by the OpenAPI spec via oapi-codegen middleware, CORS, and slog request logging), a buffered outbound HTTP Client with retry, rate limiting, and a response-body size ceiling, and a TLS-terminating reverse Proxy with pluggable rules, subnet filters, and request rewriters. It also provides the request-authentication surfaces: API keys (with pluggable stores and scope checks), FIDO/WebAuthn, HMAC request signatures, and OIDC.

TLS here is server-side only (SEC-NEW2-04): serverTLSConfig builds its tls.Config through fipsx.NewServerTLSConfig(nil), which sets ClientAuth: tls.NoClientCert, and no ServerOption in this package sets ClientAuth or ClientCAs. htpx therefore does NOT enforce mutual TLS -- client-certificate authentication lives in api/ and in the generated server (server.tls.mtls_ca_path, pkg/securex/mtlsx). Do not read the server surface here as an mTLS boundary.

Structured logging flows through pkg/log.

Index

Constants

View Source
const (
	// DefaultRead is the default http.Server.ReadTimeout used by the proxy's
	// listener.
	DefaultRead = Timeout(5 * time.Second)
	// DefaultWrite is the default http.Server.WriteTimeout used by the
	// proxy's listener.
	DefaultWrite = Timeout(10 * time.Second)
	// DefaultReadHeader is the default http.Server.ReadHeaderTimeout used by
	// the proxy's listener.
	DefaultReadHeader = Timeout(5 * time.Second)

	// DefaultClient is the default overall client-side request timeout
	// (documented alongside DefaultResponse; see the http.Client/Transport
	// timeout fields these constants mirror).
	DefaultClient = Timeout(10 * time.Second)
	// DefaultResponse is the default response-header wait timeout, mirroring
	// http.Transport.ResponseHeaderTimeout.
	DefaultResponse = Timeout(10 * time.Second)

	// DefaultTLSHandshake is the default TLS handshake timeout, mirroring
	// http.Transport.TLSHandshakeTimeout.
	DefaultTLSHandshake = Timeout(10 * time.Second)

	// DefaultKeepAlive is the default connection keep-alive interval,
	// mirroring net.Dialer.KeepAlive.
	DefaultKeepAlive = Timeout(30 * time.Second)
	// DefaultExpect is the default "Expect: 100-continue" wait timeout,
	// mirroring http.Transport.ExpectContinueTimeout.
	DefaultExpect = Timeout(1 * time.Second)
)
View Source
const (
	// OAuth2Scopes was intended as the context key under which per-path
	// OAuth2 scope requirements would be stashed for downstream handlers to
	// read, but nothing in this package ever sets or reads it. QG-095
	// (#271).
	//
	// Deprecated: unused; will be removed in the next major version.
	OAuth2Scopes = "oauth2_scopes"
	// ENV is the environment variable Gin reads its running mode from
	// (GIN_MODE). This package consults gin.Mode() (backed by the same
	// variable) rather than reading it directly; the constant documents
	// which variable that is.
	ENV = "GIN_MODE" // running environment
	// DEVENV is Gin's debug-mode value ("debug") for GIN_MODE/ENV. Gin
	// defaults to this mode when GIN_MODE is unset, which is exactly why
	// the insecure-dev bypass (see INSECURE_DEV) additionally requires an
	// explicit APIC_ENV=development assertion rather than trusting debug
	// mode alone (securex.AllowInsecureDev's condition 4).
	DEVENV = "debug" // development environment
	// INSECURE_DEV names the environment variable (APIC_INSECURE_DEV) that,
	// together with debug mode and (for per-request use) an explicit
	// APIC_ENV=development assertion, opts into the insecure-dev bypass:
	// permitting a plaintext (non-TLS) loopback-only listener in newListener,
	// and forcing CORS to allow all origins in WithCORS. See
	// securex.AllowInsecureDev/AllowInsecureDevAtStartup for the full gate.
	// Leaving this set to a truthy value in a production (release-mode)
	// deployment is refused fail-closed (the securex helpers panic) rather
	// than silently doing nothing.
	INSECURE_DEV = "APIC_INSECURE_DEV"
)
View Source
const DefaultMaxResponseBytes int64 = 32 << 20

DefaultMaxResponseBytes is the secure-default ceiling on buffered response bodies (32 MiB). An unauthenticated/compromised upstream returning a multi-gigabyte body would otherwise OOM the client via io.ReadAll (PERF-0024). Raise or disable per-client with WithMaxResponseBytes.

View Source
const MaxSignatureBodyBytes int64 = 32 << 20 // 32 MiB

MaxSignatureBodyBytes caps the request body size that signature signing and verification will hash for the Digest header. Bodies larger than this are rejected with ErrSignatureBodyTooLarge instead of being buffered into memory. The cap matches PERF-0014's response body cap so signed flows on either side of the wire share the same backpressure ceiling.

Variables

View Source
var (
	// ErrAPIKeyNotFound is returned by APIKeyStore implementations when
	// a lookup or delete targets a key that is not present. Replaces the
	// six "API key not found" anonymous errors.New sites in apikey.go.
	ErrAPIKeyNotFound = errors.New("htpx: api key not found")
	// ErrAPIKeyInvalid is returned by APIKeyManager.ValidateKey when the
	// provided key string does not hash to a known stored key. The
	// message intentionally avoids leaking whether the key was unknown
	// versus mismatched (to limit oracle behavior under brute-force).
	ErrAPIKeyInvalid = errors.New("htpx: invalid api key")
	// ErrAPIKeyDisabled is returned by APIKeyManager.ValidateKey when
	// the key exists but is not enabled.
	ErrAPIKeyDisabled = errors.New("htpx: api key is disabled")
	// ErrAPIKeyExpired is returned by APIKeyManager.ValidateKey when the
	// key exists, is enabled, but its ExpiresAt is in the past.
	ErrAPIKeyExpired = errors.New("htpx: api key is expired")

	// ErrFIDOUserNotFound is returned by FIDOStore implementations when
	// a user lookup misses. Returned by GetUser, GetUserByName, and
	// GetCredentials in MemoryFIDOStore.
	ErrFIDOUserNotFound = errors.New("htpx: fido user not found")
	// ErrFIDOConfigRequired is returned by NewFIDOServer when the
	// supplied configuration pointer is nil.
	ErrFIDOConfigRequired = errors.New("htpx: fido config is required")

	// ErrOIDCEndpointEmpty is returned by the API server constructor
	// when an authenticated build is requested without an OIDC endpoint
	// AND insecure-dev startup is not allowed.
	ErrOIDCEndpointEmpty = errors.New("htpx: oidc endpoint is empty")
	// ErrValidationSpecMissing is returned by WithValidation when OIDC
	// auth is configured but no OpenAPI spec was supplied. The OpenAPI
	// request validator mounted from the spec is the only component
	// that invokes the authentication function, so without a spec every
	// `security:`-declared route would be served unauthenticated
	// (fail-open). htpx has no embedded spec to fall back on; pass
	// WithSwaggerSpec before WithValidation. Sibling of appsec N-01
	// fixed in api/ (73d6568).
	ErrValidationSpecMissing = errors.New(
		"htpx: oidc auth is configured but no OpenAPI spec was supplied;" +
			" pass WithSwaggerSpec before WithValidation so the auth" +
			" enforcer can be mounted",
	)
	// ErrRegisterFunctionNil is returned by api.New when called without
	// a route registration function.
	ErrRegisterFunctionNil = errors.New("htpx: register function is nil")
	// ErrInsecureDevNonLoopback is returned by newListener when
	// APIC_INSECURE_DEV is set but the host is not a loopback address.
	ErrInsecureDevNonLoopback = errors.New("htpx: APIC_INSECURE_DEV is only allowed on loopback hosts")
	// ErrMissingTLSPaths is returned by newListener when neither
	// insecure-dev nor a cert/key path pair is provided.
	ErrMissingTLSPaths = errors.New("htpx: missing TLS cert/key paths")

	// ErrTransportNil is returned by WithTransport when the supplied
	// http.RoundTripper is nil.
	ErrTransportNil = errors.New("htpx: transport is nil")
	// ErrClientNil is returned by WithDoer when the supplied
	// HTTPClient is nil.
	ErrClientNil = errors.New("htpx: client is nil")

	// ErrCircuitBreakerOpen is returned by CircuitBreaker.Execute when
	// the breaker is in the open state and the call is short-circuited.
	ErrCircuitBreakerOpen = errors.New("htpx: circuit breaker is open")
	// ErrCacheMiss is returned by MemoryCacheStore.Get when no entry
	// exists for the requested key.
	ErrCacheMiss = errors.New("htpx: cache miss")
	// ErrCacheExpired is returned by MemoryCacheStore.Get when an entry
	// exists but its TTL has elapsed.
	ErrCacheExpired = errors.New("htpx: cache expired")

	// ErrReadResponseBody is wrapped via errors.Join when ReadJSON fails
	// to read the response body off the wire.
	ErrReadResponseBody = errors.New("htpx: failed to read response body")
	// ErrUnmarshalResponseBody is wrapped via errors.Join when ReadJSON
	// fails to decode the response body as JSON.
	ErrUnmarshalResponseBody = errors.New("htpx: failed to unmarshal response body")
	// ErrResponseBodyTooLarge is returned when a response body exceeds the
	// client's maxResponseBytes ceiling (see WithMaxResponseBytes). PERF-0024.
	ErrResponseBodyTooLarge = errors.New("htpx: response body exceeds maximum")

	// ErrNoTokenToRefresh is returned by TokenRefresher.Start when the
	// refresher has no current token to refresh.
	ErrNoTokenToRefresh = errors.New("htpx: no token to refresh")

	// ErrAuthTokenMissing is returned by AuthToken when no JWT is
	// present in the request context.
	ErrAuthTokenMissing = errors.New("htpx: failed to get auth token")
	// ErrJWTMissingScopeClaim is returned by OIDC.Authenticate when
	// scopes are required but the token has no "scope" claim.
	ErrJWTMissingScopeClaim = errors.New("htpx: missing scope claim in jwt")
	// ErrJWTScopeClaimNotString is returned by OIDC.Authenticate when
	// the "scope" claim exists but is not a string.
	ErrJWTScopeClaimNotString = errors.New("htpx: jwt scope claim is not a string")
	// ErrJWKEndpointEmpty is returned by OIDC.UnmarshalJSON when the
	// JWK endpoint returns no keys at cold start.
	ErrJWKEndpointEmpty = errors.New("htpx: no keys found in jwk endpoint")
	// ErrUnsupportedJWTAlg is returned by ExtractToken when an inbound
	// token declares a JWS algorithm outside the asymmetric allowlist
	// (e.g. "none" or HS256/384/512). Mirrors api/oidc.go (QG-060) so the
	// JWKS verification path cannot be downgraded via algorithm
	// confusion. QG-081.
	ErrUnsupportedJWTAlg = errors.New("htpx: unsupported jwt algorithm")

	// ErrSignatureConfigRequired is returned by NewSigner when given a
	// nil SignatureConfig.
	ErrSignatureConfigRequired = errors.New("htpx: signature config is required")
	// ErrSignatureKeyIDRequired is returned by NewSigner when the
	// SignatureConfig has an empty KeyID.
	ErrSignatureKeyIDRequired = errors.New("htpx: signature key id is required")
	// ErrSignaturePrivateKeyRequired is returned by NewSigner when the
	// SignatureConfig has a nil PrivateKey.
	ErrSignaturePrivateKeyRequired = errors.New("htpx: signature private key is required")
	// ErrSignatureHMACKeyType is returned by Signer.sign and Verifier.verify
	// when the configured key for HMAC algorithms is not a []byte.
	ErrSignatureHMACKeyType = errors.New("htpx: hmac key must be []byte")
	// ErrSignatureRSAPrivateKey is returned by Signer.sign when the
	// configured private key is not an *rsa.PrivateKey.
	ErrSignatureRSAPrivateKey = errors.New("htpx: rsa private key required")
	// ErrSignatureECDSAPrivateKey is returned by Signer.sign when the
	// configured private key is not an *ecdsa.PrivateKey.
	ErrSignatureECDSAPrivateKey = errors.New("htpx: ecdsa private key required")
	// ErrSignatureED25519PrivateKey is returned by Signer.sign when the
	// configured private key is not an ed25519.PrivateKey.
	ErrSignatureED25519PrivateKey = errors.New("htpx: ed25519 private key required")
	// ErrSignatureMissingAuthHeader is returned by VerifyRequest when
	// the request has no Authorization header.
	ErrSignatureMissingAuthHeader = errors.New("htpx: missing authorization header")
	// ErrSignatureDateHeaderMissing is returned by VerifyRequest when
	// the signed headers include "date" but the request has none.
	ErrSignatureDateHeaderMissing = errors.New("htpx: date header required but not found")
	// ErrSignatureClockSkew is returned by VerifyRequest when the date
	// header is outside the configured clock-skew window. The message
	// intentionally retains the substring "clock skew" for compatibility
	// with existing tests that match on it.
	ErrSignatureClockSkew = errors.New("htpx: date header outside allowed clock skew")
	// ErrSignatureHMACVerify is returned by Verifier.verify when the
	// HMAC signature does not match the expected value.
	ErrSignatureHMACVerify = errors.New("htpx: hmac verification failed")
	// ErrSignatureRSAPublicKey is returned by Verifier.verify when the
	// configured public key is not an *rsa.PublicKey.
	ErrSignatureRSAPublicKey = errors.New("htpx: rsa public key required")
	// ErrSignatureECDSAPublicKey is returned by Verifier.verify when the
	// configured public key is not an *ecdsa.PublicKey.
	ErrSignatureECDSAPublicKey = errors.New("htpx: ecdsa public key required")
	// ErrSignatureECDSAVerify is returned by Verifier.verify when the
	// ECDSA signature does not match.
	ErrSignatureECDSAVerify = errors.New("htpx: ecdsa verification failed")
	// ErrSignatureED25519PublicKey is returned by Verifier.verify when
	// the configured public key is not an ed25519.PublicKey.
	ErrSignatureED25519PublicKey = errors.New("htpx: ed25519 public key required")
	// ErrSignatureED25519Verify is returned by Verifier.verify when the
	// Ed25519 signature does not match.
	ErrSignatureED25519Verify = errors.New("htpx: ed25519 verification failed")
	// ErrSignatureHeaderFormat is returned by parseSignatureHeader when
	// the Authorization header does not start with "Signature ".
	ErrSignatureHeaderFormat = errors.New("htpx: invalid signature header format")
	// ErrSignatureHeaderIncomplete is returned by parseSignatureHeader
	// when the Signature header is missing required fields.
	ErrSignatureHeaderIncomplete = errors.New("htpx: incomplete signature header")
	// ErrSignatureDigestHeaderMissing is returned by verifyDigest when
	// the request is missing the Digest header that was signed.
	ErrSignatureDigestHeaderMissing = errors.New("htpx: digest header required but not found")
	// ErrSignatureDigestUnsupported is returned by verifyDigest when the
	// Digest header uses an algorithm other than SHA-256.
	ErrSignatureDigestUnsupported = errors.New("htpx: only SHA-256 digest supported")
	// ErrSignatureDigestMismatch is returned by verifyDigest when the
	// computed digest does not match the value supplied in the header.
	ErrSignatureDigestMismatch = errors.New("htpx: digest mismatch")
	// ErrSignaturePEMDecode is returned by LoadPrivateKey and
	// LoadPublicKey when the PEM block cannot be decoded.
	ErrSignaturePEMDecode = errors.New("htpx: failed to decode PEM block")
)

Sentinel errors for the htpx package. Kept at package level per repo conventions. Messages use a lowercase, package-prefixed, diagnostic form so log output reads naturally; errors.Is callers should rely on pointer equality rather than substring matching of Error().

Sentinels that have richer documentation (ErrInvalidURL, ErrInvalidScheme, ErrAuthHeaderMissing, ErrMalformedAuthHeader, ErrEmptyAudience, ErrSignatureBodyTooLarge, ErrNotApplicable) are declared at their use sites.

View Source
var ErrAuthHeaderMissing = errors.New("htpx: authorization header missing")

ErrAuthHeaderMissing is returned by ExtractToken when the request carries no Authorization header. OIDC.JWT treats this specific error as an anonymous request rather than aborting it.

View Source
var ErrEmptyAudience = errors.New("htpx: empty audience")

ErrEmptyAudience is returned by ExtractToken when the OIDC config's Aud field is empty, since an empty audience would make JWT audience validation meaningless (any token would match).

View Source
var ErrInvalidScheme = errors.New("htpx: invalid scheme")

ErrInvalidScheme is returned by LoadOIDC when the discovery URL's scheme is not "https", and by OIDC.UnmarshalJSON when any endpoint advertised in the discovery document (jwks_uri, authorization_endpoint, token_endpoint, userinfo_endpoint) is not https. OIDC endpoints are trust anchors for token verification, so a non-TLS scheme is refused rather than silently accepted.

View Source
var ErrInvalidURL = errors.New("htpx: invalid url")

ErrInvalidURL is returned when a configured URL fails to parse as a valid url.URL.

View Source
var ErrIssuerMismatch = errors.New("htpx: discovery issuer does not match the discovery URL")

ErrIssuerMismatch is returned by LoadOIDC when the discovery document's "issuer" does not match the discovery URL it was fetched from (OIDC Discovery 1.0 §4.3: issuer + "/.well-known/openid-configuration" MUST equal the request URL). Without this check, a discovery response could advertise an arbitrary issuer that ExtractToken would trust as the expected iss, letting a compromised or misconfigured discovery endpoint redirect trust to an issuer the operator never intended (SEC-0057, GitLab #311).

View Source
var ErrJWKSTransportWrapped = errors.New("htpx: jwks transport is a wrapped Doer (ClientWrapper), not a single-hop http.RoundTripper")

ErrJWKSTransportWrapped is returned by LoadOIDC (via jwksHTTPClient) when the client's transport is a ClientWrapper -- a whole *http.Client Do method (WithDoer, WithOAuth2, WithLimiter) masquerading as a RoundTripper. Such a transport resolves an entire redirect chain internally before oidcx's outer CheckRedirect (refuseJWKSRedirects) ever sees it, silently reopening the SEC-0057 (GitLab #311) JWKS-redirect bypass. The JWKS fetch needs a genuine single-hop RoundTripper, so this fails closed instead of guessing.

View Source
var ErrMalformedAuthHeader = errors.New("htpx: malformed authorization header")

ErrMalformedAuthHeader is returned by ExtractToken when the Authorization header is present but does not start with the "Bearer " prefix, and by rejectNonAsymmetricAlg when the token's JOSE header cannot be parsed.

View Source
var ErrNotApplicable = errors.New("htpx: rewriter not applicable")

ErrNotApplicable is returned by Rewriter.Process when in's host (or port, if From specifies one) does not match From, meaning this rule does not apply to the given request.

View Source
var ErrSignatureBodyTooLarge = errors.New("htpx: signed-request body exceeds size cap")

ErrSignatureBodyTooLarge is returned by computeDigest (via SignRequest and VerifyRequest) when the request body exceeds MaxSignatureBodyBytes. It is wrapped, so callers should use errors.Is to test it.

Functions

func APIKeyMiddleware

func APIKeyMiddleware(cfg *APIKeyAuthConfig) gin.HandlerFunc

APIKeyMiddleware creates a Gin middleware for API key authentication

func Args

func Args(app string) cli.Args

Args returns the shared cli.Args definitions for an htpx-based server (host, port, cert, key, ca, oidc, aud, cors, allowed-proxies, rate-limit, rate-duration), labeling the host argument's description with app. The definitions are built exactly once per process via sync.Once: only the first call's app value is ever used, and every call (with any app) thereafter returns that same shared cli.Args map.

func AuthToken

func AuthToken(ctx context.Context) (jwt.Token, error)

AuthToken retrieves the verified JWT token that OIDC.JWT previously stashed on ctx under ctxTokenKey. Returns ErrAuthTokenMissing if ctx carries no such value (e.g. the JWT middleware never ran, or ran but skipped verification). The returned jwt.Token carries the caller's claims and must be treated as sensitive identity/authorization data.

func CacheMiddleware

func CacheMiddleware(cfg *CacheConfig) gin.HandlerFunc

CacheMiddleware creates a response caching middleware

func CircuitBreakerMiddleware

func CircuitBreakerMiddleware(cb *CircuitBreaker) gin.HandlerFunc

CircuitBreakerMiddleware creates a circuit breaker middleware for Gin

func Clone

func Clone(in *http.Request) *http.Request

Clone returns a shallow copy of in with its URL (CloneURL) and Header (CloneHeader) deep-copied, so mutating the clone's URL/headers (as proxy Rules do) does not affect the original request. The Body reference itself is shared (not cloned), preserving the original method and body for forwarding.

func CloneHeader

func CloneHeader(in http.Header) http.Header

CloneHeader returns a deep copy of in: a new http.Header whose per-key value slices are independent copies, so appending to or mutating the clone's values never mutates in's (multi-value-header aware).

func CloneResponse

func CloneResponse(in *http.Response) *http.Response

CloneResponse returns a shallow copy of in with its Header deep-copied (CloneHeader), so mutating the clone's headers does not affect in's. The Body reference itself is shared, not cloned.

func CloneURL

func CloneURL(in *url.URL) *url.URL

CloneURL returns a shallow copy of in as a new *url.URL value, so mutating the fields of the returned URL does not affect in.

func CompressionMiddleware

func CompressionMiddleware(cfg *CompressionConfig) gin.HandlerFunc

CompressionMiddleware creates a response compression middleware

func ConstantTimeCompare

func ConstantTimeCompare(a, b string) bool

ConstantTimeCompare performs a constant-time comparison of two strings

func FIDOMiddleware

func FIDOMiddleware(store FIDOStore) gin.HandlerFunc

FIDOMiddleware creates a Gin middleware for FIDO2 authentication

func GenerateKeyPair

func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error)

GenerateKeyPair generates a new RSA key pair for testing

func GenerateTLSCertificate

func GenerateTLSCertificate(
	l net.Listener, names ...string,
) (tls.Certificate, error)

GenerateTLSCertificate generates a self-signed TLS certificate.

func LoadPrivateKey

func LoadPrivateKey(pemBytes []byte) (any, error)

LoadPrivateKey loads a private key from PEM bytes

func LoadPublicKey

func LoadPublicKey(pemBytes []byte) (any, error)

LoadPublicKey loads a public key from PEM bytes

func Proxy

func Proxy(
	ctx context.Context,
	l net.Listener,
	opts ...ProxyOption,
) error

Proxy runs a forward HTTP proxy on listener l until ctx is canceled, applying opts (WithClient, WithRule, WithSubnetFilter, WithCertificate, WithTLSConfig, WithRewriters) to configure it first. If WithTLSConfig set a *tls.Config, l is wrapped in a TLS listener using WithCertificate's certgen as GetCertificate. Any WithRewriters entries are collapsed into a single "rewriters" Rule that applies the first Rewriter whose Process succeeds. Blocks serving requests (see (*proxy).serve) until the listener is closed or ctx is done, at which point the server shuts down gracefully. CONNECT requests are not implemented and receive 501 Not Implemented.

func RequestLogger

func RequestLogger(logger *slog.Logger) gin.HandlerFunc

RequestLogger returns the Gin request-logging middleware htpx installs on every server. It emits one structured slog record per request via github.com/samber/slog-gin with slog-gin's DefaultConfig: the request method and path are logged under a "request" group (request.method, request.path), the response status under a "response" group (response.status), and a request id under the "id" key (taken from the inbound X-Request-Id header, or generated when absent). Passing a logger backed by a custom slog.Handler lets callers/tests capture and assert that contract; New uses RequestLogger(slog.Default()) so the production wiring and the tested wiring share one construction point (GAP-0057).

func RequireScope

func RequireScope(scope string) gin.HandlerFunc

RequireScope returns a middleware that requires a specific scope

func SignatureMiddleware

func SignatureMiddleware(verifier *Verifier) gin.HandlerFunc

SignatureMiddleware creates a Gin middleware for signature verification

func To

func To[T any](c *Client, resp *http.Response) (T, error)

To reads and closes resp.Body, decoding it as JSON into a zero value of T and returning it. If resp.StatusCode indicates an error (>= 300), the body is read only as a diagnostic (truncation on a read failure is tolerated) and To instead returns the zero value of T along with an error describing the status code/text; a body-read failure wraps ErrReadResponseBody and a JSON-decode failure wraps ErrUnmarshalResponseBody. Callers must not also close resp.Body themselves.

Types

type APIKey

type APIKey struct {
	ID          string         `json:"id"`
	Name        string         `json:"name"`
	KeyHash     string         `json:"key_hash"`
	Prefix      string         `json:"prefix"`
	Scopes      []string       `json:"scopes"`
	Metadata    map[string]any `json:"metadata"`
	Created     time.Time      `json:"created"`
	LastUsed    time.Time      `json:"last_used"`
	ExpiresAt   *time.Time     `json:"expires_at,omitempty"`
	RateLimitID string         `json:"rate_limit_id"`
	Enabled     bool           `json:"enabled"`
}

APIKey represents an API key

func GetAPIKey

func GetAPIKey(c *gin.Context) (*APIKey, bool)

GetAPIKey retrieves the authenticated API key from the context

func (*APIKey) HasScope

func (k *APIKey) HasScope(scope string) bool

HasScope checks if the API key has a specific scope

func (*APIKey) IsExpired

func (k *APIKey) IsExpired() bool

IsExpired checks if the API key has expired

type APIKeyAuthConfig

type APIKeyAuthConfig struct {
	// Manager is the API key manager
	Manager *APIKeyManager
	// HeaderName is the header to check for the API key (default: X-API-Key)
	HeaderName string
	// RequireScopes are the scopes required for this route
	RequireScopes []string
	// Optional makes the API key optional (for public endpoints with optional auth)
	Optional bool
}

APIKeyAuthConfig configures API key authentication

type APIKeyManager

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

APIKeyManager manages API keys

func NewAPIKeyManager

func NewAPIKeyManager(store APIKeyStore, logger *slog.Logger) *APIKeyManager

NewAPIKeyManager creates a new API key manager

func (*APIKeyManager) DeleteKey

func (m *APIKeyManager) DeleteKey(id string) error

DeleteKey deletes an API key

func (*APIKeyManager) GenerateKey

func (m *APIKeyManager) GenerateKey(name string, scopes []string, metadata map[string]any, expiresAt *time.Time) (*APIKey, string, error)

GenerateKey generates a new API key

func (*APIKeyManager) GetKey

func (m *APIKeyManager) GetKey(id string) (*APIKey, error)

GetKey returns a specific API key by ID

func (*APIKeyManager) ListKeys

func (m *APIKeyManager) ListKeys() ([]*APIKey, error)

ListKeys returns all API keys

func (*APIKeyManager) RevokeKey

func (m *APIKeyManager) RevokeKey(id string) error

RevokeKey revokes an API key

func (*APIKeyManager) ValidateKey

func (m *APIKeyManager) ValidateKey(keyStr string) (*APIKey, error)

ValidateKey validates an API key and returns the key object

type APIKeyStats

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

APIKeyStats tracks API key usage statistics

func NewAPIKeyStats

func NewAPIKeyStats() *APIKeyStats

NewAPIKeyStats creates a new stats tracker

func (*APIKeyStats) GetAllStats

func (s *APIKeyStats) GetAllStats() map[string]any

GetAllStats returns stats for all API keys

func (*APIKeyStats) GetStats

func (s *APIKeyStats) GetStats(keyID string) map[string]any

GetStats returns stats for a specific API key

func (*APIKeyStats) RecordRequest

func (s *APIKeyStats) RecordRequest(keyID string, err error)

RecordRequest records a request for an API key

type APIKeyStore

type APIKeyStore interface {
	// GetByHash retrieves an API key by its hash
	GetByHash(hash string) (*APIKey, error)
	// GetByID retrieves an API key by its ID
	GetByID(id string) (*APIKey, error)
	// GetByPrefix retrieves an API key by its prefix
	GetByPrefix(prefix string) (*APIKey, error)
	// Save saves or updates an API key
	Save(key *APIKey) error
	// Delete deletes an API key by ID
	Delete(id string) error
	// List returns all API keys
	List() ([]*APIKey, error)
	// UpdateLastUsed updates the last used timestamp
	UpdateLastUsed(id string, t time.Time) error
}

APIKeyStore defines the interface for storing and retrieving API keys

type CacheConfig

type CacheConfig struct {
	// Store is the cache store
	Store CacheStore
	// TTL is the default cache time-to-live
	TTL time.Duration
	// KeyFunc generates cache keys (default: method + URL)
	KeyFunc func(*gin.Context) string
	// ShouldCache determines if a response should be cached
	ShouldCache func(*gin.Context) bool
}

CacheConfig configures response caching

type CacheEntry

type CacheEntry struct {
	StatusCode int
	Headers    http.Header
	Body       []byte
	Timestamp  time.Time
	TTL        time.Duration
}

CacheEntry represents a cached response

func (*CacheEntry) IsExpired

func (e *CacheEntry) IsExpired() bool

IsExpired checks if the cache entry has expired

type CacheStore

type CacheStore interface {
	Get(key string) (*CacheEntry, error)
	Set(key string, entry *CacheEntry) error
	Delete(key string) error
	Clear() error
}

CacheStore defines the interface for caching

type CircuitBreaker

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

CircuitBreaker implements the circuit breaker pattern

func NewCircuitBreaker

func NewCircuitBreaker(cfg *CircuitBreakerConfig) *CircuitBreaker

NewCircuitBreaker creates a new circuit breaker

func (*CircuitBreaker) Execute

func (cb *CircuitBreaker) Execute(fn func() error) error

Execute executes a function with circuit breaker protection

func (*CircuitBreaker) State

func (cb *CircuitBreaker) State() CircuitState

State returns the current circuit state

func (*CircuitBreaker) Stats

func (cb *CircuitBreaker) Stats() map[string]any

Stats returns circuit breaker statistics

type CircuitBreakerConfig

type CircuitBreakerConfig struct {
	// MaxFailures is the number of failures before opening the circuit
	MaxFailures int64
	// Timeout is how long to wait before attempting to close the circuit
	Timeout time.Duration
	// FailureThreshold is the percentage of failures to trigger circuit open (0-100)
	FailureThreshold float64
	// MinRequests is the minimum number of requests before evaluating failure rate
	MinRequests int64
	// OnStateChange is called when the circuit state changes
	OnStateChange func(from, to CircuitState)
}

CircuitBreakerConfig configures a circuit breaker

type CircuitState

type CircuitState int

CircuitState represents the state of a circuit breaker

const (
	// CircuitClosed is the normal operating state: requests are allowed
	// through and failures/successes are tallied to decide whether to trip
	// the breaker open.
	CircuitClosed CircuitState = iota
	// CircuitOpen means the breaker has tripped: requests are rejected with
	// ErrCircuitBreakerOpen (via CircuitBreaker.allow) until Timeout has
	// elapsed since the last recorded failure, at which point the breaker
	// moves to CircuitHalfOpen.
	CircuitOpen
	// CircuitHalfOpen is the trial state entered after Timeout elapses in
	// CircuitOpen: a single request is allowed through to probe the
	// downstream; success closes the circuit again, failure reopens it.
	CircuitHalfOpen
)

func (CircuitState) String

func (s CircuitState) String() string

String renders s as one of "closed", "open", "half-open", or "unknown" for a state value outside the three defined constants.

type Client

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

Client is htpx's HTTP client: a configurable http.RoundTripper wrapper that layers default headers, request/response logging, an optional debug body dump, and a bounded response-body reader on top of an underlying http.RoundTripper. Construct one with NewClient and Option functions (WithTransport, WithRoot, WithHeader, WithBearerToken, WithOAuth2, WithLimiter, WithRetry, WithCircuitBreaker, WithDebugFile, WithMaxResponseBytes, ...); zero-value Client is not usable directly.

func NewClient

func NewClient(ctx context.Context, opts ...Option) (*Client, error)

NewClient builds a Client bound to ctx, applying opts in order. Defaults are http.DefaultTransport as the underlying RoundTripper, the package's default log.Logger, an empty set of default request headers, and DefaultMaxResponseBytes as the response-body ceiling; any Option (WithTransport, WithHeader, WithRoot, WithMaxResponseBytes, auth helpers like WithBearerToken, etc.) can override those before the Client is returned. Returns the first error an Option produces.

func (*Client) Close

func (c *Client) Close() (out error)

Close releases resources held by c, closing the debug dump file opened by WithDebugFile (if any). It is a no-op when no debug file was configured. Any close error is returned (wrapped with errors.Join if more than one resource is ever added).

func (*Client) Delete

func (c *Client) Delete(
	uri string,
	query url.Values,
	opts ...RequestOption,
) (*http.Response, error)

Delete issues a DELETE request to uri with query as the URL's query string, after applying opts and the client's default headers.

func (*Client) Do

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do sends req through the client's configured underlying transport and, if a response is returned, captures a bounded prefix of its body to the debug file (if WithDebugFile was set) before handing the response back to the caller. Unlike request/Get/Post/Put/Delete, Do does not apply the client's default headers or query normalization — callers using Do directly are responsible for building a complete *http.Request.

func (*Client) Get

func (c *Client) Get(
	uri string,
	query url.Values,
	opts ...RequestOption,
) (*http.Response, error)

Get issues a GET request to uri (resolved against the client's root URL via WithRoot, if any) with query encoded as the URL's query string, after applying opts and the client's default headers. Returns the raw *http.Response for the caller to read/close; pair with To for JSON decoding.

func (*Client) JSONReader

func (c *Client) JSONReader(in any) (io.ReadCloser, error)

JSONReader marshals in to JSON and wraps it in a no-op io.ReadCloser suitable for passing as the body argument to Post/Put (whose signatures require io.ReadCloser).

func (*Client) LogValue

func (c *Client) LogValue() slog.Value

LogValue implements slog.LogValuer so logging a Client (as the "client" attribute used throughout this package) emits only its root URL and default headers rather than the full struct, avoiding the underlying transport, debug file handle, and mutex state. Note this does NOT redact header values: if an Option such as WithBearerToken or WithBasicAuth set a credential into the default headers, that credential is emitted as-is here, so callers should not log this value in contexts where an Authorization header would be sensitive.

func (*Client) Post

func (c *Client) Post(
	uri string,
	query url.Values,
	body io.ReadCloser,
	opts ...RequestOption,
) (*http.Response, error)

Post issues a POST request to uri with query as the URL's query string and body as the request body (see JSONReader for building one from a Go value), after applying opts and the client's default headers.

func (*Client) Put

func (c *Client) Put(
	uri string,
	query url.Values,
	body io.ReadCloser,
	opts ...RequestOption,
) (*http.Response, error)

Put issues a PUT request to uri with query as the URL's query string and body as the request body, after applying opts and the client's default headers.

func (*Client) RoundTrip

func (c *Client) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper by delegating to Do, letting a Client itself be used as another http.Client's Transport.

func (*Client) Transport added in v0.18.3

func (c *Client) Transport() http.RoundTripper

Transport returns the client's underlying http.RoundTripper. It exists so callers that must build their own *http.Client sharing this client's TLS trust/transport configuration (proxy, mTLS, custom dialer, pinned CA) can do so without duplicating it -- e.g. wrapping it in a *http.Client that carries a policy (redirect refusal, a finite Timeout) this Client itself does not expose a way to set. SEC-0057 (GitLab #311).

type ClientWrapper

type ClientWrapper func(*http.Request) (*http.Response, error)

ClientWrapper adapts a plain request-handling function into both http.RoundTripper (via RoundTrip) and an ad hoc "Do"-style caller (via Do), letting a *http.Client's Do method (e.g. from clientcredentials.Config.Client, or any HTTPClient) be installed as a Client's transport.

func (ClientWrapper) Do

func (t ClientWrapper) Do(req *http.Request) (*http.Response, error)

Do calls the wrapped function with req, satisfying an HTTPClient-style Do(req) signature.

func (ClientWrapper) RoundTrip

func (t ClientWrapper) RoundTrip(r *http.Request) (*http.Response, error)

RoundTrip calls the wrapped function with r, satisfying http.RoundTripper so a ClientWrapper can be assigned directly to Client.transport.

type CompressionConfig

type CompressionConfig struct {
	// Level is the compression level (1-9 for gzip)
	Level int
	// MinSize is the minimum response size to compress (bytes)
	MinSize int
	// Types are the content types to compress
	Types []string
}

CompressionConfig configures CompressionMiddleware's gzip response compression.

type FIDOConfig

type FIDOConfig struct {
	// RPDisplayName is the relying party display name (e.g., "My App")
	RPDisplayName string
	// RPID is the relying party ID (e.g., "example.com")
	RPID string
	// RPOrigins are the allowed origins for WebAuthn requests
	RPOrigins []string
	// Timeout is the timeout for WebAuthn ceremonies (default: 60s)
	Timeout time.Duration
	// Debug enables debug logging
	Debug bool
	// Logger for WebAuthn operations
	Logger *slog.Logger
	// Store is the credential store
	Store FIDOStore
}

FIDOConfig configures WebAuthn/FIDO2 authentication

type FIDOCredential

type FIDOCredential struct {
	ID              []byte                            `json:"id"`
	PublicKey       []byte                            `json:"public_key"`
	AttestationType string                            `json:"attestation_type"`
	Transport       []protocol.AuthenticatorTransport `json:"transport"`
	Flags           webauthn.CredentialFlags          `json:"flags"`
	Authenticator   webauthn.Authenticator            `json:"authenticator"`
	BackupEligible  bool                              `json:"backup_eligible"`
	BackupState     bool                              `json:"backup_state"`
	Created         time.Time                         `json:"created"`
	LastUsed        time.Time                         `json:"last_used"`
}

FIDOCredential represents a WebAuthn credential

func FromWebAuthn

func FromWebAuthn(cred *webauthn.Credential) *FIDOCredential

FromWebAuthn creates a FIDOCredential from webauthn.Credential

func (*FIDOCredential) ToWebAuthn

func (c *FIDOCredential) ToWebAuthn() webauthn.Credential

ToWebAuthn converts FIDOCredential to webauthn.Credential

type FIDOServer

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

FIDOServer provides WebAuthn/FIDO2 authentication handlers

func NewFIDOServer

func NewFIDOServer(cfg *FIDOConfig) (*FIDOServer, error)

NewFIDOServer creates a new FIDO server

func (*FIDOServer) BeginLogin

func (s *FIDOServer) BeginLogin(c *gin.Context)

BeginLogin starts the FIDO2 authentication ceremony

func (*FIDOServer) BeginRegistration

func (s *FIDOServer) BeginRegistration(c *gin.Context)

BeginRegistration starts the FIDO2 registration ceremony

func (*FIDOServer) FinishLogin

func (s *FIDOServer) FinishLogin(c *gin.Context)

FinishLogin completes the FIDO2 authentication ceremony

func (*FIDOServer) FinishRegistration

func (s *FIDOServer) FinishRegistration(c *gin.Context)

FinishRegistration completes the FIDO2 registration ceremony

func (*FIDOServer) RegisterRoutes

func (s *FIDOServer) RegisterRoutes(r gin.IRouter)

RegisterRoutes registers FIDO2 routes on a Gin router

type FIDOStore

type FIDOStore interface {
	// GetUser retrieves a user by ID
	GetUser(userID []byte) (*FIDOUser, error)
	// GetUserByName retrieves a user by username
	GetUserByName(username string) (*FIDOUser, error)
	// SaveUser saves or updates a user
	SaveUser(user *FIDOUser) error
	// SaveCredential saves a credential for a user
	SaveCredential(userID []byte, credential *FIDOCredential) error
	// GetCredentials retrieves all credentials for a user
	GetCredentials(userID []byte) ([]*FIDOCredential, error)
	// UpdateCredential updates a credential (e.g., sign count)
	UpdateCredential(credentialID []byte, credential *FIDOCredential) error
}

FIDOStore defines the interface for storing and retrieving WebAuthn credentials

type FIDOUser

type FIDOUser struct {
	ID          []byte `json:"id"`
	Name        string `json:"name"`
	DisplayName string `json:"display_name"`
	Credentials []*FIDOCredential
}

FIDOUser represents a WebAuthn user

func GetFIDOUser

func GetFIDOUser(c *gin.Context) (*FIDOUser, bool)

GetFIDOUser retrieves the authenticated FIDO user from the context

func (*FIDOUser) MarshalJSON

func (u *FIDOUser) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for FIDOUser

func (*FIDOUser) UnmarshalJSON

func (u *FIDOUser) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for FIDOUser

func (*FIDOUser) WebAuthnCredentials

func (u *FIDOUser) WebAuthnCredentials() []webauthn.Credential

WebAuthnCredentials returns the user's credentials

func (*FIDOUser) WebAuthnDisplayName

func (u *FIDOUser) WebAuthnDisplayName() string

WebAuthnDisplayName returns the display name for WebAuthn

func (*FIDOUser) WebAuthnID

func (u *FIDOUser) WebAuthnID() []byte

WebAuthnID returns the user ID for WebAuthn

func (*FIDOUser) WebAuthnIcon

func (u *FIDOUser) WebAuthnIcon() string

WebAuthnIcon returns the user icon URL (deprecated in WebAuthn spec)

func (*FIDOUser) WebAuthnName

func (u *FIDOUser) WebAuthnName() string

WebAuthnName returns the username for WebAuthn

type HTTPClient

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPClient is the minimal Do(req) contract WithDoer accepts, satisfied by *http.Client and any compatible wrapper.

type HTTPExchange

type HTTPExchange struct {
	Request  *ReplayRequest `json:"request"`
	Response *ReplayResult  `json:"response"`
}

HTTPExchange represents an HTTP request/response pair

type Host

type Host string

Host is a "host" or "host:port" value, used by Rewriter to describe the source and destination endpoints of a rewrite rule.

type ID

type ID string

ID is a string identifier type reserved for typed server/resource identifiers; it is not currently used internally by this package.

type KeyStore

type KeyStore interface {
	// GetPublicKey retrieves a public key by key ID
	GetPublicKey(keyID string) (any, error)
}

KeyStore provides public keys for signature verification

type MemoryAPIKeyStore

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

MemoryAPIKeyStore is an in-memory implementation of APIKeyStore

func NewMemoryAPIKeyStore

func NewMemoryAPIKeyStore() *MemoryAPIKeyStore

NewMemoryAPIKeyStore creates a new in-memory API key store

func (*MemoryAPIKeyStore) Delete

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

Delete removes the key with the given ID from all three indexes (by ID, hash, and prefix). Returns ErrAPIKeyNotFound if no such key is stored.

func (*MemoryAPIKeyStore) GetByHash

func (s *MemoryAPIKeyStore) GetByHash(hash string) (*APIKey, error)

GetByHash looks up a key by its stored SHA-256 hash (never the raw key material, which is never persisted). Returns ErrAPIKeyNotFound if no key has that hash.

func (*MemoryAPIKeyStore) GetByID

func (s *MemoryAPIKeyStore) GetByID(id string) (*APIKey, error)

GetByID looks up a key by its generated ID. Returns ErrAPIKeyNotFound if no key with that ID is stored.

func (*MemoryAPIKeyStore) GetByPrefix

func (s *MemoryAPIKeyStore) GetByPrefix(prefix string) (*APIKey, error)

GetByPrefix looks up a key by its short (first-8-character) display prefix, the non-secret identifier shown to users alongside a masked key. Returns ErrAPIKeyNotFound if no key with that prefix is stored.

func (*MemoryAPIKeyStore) List

func (s *MemoryAPIKeyStore) List() ([]*APIKey, error)

List returns every stored key in unspecified order. Never returns an error; the signature exists to satisfy APIKeyStore for backing stores whose listing can fail.

func (*MemoryAPIKeyStore) Save

func (s *MemoryAPIKeyStore) Save(key *APIKey) error

Save inserts or overwrites key, indexing it by ID, hash, and prefix so it is reachable through any of GetByID/GetByHash/GetByPrefix. Never returns an error; the signature exists to satisfy APIKeyStore for backing stores that can fail (e.g. a database-backed implementation).

func (*MemoryAPIKeyStore) UpdateLastUsed

func (s *MemoryAPIKeyStore) UpdateLastUsed(id string, t time.Time) error

UpdateLastUsed records t as the key's most recent-use timestamp in place. Returns ErrAPIKeyNotFound if no key with that ID is stored.

type MemoryCacheStore

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

MemoryCacheStore is an in-memory cache store

func NewMemoryCacheStore

func NewMemoryCacheStore() *MemoryCacheStore

NewMemoryCacheStore creates a new in-memory cache store

func (*MemoryCacheStore) Clear

func (s *MemoryCacheStore) Clear() error

Clear discards every cached entry by replacing the underlying map. Never returns an error; the signature exists to satisfy CacheStore for backing stores that can fail.

func (*MemoryCacheStore) Delete

func (s *MemoryCacheStore) Delete(key string) error

Delete removes the cached entry for key, if any. It is a no-op (not an error) when key is not present.

func (*MemoryCacheStore) Get

func (s *MemoryCacheStore) Get(key string) (*CacheEntry, error)

Get looks up the cached entry for key. Returns ErrCacheMiss if no entry is stored for key, or ErrCacheExpired if an entry exists but its TTL has elapsed (the expired entry is left in the map, not evicted, until overwritten by Set).

func (*MemoryCacheStore) Set

func (s *MemoryCacheStore) Set(key string, entry *CacheEntry) error

Set stores entry under key, overwriting any existing entry. Never returns an error; the signature exists to satisfy CacheStore for backing stores that can fail.

type MemoryFIDOStore

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

MemoryFIDOStore is an in-memory implementation of FIDOStore for development/testing

func NewMemoryFIDOStore

func NewMemoryFIDOStore() *MemoryFIDOStore

NewMemoryFIDOStore creates a new in-memory FIDO store

func (*MemoryFIDOStore) GetCredentials

func (s *MemoryFIDOStore) GetCredentials(userID []byte) ([]*FIDOCredential, error)

GetCredentials returns the credentials registered for the user with the given userID, resolved by intersecting the stored credential map against the user's own in-memory Credentials slice (a real implementation would instead maintain a persisted user->credential index). Returns ErrFIDOUserNotFound if no user with that ID is stored.

func (*MemoryFIDOStore) GetUser

func (s *MemoryFIDOStore) GetUser(userID []byte) (*FIDOUser, error)

GetUser looks up a user by their raw WebAuthn user handle (userID) and populates the returned FIDOUser's Credentials by scanning the credential map for entries whose credential ID base64-encodes to the same key as the user handle (a placeholder linkage — see the comment in GetCredentials for the real per-user mapping this store does not implement). Returns ErrFIDOUserNotFound if no user has that ID.

func (*MemoryFIDOStore) GetUserByName

func (s *MemoryFIDOStore) GetUserByName(username string) (*FIDOUser, error)

GetUserByName looks up a user by their WebAuthn username. Unlike GetUser, it does not populate Credentials from the credential map. Returns ErrFIDOUserNotFound if no user with that name is stored.

func (*MemoryFIDOStore) SaveCredential

func (s *MemoryFIDOStore) SaveCredential(userID []byte, credential *FIDOCredential) error

SaveCredential stores credential, keyed by its own base64-encoded credential ID. The userID parameter is currently unused by this in-memory implementation (see GetCredentials for how it links credentials back to a user via user.Credentials instead). Never returns an error; the signature exists to satisfy FIDOStore for backing stores that can fail. The credential's public key is stored as-is and must be treated as sensitive key material by callers.

func (*MemoryFIDOStore) SaveUser

func (s *MemoryFIDOStore) SaveUser(user *FIDOUser) error

SaveUser inserts or overwrites user, indexing it both by username and by base64-encoded user ID so it is reachable through either GetUserByName or GetUser. Never returns an error; the signature exists to satisfy FIDOStore for backing stores that can fail.

func (*MemoryFIDOStore) UpdateCredential

func (s *MemoryFIDOStore) UpdateCredential(credentialID []byte, credential *FIDOCredential) error

UpdateCredential overwrites the stored credential keyed by credentialID's base64 encoding with credential — used after a successful authentication ceremony to persist the authenticator's updated sign count and last-used time. Never returns an error; the signature exists to satisfy FIDOStore for backing stores that can fail.

type MemoryKeyStore

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

MemoryKeyStore is an in-memory key store

func NewMemoryKeyStore

func NewMemoryKeyStore() *MemoryKeyStore

NewMemoryKeyStore creates a new in-memory key store

func (*MemoryKeyStore) AddKey

func (s *MemoryKeyStore) AddKey(keyID string, key any)

AddKey adds a key to the store

func (*MemoryKeyStore) GetPublicKey

func (s *MemoryKeyStore) GetPublicKey(keyID string) (any, error)

GetPublicKey retrieves a public key by ID

type MockEndpoint

type MockEndpoint struct {
	Method     string            `json:"method"`
	Path       string            `json:"path"`
	StatusCode int               `json:"status_code"`
	Response   any               `json:"response"`
	Headers    map[string]string `json:"headers,omitempty"`
	Delay      time.Duration     `json:"delay,omitempty"`
	MatchQuery map[string]string `json:"match_query,omitempty"`
	MatchBody  map[string]any    `json:"match_body,omitempty"`
}

MockEndpoint represents a mock API endpoint

func LoadMockEndpointsFromJSON

func LoadMockEndpointsFromJSON(data []byte) ([]*MockEndpoint, error)

LoadMockEndpointsFromJSON loads mock endpoints from JSON

type MockServer

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

MockServer provides a configurable mock HTTP server

func NewMockServer

func NewMockServer(logger *slog.Logger) *MockServer

NewMockServer creates a new mock server

func (*MockServer) AddEndpoint

func (ms *MockServer) AddEndpoint(endpoint *MockEndpoint)

AddEndpoint adds a mock endpoint

func (*MockServer) AddEndpoints

func (ms *MockServer) AddEndpoints(endpoints []*MockEndpoint)

AddEndpoints adds multiple endpoints

func (*MockServer) Handler

func (ms *MockServer) Handler() http.Handler

Handler returns the HTTP handler

type OIDC

type OIDC struct {
	Issuer string `json:"issuer" validate:"required,url,max=255"`

	AuthZURL *url.URL
	TokenURL *url.URL
	UserURL  *url.URL

	LogoutURL      *url.URL
	IntrospectURL  *url.URL
	RevocationURL  *url.URL
	DeviceAuthzURL *url.URL

	JWKsURL *url.URL
	// JWKs holds the cold-start key set so legacy callers continue to
	// observe a non-nil reference. The authoritative, auto-refreshing
	// view lives behind jwkCache and is consulted on every JWT
	// validation path so IdP key rotation is picked up without a host
	// process restart (PERF-0043, SEC #150).
	JWKs jwk.Set

	Aud string

	ResponseTypes [][]string
	ResponseModes []string `json:"response_modes_supported"`
	GrantTypes    []string `json:"grant_types_supported"`

	SigningAlgos         []string `json:"id_token_signing_alg_values_supported"`
	SubjectTypes         []string `json:"subject_types_supported"`
	AuthMethods          []string `json:"token_endpoint_auth_methods_supported"`
	AcrValues            []string `json:"acr_values_supported"`
	Scopes               []string `json:"scopes_supported"`
	Claims               []string `json:"claims_supported"`
	CodeChallengeMethods []string `json:"code_challenge_methods_supported"`

	ClaimsParameters bool `json:"claims_parameter_supported"`
	RequestParameter bool `json:"request_parameter_supported"`
	// contains filtered or unexported fields
}

OIDC holds an OpenID Connect provider's discovery-document configuration (endpoints, supported algorithms/scopes/claims, and the JWKS used to verify tokens) as loaded by LoadOIDC. Its exported endpoint/metadata fields are populated by UnmarshalJSON directly from the discovery document's JSON, while the unexported ctx/client/discoveryURL fields carry the request context, HTTP client, and originating discovery URL needed for issuer validation and JWKS auto-refresh. Use OIDC.JWT as Gin middleware to verify bearer tokens, or OIDC.Authenticate as an openapi3filter.AuthenticationFunc.

func LoadOIDC

func LoadOIDC(
	ctx context.Context,
	client *Client,
	oidcURL string,
	aud string,
) (*OIDC, error)

LoadOIDC loads the OpenID Connect configuration from the given URL. @client: HTTP client to use for the request. @url: URL of the OpenID Connect configuration endpoint. @aud: Audience of the OpenID Connect configuration endpoint (usually the client ID of the application). @return: OpenID Connect configuration.

func (*OIDC) Authenticate

func (o *OIDC) Authenticate(
	ctx context.Context, input *oapifilter.AuthenticationInput,
) error

Authenticate is the oapi-codegen AuthenticationFunc for this OIDC instance: it requires a verified JWT on the request and enforces every OAuth2 scope the OpenAPI security scheme declared for the operation. The token itself is verified upstream by the JWT extractor middleware; this function only reads the verified token off the context and checks scopes.

func (*OIDC) ExtractToken

func (o *OIDC) ExtractToken(ctx *gin.Context) (jwt.Token, error)

ExtractToken parses the Authorization HTTP header for valid JWT token and validates it with the JWK keys. Also verifies if the audience present in the token matches with the designated audience as per current configuration.

func (*OIDC) JWT

func (o *OIDC) JWT(ctx *gin.Context)

JWT is Gin middleware that extracts and verifies a bearer JWT from the incoming request via ExtractToken and, on success, stashes the verified token both in Gin's per-request key/value store (under ctxTokenKey as a string) and on the request's context.Context (under the typed ctxTokenKey, readable later via AuthToken). If o.JWKs is nil (no keys configured), the request is allowed through unauthenticated UNLESS securex.AllowInsecureDev refuses it (e.g. not running in Gin debug mode), in which case it aborts with 401. A missing Authorization header is treated as anonymous (request continues unauthenticated); any other extraction/verification failure aborts the request with 401.

func (*OIDC) UnmarshalJSON

func (o *OIDC) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for OIDC, decoding an OpenID Connect discovery document (RFC/OIDC Discovery 1.0) into o. Beyond plain field mapping it enforces several security invariants fail-closed: required endpoint fields must be present and well-formed (validated via the package-level validator), every derived endpoint URL (jwks_uri, authorization_endpoint, token_endpoint, userinfo_endpoint) must use https (ErrInvalidScheme otherwise), and — when o.discoveryURL was set by LoadOIDC — the document's advertised issuer must match the discovery URL per OIDC Discovery 1.0 §4.3 (ErrIssuerMismatch otherwise, guarding against a compromised/misconfigured discovery endpoint redirecting trust). On success it also builds an auto-refreshing JWK cache (oidcx.NewJWKCache) bound to o.ctx, sharing o.client's transport when present, and fails with ErrJWKEndpointEmpty if the fetched key set is empty.

type Option

type Option func(*Client) error

Option configures a Client during NewClient, mutating it in place and returning an error to abort construction (e.g. WithTransport rejecting a nil transport). Options are applied in the order passed.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey adds an API key to the client

func WithAutoRefreshToken

func WithAutoRefreshToken(refresher *TokenRefresher) Option

WithAutoRefreshToken creates a client with automatic token refresh

func WithBasicAuth

func WithBasicAuth(username, password string) Option

WithBasicAuth sets a default "Authorization: Basic <base64(user:pass)>" header on every request the client sends (RFC 7617). username and password are base64-encoded, not encrypted; treat them as secrets and only use this option over a trusted transport (e.g. TLS).

func WithBearerToken

func WithBearerToken(token string) Option

WithBearerToken sets a default "Authorization: Bearer <token>" header on every request the client sends. token is bearer-credential material; treat it as a secret (note Client.LogValue does not redact it if it ends up in the default headers) and only use this option over a trusted transport.

func WithCircuitBreaker

func WithCircuitBreaker(cb *CircuitBreaker) Option

WithCircuitBreaker wraps the client's current transport (or http.DefaultTransport if none is set) with cb, so every request goes through cb.Execute and is rejected with ErrCircuitBreakerOpen while the breaker is open, and any 5xx response is recorded as a failure.

func WithDebugFile

func WithDebugFile(file string) Option

WithDebugFile opens (creating/appending as needed) file and sets it as the client's debug dump target: every response body, up to the client's response-byte ceiling, is written to it (see Client.debugWrite). The file is opened owner-only (mode 0600) and with O_NOFOLLOW so a pre-planted symlink at the final path component cannot redirect captured request/response bodies (which may contain sensitive data) into a file an attacker controls (SEC-0013).

func WithDoer

func WithDoer(client HTTPClient) Option

WithDoer installs client's Do method as the Client's transport (via ClientWrapper), letting callers reuse an existing HTTPClient (such as a pre-configured *http.Client) instead of a bare http.RoundTripper. Returns ErrClientNil if client is nil.

func WithHeader

func WithHeader(key, value string) Option

WithHeader sets key to value in the client's default headers, applied to every outgoing request (before any per-request RequestOption, so a RequestOption can still override it). Repeated calls with the same key replace the previous value (http.Header.Set semantics).

func WithLimiter

func WithLimiter(
	ctx context.Context,
	delay time.Duration,
	retries int,
	concurrency int,
	requestTimeout time.Duration,
) Option

WithLimiter wraps the client's current transport with a devnw.dev/bk rate-limited/retrying client (bk.New), bound to ctx: delay is the pacing interval between requests, retries the number of retry attempts, concurrency the maximum number of in-flight requests, and requestTimeout the per-request deadline. The bk client's Do method replaces the client's transport.

func WithLogger

func WithLogger(log log.Logger) Option

WithLogger sets the client's log.Logger, used for request/response tracing and debug output. A nil logger is silently ignored, leaving the client's existing (default) logger in place.

func WithMaxResponseBytes

func WithMaxResponseBytes(n int64) Option

WithMaxResponseBytes sets the ceiling on response-body bytes buffered by ReadJSON. A value <=0 disables the limit (unbounded; use only for trusted upstreams). The default is DefaultMaxResponseBytes (32 MiB) (PERF-0024).

func WithOAuth2

func WithOAuth2(
	clientID, clientSecret, tokenURL string,
	scopes ...string,

) Option

WithOAuth2 configures the client to authenticate outgoing requests using the OAuth2 client-credentials grant (golang.org/x/oauth2/clientcredentials): clientID/clientSecret are exchanged with tokenURL for an access token, scoped to scopes, and the resulting token-managing *http.Client (which transparently refreshes the token as needed) replaces the client's transport entirely. clientSecret is credential material handled by the oauth2 library and is never logged by this option itself.

func WithRetry

func WithRetry(cfg *RetryConfig) Option

WithRetry adds retry logic to the client

func WithRoot

func WithRoot(rootURL string) Option

WithRoot sets the client's root URL, parsed from rootURL, against which relative request paths (e.g. those passed to Client.Get/Post/Put/Delete) are resolved. Returns a parse error if rootURL is not a valid URL.

func WithSignature

func WithSignature(signer *Signer) Option

WithSignature adds HTTP signature signing to the client

func WithTransport

func WithTransport(transport http.RoundTripper) Option

WithTransport sets the client's underlying http.RoundTripper, replacing http.DefaultTransport (e.g. to install a custom TLS config, proxy, or pinned CA). Returns ErrTransportNil if transport is nil.

type ProxyOption

type ProxyOption func(*proxy) error

ProxyOption configures a proxy during Proxy, mutating it in place and returning an error to abort construction.

func WithCertificate

func WithCertificate(cert, key string) ProxyOption

WithCertificate configures the proxy's TLS certificate loading (via loadCert) from the given cert/key file paths, used as GetCertificate on every handshake so certificate rotation (e.g. a symlink retarget by certbot or a Kubernetes secret volume) is picked up without a restart. If resolution or loading fails, a fresh self-signed certificate is served instead (fail-open) rather than failing the handshake.

func WithClient

func WithClient(c *Client) ProxyOption

WithClient sets the *Client the proxy uses to forward requests upstream, replacing the default client Proxy constructs internally (which carries no special options).

func WithRewriters

func WithRewriters(r ...Rewriter) ProxyOption

WithRewriters appends r to the proxy's list of Rewriters. Proxy collapses the full accumulated list into a single "rewriters" Rule that, for each request, tries each Rewriter's Process method in order and stops at the first one that succeeds (a failing Rewriter is skipped, not fatal).

func WithRule

func WithRule(name string, rule Rule) ProxyOption

WithRule appends rule to the named list of rules run (in registration order) before every proxied request. The name is used only for logging; multiple WithRule calls with the same name accumulate rather than replace.

func WithSubnetFilter

func WithSubnetFilter(subnets ...netip.Prefix) ProxyOption

WithSubnetFilter registers a "subnet-filter" rule that only allows a proxied request through when the client's remote address (parsed from r.RemoteAddr) falls within one of the given subnets; otherwise it returns a "forbidden" error, which the proxy's serve loop turns into a 500 response and aborts forwarding. A remote address that fails to parse as host:port or as an IP writes its own 500 response directly and also returns an error.

func WithTLSConfig

func WithTLSConfig(cfg *tls.Config) ProxyOption

WithTLSConfig sets the *tls.Config the proxy uses when Proxy wraps the listener in a TLS listener. Its GetCertificate field is overwritten by Proxy with the certgen configured via WithCertificate, so callers do not need to (and should not rely on) setting GetCertificate here themselves.

type Register

type Register func(*gin.Engine) error

Register is the callback New invokes with the constructed *gin.Engine so callers can mount their application's routes before the listener starts. A non-nil error aborts server construction.

type ReplayOptions added in v0.18.3

type ReplayOptions struct {
	// MaxParallel bounds the number of concurrent in-flight Replay calls.
	// <=0 uses the default (runtime.GOMAXPROCS(0)*2). Before this bound
	// existed, a parallel ReplayBatch launched one goroutine and one
	// outbound HTTP call per element with no cap, so a large batch could
	// open unbounded concurrent connections to the replay target -- the
	// same semaphore-bounded dispatch pattern pkg/mcpx's dispatchHTTP and
	// pkg/gqlx's batch handler already use.
	MaxParallel int
}

ReplayOptions configures ReplayBatchWithOptions's parallel fan-out (PERF-0122).

type ReplayRequest

type ReplayRequest struct {
	Method  string            `json:"method"`
	URL     string            `json:"url"`
	Headers map[string]string `json:"headers"`
	Body    []byte            `json:"body,omitempty"`
}

ReplayRequest represents a captured HTTP request for replay

func LoadRequestsFromJSON

func LoadRequestsFromJSON(data []byte) ([]*ReplayRequest, error)

LoadRequestsFromJSON loads replay requests from JSON

type ReplayResult

type ReplayResult struct {
	Request    *ReplayRequest
	StatusCode int
	Headers    http.Header
	Body       []byte
	Duration   time.Duration
	Error      error
	Timestamp  time.Time
}

ReplayResult contains the result of a replayed request

type Replayer

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

Replayer replays captured HTTP requests

func NewReplayer

func NewReplayer(client *http.Client, baseURL string, logger *slog.Logger) *Replayer

NewReplayer creates a new request replayer

func (*Replayer) Replay

func (r *Replayer) Replay(ctx context.Context, req *ReplayRequest) *ReplayResult

Replay replays a single request

func (*Replayer) ReplayBatch

func (r *Replayer) ReplayBatch(ctx context.Context, requests []*ReplayRequest, parallel bool) []*ReplayResult

ReplayBatch replays multiple requests, sequentially or in parallel. The parallel path is bounded by the default MaxParallel (runtime.GOMAXPROCS(0)*2); use ReplayBatchWithOptions to configure the bound explicitly.

func (*Replayer) ReplayBatchWithOptions added in v0.18.3

func (r *Replayer) ReplayBatchWithOptions(ctx context.Context, requests []*ReplayRequest, opts ReplayOptions) []*ReplayResult

ReplayBatchWithOptions replays multiple requests in parallel, bounding the number of concurrently in-flight Replay calls to opts.MaxParallel (or its default). Results are written to results[i] by the goroutine handling requests[i], so the returned slice stays ordered to match the input regardless of completion order.

type RequestOption

type RequestOption func(*Client, *http.Request) error

RequestOption customizes a single outgoing *http.Request, applied after the client's default headers so a RequestOption can override them.

func WithBody

func WithBody(body io.ReadCloser) RequestOption

WithBody sets the request's Body to body (e.g. built via Client.JSONReader). It does not set Content-Length or Content-Type; callers needing those should also apply WithRequestHeader.

func WithMethod

func WithMethod(method string) RequestOption

WithMethod sets the request's HTTP method. The package-level GET, POST, PUT, DELETE, and PATCH vars are pre-built RequestOptions from this function for the common verbs.

func WithQuery

func WithQuery(query url.Values) RequestOption

WithQuery sets the request's URL query string from query, after passing it through the client's queryNorm normalization.

func WithRequestAuth

func WithRequestAuth(auth string) RequestOption

WithRequestAuth sets the Authorization header on this request only to the literal value auth (e.g. "Bearer <token>" or "Basic <base64>"). Treat auth as a credential: it is placed directly on the outgoing request unmodified.

func WithRequestHeader

func WithRequestHeader(key, value string) RequestOption

WithRequestHeader sets key to value on this request only (via http.Header.Set, replacing any existing value for key), overriding the client's default headers for this call.

type RetryConfig

type RetryConfig struct {
	// MaxAttempts is the maximum number of retry attempts
	MaxAttempts int
	// InitialDelay is the delay before the first retry
	InitialDelay time.Duration
	// MaxDelay is the maximum delay between retries
	MaxDelay time.Duration
	// Multiplier is the backoff multiplier (default: 2.0)
	Multiplier float64
	// ShouldRetry determines if a request should be retried
	ShouldRetry func(*http.Response, error) bool
}

RetryConfig configures retry behavior

type Rewriter

type Rewriter struct {
	From Host
	To   Host

	Paths   map[string]string
	Headers map[string]http.Header
	Query   map[string]url.Values
}

Rewriter describes a single host-scoped request rewrite rule: requests whose host (and, if present, port) match From are rewritten to target To, with per-original-path substitutions for the request path (Paths), and per-target-path additions merged into the query string (Query) and headers (Headers). A Rewriter is a value type; Process applies it to a request without mutating the Rewriter itself.

func (Rewriter) Process

func (r Rewriter) Process(in *http.Request) error

Process rewrites in in place according to r: it first checks that in's host (and port, if r.From specifies one) matches r.From, returning ErrNotApplicable if not. On a match it rewrites in.Host/in.URL.Host to r.To (keeping the original port when r.To specifies none), remaps in.URL.Path via r.Paths (falling back to the original path if unmapped), merges any r.Query entries for the resulting path into the existing query string, and merges any r.Headers entries for that path into in.Header. Query and header merges are applied in sorted key order for determinism; existing keys are overwritten with the configured values.

type Rule

type Rule func(w http.ResponseWriter, r *http.Request) error

Rule is a proxy request hook invoked before the request is forwarded upstream: it may inspect/mutate r, write directly to w (e.g. http.Error) to short-circuit the request, and return a non-nil error to abort forwarding (the proxy's handler responds 500 and stops). Rules are registered by name via WithRule (or synthesized, as the "rewriters" rule installed by WithRewriters) and run in the order added.

type Server

type Server interface {
	Serve(context.Context) error
}

Server is the interface returned by New: a running (or ready-to-run) htpx API server whose lifecycle is driven entirely through Serve.

func New

func New(reg Register, opts ...ServerOption) (_ Server, err error)

New builds a Server: it installs the default request-logging/recovery middleware, applies opts in order (host/port/TLS/CORS/validation/etc.), fills in default rate-limit settings and builds the token-bucket rateLimiter, appends the rate-limit/metrics middleware, sets trusted proxies (if configured), mounts a GET /metrics route, invokes reg to register application routes, and finally opens the listener (TLS unless the insecure-dev bypass permits a plaintext loopback listener; see INSECURE_DEV). It returns ErrRegisterFunctionNil if reg is nil, and wraps any trusted-proxy, route-registration, or listener-construction error.

type ServerOption

type ServerOption func(*apiServer) error

ServerOption is a functional option applied to an apiServer during New to configure host/port/TLS/CORS/middleware/validation/etc. It returns an error to abort server construction.

func RateLimit

func RateLimit(limit int, duration time.Duration) ServerOption

RateLimit sets the token-bucket rate limit (limit requests per duration) used to build the server's shared rateLimiter. It returns an error (without modifying the server) if limit falls outside [rateLimitMin, rateLimitMax] or duration falls outside [rateMinDuration, rateMaxDuration].

func WithAPIKeyManager

func WithAPIKeyManager(manager *APIKeyManager, adminAuth gin.HandlerFunc) ServerOption

WithAPIKeyManager adds API key management routes to the server

func WithCORS

func WithCORS(allowedOrigins []string, methods ...string) ServerOption

WithCORS configures gin-contrib/cors on the server. Empty allowedOrigins defaults to the server's own default host:port; an empty methods list defaults to GET/POST/PUT/DELETE. When the insecure-dev bypass is active at startup (see INSECURE_DEV), AllowAllOrigins is forced to true regardless of allowedOrigins -- a dev-only convenience that must never be reachable in a production deployment.

func WithErrorHandler

func WithErrorHandler(handler func(ctx *gin.Context, err error, i int)) ServerOption

WithErrorHandler sets the server's error handler, used to translate an error and status code into an HTTP response. Passing a nil handler logs a warning and installs a default handler that writes {"error": err.Error()} as JSON with the given status (and does nothing when err is nil).

func WithFIDO

func WithFIDO(cfg *FIDOConfig) ServerOption

WithFIDO adds FIDO2 authentication to an API server

func WithHost

func WithHost(host string) ServerOption

WithHost sets the host/interface the server listens on.

func WithImpl

func WithImpl(impl any) ServerOption

WithImpl logs the supplied impl value for diagnostics only; it is currently a placeholder and does not store or wire impl into the server in any other way.

func WithMaxBodyBytes added in v0.18.3

func WithMaxBodyBytes(n int64) ServerOption

WithMaxBodyBytes overrides the request-body size limit (mirrors api.WithMaxBodyBytes; PERF-0036/QG-095 #271). The default is defaultMaxBodyBytes (10 MiB); pass a larger value for routes that accept big uploads, or 0 to disable bounding entirely (e.g. fully streaming endpoints). The limit is enforced by http.MaxBytesReader before any handler or the OpenAPI request validator reads the body.

func WithMiddleware

func WithMiddleware(middleware ...gin.HandlerFunc) ServerOption

WithMiddleware appends the given gin.HandlerFuncs to the server's middleware chain, in the order supplied, after the built-in request logging/recovery middleware and before the rate-limit middleware New adds.

func WithPerIPRateLimit

func WithPerIPRateLimit() ServerOption

WithPerIPRateLimit enables per-remote-IP token buckets instead of global bucket.

func WithPort

func WithPort(port int) ServerOption

WithPort sets the TCP port the server listens on.

func WithServerLogger

func WithServerLogger(logger log.Logger) ServerOption

WithServerLogger overrides the server's log.Logger (used for both the server's own diagnostic logging and as the default passed to WithValidation's discovery client).

func WithShutdownTimeout added in v0.18.3

func WithShutdownTimeout(d time.Duration) ServerOption

WithShutdownTimeout overrides the graceful http.Server.Shutdown budget Serve applies once ctx is cancelled (default 30s; mirrors api.WithShutdownTimeout, QG-095/#271). A non-positive d is ignored (the default is kept).

func WithSwaggerSpec

func WithSwaggerSpec(spec *openapi3.T) ServerOption

WithSwaggerSpec attaches the OpenAPI document used for request validation middleware. It must be applied (via an earlier ServerOption) before WithValidation whenever OIDC authentication is configured, since WithValidation refuses (ErrValidationSpecMissing) to build a server with OIDC auth but no spec.

func WithTLS

func WithTLS(cert, key string) ServerOption

WithTLS sets the paths to the TLS certificate and key files newListener uses to build the server's TLS listener. These are ignored (and may be left empty) when the insecure-dev bypass is active at startup (see INSECURE_DEV); otherwise both are required.

func WithTrustedProxies

func WithTrustedProxies(proxies []string) ServerOption

WithTrustedProxies sets the trusted proxy addresses/CIDRs passed to gin.Engine.SetTrustedProxies during New. A nil proxies slice logs a warning and leaves the server's configured proxies untouched (New then skips calling SetTrustedProxies, so Gin's own default applies) rather than explicitly clearing it.

func WithValidation

func WithValidation(
	ctx context.Context,
	oidc string,
	audience string,
) ServerOption

WithValidation wires OpenAPI request validation and, when oidc is non-empty, OIDC JWT authentication onto the server: it builds a discovery client, loads the OIDC configuration via LoadOIDC (setting s.oidc), and prepends s.oidc.JWT to the middleware chain so tokens are extracted before the OpenAPI validator's AuthenticationFunc runs. If oidc is empty, OIDC auth is skipped entirely, but that is only permitted when the insecure-dev bypass is active at startup (securex.AllowInsecureDevAtStartup); otherwise it returns ErrOIDCEndpointEmpty. If OIDC is configured but WithSwaggerSpec was never applied, it returns ErrValidationSpecMissing rather than building a server whose only auth-invoking component (the OpenAPI validator) is missing -- which would otherwise serve every security-declared route unauthenticated. When a spec is present, its Servers list is cleared (so validation does not require the request's host to match a declared server) and the OpenAPI request-validator middleware is appended to the chain.

type SignatureAlgorithm

type SignatureAlgorithm string

SignatureAlgorithm represents the signature algorithm

const (
	// AlgoHMAC_SHA256 signs/verifies with HMAC-SHA256 using a shared secret
	// ([]byte) as both PrivateKey and PublicKey.
	AlgoHMAC_SHA256 SignatureAlgorithm = "hmac-sha256"
	// AlgoRSA_SHA256 signs with RSASSA-PKCS1-v1_5 over SHA-256 using an
	// *rsa.PrivateKey/*rsa.PublicKey pair.
	AlgoRSA_SHA256 SignatureAlgorithm = "rsa-sha256"
	// AlgoECDSA_SHA256 signs with ECDSA (ASN.1 signature) over SHA-256 using
	// an *ecdsa.PrivateKey/*ecdsa.PublicKey pair.
	AlgoECDSA_SHA256 SignatureAlgorithm = "ecdsa-sha256"
	// AlgoED25519 signs/verifies with Ed25519 using an
	// ed25519.PrivateKey/ed25519.PublicKey pair.
	AlgoED25519 SignatureAlgorithm = "ed25519"
)

type SignatureConfig

type SignatureConfig struct {
	// KeyID identifies the key used for signing
	KeyID string
	// Algorithm is the signature algorithm
	Algorithm SignatureAlgorithm
	// PrivateKey is the private key for signing (crypto.Signer or []byte for HMAC)
	PrivateKey any
	// PublicKey is the public key for verification (crypto.PublicKey or []byte for HMAC)
	PublicKey any
	// Headers are the headers to include in the signature (default: date, digest)
	Headers []string
	// IncludeDigest adds a Digest header with the request body hash
	IncludeDigest bool
	// MaxClockSkew is the maximum allowed clock skew for date validation (default: 5 minutes)
	MaxClockSkew time.Duration
}

SignatureConfig configures HTTP signature signing/verification

type SignatureInfo

type SignatureInfo struct {
	KeyID     string
	Algorithm SignatureAlgorithm
	Headers   []string
	Signature string
}

SignatureInfo contains parsed signature information

type SignatureStats

type SignatureStats struct {
	Total   int64
	Success int64
	Failed  int64
	Errors  map[string]int64
}

SignatureStats tracks signature verification statistics

func NewSignatureStats

func NewSignatureStats() *SignatureStats

NewSignatureStats creates a new stats tracker

func (*SignatureStats) Record

func (s *SignatureStats) Record(err error)

Record records a verification result

func (*SignatureStats) Summary

func (s *SignatureStats) Summary() map[string]any

Summary returns a summary of statistics

type Signer

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

Signer signs HTTP requests

func NewSigner

func NewSigner(cfg *SignatureConfig) (*Signer, error)

NewSigner creates a new HTTP signature signer

func (*Signer) SignRequest

func (s *Signer) SignRequest(req *http.Request) error

SignRequest signs an HTTP request

type TOKEN

type TOKEN string

TOKEN is the type of the context key used to stash a verified JWT token on a request context (see ctxTokenKey, OIDC.JWT, and AuthToken). It holds no data of its own; it exists only so the key's type is distinct from any plain string key.

type Timeout

type Timeout time.Duration

Timeout is a time.Duration wrapper used for the proxy's named default timeout constants, giving each a distinct, self-documenting type.

func (Timeout) D

func (t Timeout) D() time.Duration

D returns t as a plain time.Duration, for passing to APIs (http.Server, http.Transport, net.Dialer, ...) that expect one.

type TokenRefresher

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

TokenRefresher automatically refreshes OAuth2 tokens

func NewTokenRefresher

func NewTokenRefresher(config *oauth2.Config, token *oauth2.Token, logger *slog.Logger) *TokenRefresher

NewTokenRefresher creates a new token refresher

func (*TokenRefresher) Client

func (tr *TokenRefresher) Client(ctx context.Context) *http.Client

Client returns an HTTP client with automatic token refresh

func (*TokenRefresher) OnRefresh

func (tr *TokenRefresher) OnRefresh(fn func(*oauth2.Token))

OnRefresh sets a callback for when the token is refreshed

func (*TokenRefresher) Start

func (tr *TokenRefresher) Start(ctx context.Context) error

Start starts the automatic token refresh

func (*TokenRefresher) Stop

func (tr *TokenRefresher) Stop()

Stop stops the automatic refresh

func (*TokenRefresher) Token

func (tr *TokenRefresher) Token() *oauth2.Token

Token returns the current token

type TrafficDiff

type TrafficDiff struct {
	Type        string `json:"type"` // "method", "path", "header", "body", "status"
	Field       string `json:"field,omitempty"`
	Expected    any    `json:"expected"`
	Actual      any    `json:"actual"`
	Description string `json:"description"`
}

TrafficDiff represents a difference between two HTTP exchanges

type TrafficDiffer

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

TrafficDiffer compares HTTP traffic

func NewTrafficDiffer

func NewTrafficDiffer() *TrafficDiffer

NewTrafficDiffer creates a new traffic differ

func (*TrafficDiffer) Compare

func (td *TrafficDiffer) Compare(expected, actual *HTTPExchange) []*TrafficDiff

Compare compares two HTTP exchanges

func (*TrafficDiffer) IgnoreFields

func (td *TrafficDiffer) IgnoreFields(fields ...string)

IgnoreFields sets JSON fields to ignore in body comparisons

func (*TrafficDiffer) IgnoreHeaders

func (td *TrafficDiffer) IgnoreHeaders(headers ...string)

IgnoreHeaders sets headers to ignore in comparisons

type Verifier

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

Verifier verifies HTTP signatures

func NewVerifier

func NewVerifier(keyStore KeyStore, maxClockSkew time.Duration) *Verifier

NewVerifier creates a new HTTP signature verifier

func (*Verifier) VerifyRequest

func (v *Verifier) VerifyRequest(req *http.Request) error

VerifyRequest verifies an HTTP request signature

Directories

Path Synopsis
cmd
apimap command
internal
ui

Jump to

Keyboard shortcuts

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