statute

package module
v0.2.1-0...-47891d8 Latest Latest
Warning

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

Go to latest
Published: May 17, 2026 License: MIT Imports: 53 Imported by: 0

README

statute

Config-as-code reverse proxy in Go. The binary is the configuration.

ci Go Reference Go Report Card Release License

statute is a reverse proxy framework where your routing topology, TLS material, upstream pools, and middleware stack are expressed as Go values — type-checked, IDE-completed, and validated at startup. There is no runtime config file, no hot reload, no module loader. You write a main.go, you go build, you ship a single binary that boots, validates, and serves.

package main

import "statute.kjanat.dev"

func main() {
    statute.Main(statute.Config{
        Listeners: statute.Listeners{
            statute.HTTP(":80").RedirectTo("https"),
            statute.HTTPS(":443",
                statute.AutoTLS("example.com").Email("ops@example.com").Storage("/var/lib/statute/certs"),
                statute.HTTP2(),
            ),
        },
        Upstreams: statute.Upstreams{
            "api": statute.Pool{
                Backends: []statute.Backend{{Address: "10.0.0.1:8080"}, {Address: "10.0.0.2:8080"}},
                Strategy: statute.LeastConnections,
                HealthCheck: statute.HealthCheck{Path: "/healthz", Interval: "10s"},
            },
        },
        Routes: statute.Routes{
            statute.Match("/*").ProxyTo("api").With(statute.Timeout("30s")),
        },
        Defaults:      statute.Defaults{ReadHeaderTimeout: "5s", WriteTimeout: "30s", IdleTimeout: "120s"},
        Observability: statute.Observability{AccessLog: statute.JSONLog(statute.Stdout), Metrics: statute.Prometheus(":9090", "/metrics")},
        Shutdown:      statute.Shutdown{GracePeriod: "30s", DrainListeners: true},
    })
}

Why this and not nginx, Caddy, or Traefik

Choose statute when you want your reverse proxy configuration to be Go code that compiles. You get: type checking on every field, IDE completion as you write it, refactoring tools that work, the ability to put helper functions and conditional logic in your config without learning a templating language, and a single static binary that can't drift between "the config file on disk" and "what the daemon loaded".

You give up: hot reload, runtime configuration changes, plugin loading, web admin UIs, and a community ecosystem of off-the-shelf middleware. If those are deal-breakers, run Caddy or Traefik instead — they're better at being them.

statute is designed for teams that already build and ship Go binaries, where adding "edit the config file" as an extra deployment path costs more than recompiling and re-rolling.

Status

The framework is implemented and works end-to-end for the documented features. The HTTP-only, AutoTLS+HTTP-01, AutoTLS+DNS-01, and HTTP/3 paths all pass smoke tests. The API is design-stage and may shift in incompatible ways before a 1.0 release.

What's implemented:

  • HTTP/1.1 and HTTP/2 listeners
  • HTTP/3 (QUIC) via quic-go, with Alt-Svc advertisement
  • TLS termination: static certs, AutoTLS via autocert (HTTP-01 + TLS-ALPN-01), AutoTLS via custom DNS-01 manager with Cloudflare API
  • Upstream pools with round-robin, least-connections, IP-hash, and smooth weighted round-robin strategies
  • Backup tier failover when all primary backends are unhealthy
  • Active health checks with configurable thresholds
  • Per-route middleware: timeout, rate limit (token bucket), retry (idempotent-method only, gRPC-aware), cache, gzip + brotli compression, ETag
  • Static file serving
  • WebSocket pass-through (default httputil.ReverseProxy behaviour)
  • Structured JSON access logging with sampling
  • Prometheus-format metrics
  • OpenTelemetry tracing via OTLP/gRPC
  • pprof endpoints on the metrics server
  • Graceful shutdown with listener draining
  • Cloudflare-aware mode: BehindCloudflare() flips ALPN to suppress TLS-ALPN-01 and trusts CF-Connecting-IP
  • statute.Main CLI wrapper with -validate and -export flags

Install

go get statute.kjanat.dev

Requires Go 1.26 or newer.

Concepts

Config-as-code

The configuration is Go. Every field is a typed Go value. The Config struct is the entire surface of the framework.

statute.Run(statute.Config{
    Listeners:     ...,
    Upstreams:     ...,
    Routes:        ...,
    Defaults:      ...,
    Observability: ...,
    Shutdown:      ...,
})

Helper functions (HTTP, HTTPS, Match, RateLimit, …) construct the values; struct literals fill in named fields. Durations are strings ("10s", "90s", "1h") so the configuration reads like a config file rather than a Go program. Rate limits are strings ("100/min"). Sizes are strings (when added). Type checking still applies — invalid fields are caught at build time.

Two layers: surface and resolved

statute has two type packages:

  • statute.kjanat.dev is the surface API. It is what you write. It optimises for readability and ergonomic chaining.
  • statute.kjanat.dev/resolved is the resolved schema. It is what the runtime executes against. It optimises for invariants: durations are time.Duration, upstream references are *Pool pointers, optional fields are filled with their canonical defaults, no string-encoded values remain.

Tooling (validators, exporters, dashboards) targets the resolved schema. End users target the surface API. They are connected by a single Resolve(cfg) (*resolved.Config, error) function.

The pipeline: validate → resolve → run

Every config flows through three stages on startup:

  • Validate rejects structural and semantic errors with path-style locations: route[2] "/api/v1/*": unknown upstream "users".
  • Resolve parses durations, dereferences upstream names, fills defaults, normalises addresses, and emits a *resolved.Config.
  • Run opens listeners, builds per-backend reverse proxies, starts health checks, registers signal handlers, and serves.

You can stop after stage 2 with statute.Resolve() (for tooling) or statute.Export() (for snapshot and diff in CI).

Feature reference

Listeners
statute.HTTP(":80").RedirectTo("https")
statute.HTTPS(":443",
    statute.AutoTLS("example.com").Email("ops@example.com").Storage("/var/lib/statute/certs"),
    statute.HTTP2(),
    statute.HTTP3(":443/udp"),
    statute.BehindCloudflare(),
)
statute.HTTPS(":443",
    statute.StaticTLS("/etc/ssl/cert.pem", "/etc/ssl/key.pem"),
    statute.HTTP2(),
)

HTTP and HTTPS declare a listener; RedirectTo turns a listener into a permanent redirect. The HTTPS variant takes options as variadic arguments — TLS material, HTTP/2, HTTP/3, Cloudflare-awareness — composed flat rather than nested.

When AutoTLS is configured anywhere in the config, the plain-HTTP listener automatically serves /.well-known/acme-challenge/* so HTTP-01 validation works without separate plumbing.

Upstreams
Upstreams: statute.Upstreams{
    "api": statute.Pool{
        Backends: []statute.Backend{
            {Address: "10.0.0.1:8080", Weight: 2},
            {Address: "10.0.0.2:8080", Weight: 1},
            {Address: "10.0.0.3:8080", Backup: true},
        },
        Strategy: statute.LeastConnections,
        HealthCheck: statute.HealthCheck{
            Path: "/healthz", Interval: "10s", Timeout: "2s",
            Healthy: 2, Unhealthy: 3,
        },
        Transport: statute.Transport{
            MaxIdleConnsPerHost: 32,
            IdleConnTimeout:     "90s",
        },
    },
}

Upstreams are a named map. Routes refer to pools by string key. A single pool can be reused across many routes.

Strategies:

  • RoundRobin — even distribution.
  • LeastConnections — pick the backend with fewest in-flight requests. Best when request durations vary.
  • IPHash — consistent per-client routing for session affinity.
  • Weighted — smooth weighted round-robin (Nginx-style).

The picker filters to healthy primary backends; when no primary is healthy, it falls through to the backup tier; when none of those are healthy either, it goes degraded and tries primaries anyway. Active health checks demote and promote backends in the background based on consecutive success/failure thresholds.

Transport tunes the HTTP transport reused across all backends in the pool. The default MaxIdleConnsPerHost (32) is a much better default for a proxy than Go's stdlib value (2); leave it alone unless you know why you're changing it.

Routes and middleware
Routes: statute.Routes{
    statute.Match("/api/v1/*").Host("api.example.com").ProxyTo("api").
        With(
            statute.RateLimit("100/min").Per(statute.ClientIP),
            statute.Retry(3, statute.OnStatus(502, 503, 504)),
            statute.Timeout("30s"),
        ),
    statute.Match("/static/*").Serve("./public").
        With(statute.Cache("1h"), statute.Compress(statute.Gzip, statute.Brotli), statute.ETag()),
}

Routes are matched in declaration order; the first match wins. Patterns support exact match (/api) and a trailing wildcard (/api/*). Host scopes a route to a specific Host header value. Catch-all /* should be last.

Each route is either a proxy (ProxyTo("pool")) or a static-file serve (Serve("./dir")), not both.

Middleware:

  • Timeout(dur) — wraps the handler in http.TimeoutHandler. Returns 503 when exceeded.
  • RateLimit(rate).Per(key) — token bucket per key. Rate format is "N/unit" where unit is s, min, h. Keys: ClientIP (default), HostHeader.
  • Retry(max, OnStatus(...)) — retries upstream calls up to max attempts when the response status matches one of the listed codes. Skips for non-idempotent methods (POST, PATCH), gRPC, SSE, WebSocket upgrades, and bodies > 1 MiB. Buffers smaller bodies to replay on retry.
  • Cache(ttl) — in-process cache for 2xx GET/HEAD responses. Replace with a real LRU for high-cardinality deployments.
  • Compress(Gzip, Brotli) — negotiates content encoding via Accept-Encoding. Brotli preferred when the client advertises both.
  • ETag() — adds an SHA-256-based ETag to 200 responses; answers 304 on If-None-Match match.
Observability
Observability: statute.Observability{
    AccessLog: statute.JSONLog(statute.Stdout).Sample(0.1),
    Metrics:   statute.Prometheus(":9090", "/metrics"),
    Tracing:   statute.OTLP("otel-collector:4317").ServiceName("edge").Insecure().Sample(0.05),
}

Access log — one JSON line per request. Fields: ts, method, host, path, query, remote, user_agent, referer, status, duration_us, proto, forwarded_for. Sample(rate) records a fraction of successful requests; errors (status ≥ 400) are always logged regardless.

Metrics — Prometheus exposition format on a separate listener. Counters for total requests, requests by status, and request duration. pprof is mounted under /debug/pprof/* on the same listener.

Tracing — OTLP/gRPC export to an OpenTelemetry collector. Spans use HTTP semantic conventions. W3C trace context is automatically propagated to upstream backends (the reverse proxy injects traceparent and tracestate headers). Sample(rate) is TraceIDRatioBased with parent-based sampling, so trace continuity is preserved across hops.

TLS
// Static cert from disk
statute.StaticTLS("/etc/ssl/cert.pem", "/etc/ssl/key.pem")

// Auto-provisioned via Let's Encrypt with HTTP-01 (default)
statute.AutoTLS("example.com", "api.example.com").
    Email("ops@example.com").
    Storage("/var/lib/statute/certs")

// Auto-provisioned via Let's Encrypt with DNS-01 + Cloudflare
// (required for wildcards and when port 80 is not reachable)
statute.AutoTLS("*.example.com", "example.com").
    Email("ops@example.com").
    Storage("/var/lib/statute/certs").
    CloudflareDNS01(token).Zone(zoneID)

AutoTLS persistence is mandatory. The Storage directory holds the ACME account key, issued certs, and renewal state; without it, every restart re-registers and re-issues, blowing through Let's Encrypt rate limits in days.

The DNS-01 path is implemented in-tree using golang.org/x/crypto/acme directly + a tiny Cloudflare DNS API client. It does not pull in lego or certmagic. It supports wildcards and works without a publicly-reachable port 80. See docs/cloudflare.md for setup details.

HTTP/3
statute.HTTPS(":443",
    statute.AutoTLS(...),
    statute.HTTP2(),
    statute.HTTP3(":443/udp"),
)

When HTTP3() is on a listener, statute runs a quic-go HTTP/3 server alongside the HTTPS listener and adds an Alt-Svc: h3=":443"; ma=86400 header on every HTTPS response so browsers upgrade subsequent requests. The same TLS material is shared between the HTTPS listener and the HTTP/3 server.

CLI

The statute.Main(cfg) wrapper provides standard flags:

$ ./myproxy -validate          # parse and resolve, exit 0/1
$ ./myproxy -export             # write resolved config as JSON to stdout
$ ./myproxy                     # run the server

Use Run(cfg) directly if you want to handle flags yourself.

Production checklist

The following are framework-enforced or strongly recommended:

  • ReadHeaderTimeout is required. The default scaffold sets 5s. Without it, statute is vulnerable to Slowloris.
  • Graceful shutdown with Shutdown.GracePeriod and DrainListeners: true. Without it, every deploy drops in-flight requests.
  • Observability — at minimum, access log + metrics. A proxy without observability is operationally blind.
  • Persistent AutoTLS storage. Re-issuing on every restart will get the account rate-limited.
  • Health checks on every pool. Without them, statute keeps sending traffic to dead backends.
  • At least two backends per pool. A single-backend pool has no failover.
  • Tracing in production. Set a sample rate (e.g. 0.05) to control collector cost. Errors are still captured because parent-based sampling preserves traced error paths.
  • BehindCloudflare() when fronted by Cloudflare. Without it, client IPs collapse to the CF edge node and rate limiting becomes useless.

Examples

Examples are runnable Go programs in examples/:

  • examples/http-only — HTTP-only proxy on :8080. Smallest runnable config.
  • examples/basic — canonical AutoTLS + HTTP/2 + HTTP/3 setup.
  • examples/cloudflare — fronted by Cloudflare with HTTP-01 (no API key).
  • examples/cloudflare-wildcard — wildcard cert via Cloudflare DNS-01 + OTLP tracing.

Run any of them:

go run ./examples/http-only
go run ./examples/cloudflare-wildcard       # needs CLOUDFLARE_API_TOKEN

Deeper docs

  • docs/cloudflare.md — running behind Cloudflare, HTTP-01 vs DNS-01, settings to enable on the Cloudflare side, failure modes.
  • docs/observability.md — access log fields, metric names, span structure, sampling guidance.
  • docs/production.md — deployment patterns, ports, capabilities, the setcap trick for binding low ports as a non-root user.

Testing

go test ./...           # all unit tests
go vet ./...            # vet
golangci-lint run ./... # lint

The race detector (go test -race) does not work on Raspberry Pi / older 64-bit Arm kernels with VMA range < 48; this is a TSAN limitation, not a code issue.

License

See LICENSE.

Contributing

The API is design-stage. If you want to use statute in production, pin a specific commit, expect breakage on updates, and read the source — the tree is small (~4.3 kLOC across ~30 files) and self-contained.

Documentation

Overview

Package statute is a config-as-code reverse proxy framework.

Configurations are written as Go values, validated and resolved at startup, then executed by the runtime. There is no runtime config file, no hot reload, and no module loader — the binary IS the configuration.

See the examples directory for canonical usage.

Index

Examples

Constants

This section is empty.

Variables

View Source
var Stderr = LogWriter{/* contains filtered or unexported fields */}

Stderr writes logs to process stderr.

View Source
var Stdout = LogWriter{/* contains filtered or unexported fields */}

Stdout writes logs to process stdout.

Functions

func AllowIPs

func AllowIPs(cidrs ...string) *allowIPsMW

AllowIPs returns a middleware that admits only requests whose client IP falls within at least one of the configured CIDR ranges. Other requests are answered with 403 Forbidden.

CIDRs are parsed at resolve time via net/netip; both IPv4 and IPv6 are supported. Examples: "10.0.0.0/8", "2001:db8::/32", "203.0.113.5/32".

The client IP comes from clientIP(), which respects BehindCloudflare() (CF-Connecting-IP) when configured on the listener.

Example

ExampleAllowIPs shows the IP allow-list middleware. CIDRs are parsed once at startup; the runtime match is O(prefixes).

_ = statute.AllowIPs("10.0.0.0/8", "192.168.0.0/16")

func BasicAuth

func BasicAuth(realm string, users map[string]string) *basicAuthMW

BasicAuth returns an HTTP Basic Auth middleware. The users map keys are usernames; the values must be bcrypt hashes (the kind produced by `bcrypt.GenerateFromPassword`, prefixed with $2a$ / $2b$ / $2y$). At resolve time, every value is validated to be a recognisable bcrypt hash; non-bcrypt values are rejected loudly.

Realm is the value sent in the WWW-Authenticate header on unauthorized responses; browsers display it in the password prompt.

Important: bcrypt is intentionally slow. Each unauthorized request costs one bcrypt verification (~80ms at cost 10). For unauthenticated requests the cost is per attempt. Combine with RateLimit on the same route so a brute-force attacker cannot trivially DoS the proxy by repeatedly sending invalid credentials.

Note: BasicAuth over plain HTTP transmits credentials in clear-text base64. The lint check AUTH001 flags this configuration.

Example

ExampleBasicAuth shows BasicAuth with bcrypt password hashes. Generate hashes with bcrypt.GenerateFromPassword (cost >= 10).

users := map[string]string{
	"alice": "$2a$10$HwrzUQtDrRX0/09su3BahezCIqD.f4HjCkYD5b9w8gl4eUkPJzCyu", // password: "hunter2"
}
_ = statute.BasicAuth("Admin", users)

func BodyLimit

func BodyLimit(size string) *bodyLimitMW

BodyLimit returns a middleware that caps the request body at the given size. Sizes are strings like "1MB", "512KiB", or "10485760". Requests with a body larger than the limit receive a 413 Request Entity Too Large response; the upstream handler never sees them.

The cap is enforced via http.MaxBytesReader on r.Body; calls to Read past the limit return an error, which the wrapped handler is expected to surface as 413. For upstream proxies the reverse-proxy transport handles this transparently.

func CORS

func CORS() *corsMW

CORS returns a Cross-Origin Resource Sharing middleware. Pass at least Origins(...) to enable; without explicit origins the middleware does nothing.

Preflight (OPTIONS + Access-Control-Request-Method) is handled directly and responds 204 with the configured headers. Non-preflight requests are passed through after Access-Control-Allow-* headers are set; the upstream handler still sees the request.

A wildcard origin ("*") combined with Credentials() is rejected at resolve time — the CORS spec forbids credentialed wildcards.

Example

ExampleCORS shows a credentialed CORS policy bound to a specific origin.

_ = statute.CORS().
	Origins("https://app.example.com").
	Methods("GET", "POST", "PUT", "DELETE").
	Headers("Authorization", "Content-Type").
	Credentials().
	MaxAge("1h")

func Cache

func Cache(ttl string) *cacheMW

Cache returns a response-cache middleware with the given TTL.

func Compress

func Compress(algos ...CompressAlgo) *compressMW

Compress returns a response-compression middleware that negotiates one of the listed algorithms based on the request's Accept-Encoding header.

func DenyIPs

func DenyIPs(cidrs ...string) *denyIPsMW

DenyIPs returns a middleware that rejects requests whose client IP falls within at least one of the configured CIDR ranges. Other requests pass through.

DenyIPs is checked independently of AllowIPs; if both are configured on a route, the order they were added to With(...) determines precedence.

func ETag

func ETag() *etagMW

ETag returns a middleware that adds ETag headers to static file responses and serves 304 Not Modified for matching If-None-Match requests.

func Export

func Export(cfg Config, w io.Writer) error

Export validates and resolves the surface configuration, then writes the canonical resolved schema as JSON. Useful for diffing deployments and snapshotting in CI without starting a server.

func GraphDOT

func GraphDOT(cfg Config, w io.Writer) error

GraphDOT writes a Graphviz DOT representation of the resolved config to w. Render with `dot -Tsvg < input.dot > topology.svg` (or any DOT-capable renderer).

The graph has four kinds of nodes:

  • Listeners (Mrecord, blue) — one per HTTP/HTTPS listener.
  • Routes (rectangle, light yellow) — one per declared route.
  • Upstream pools (ellipse, green) — one per named pool.
  • Backends (circle, gray) — one per backend in each pool, dashed if Backup.

Edges:

  • Listener → Listener for redirect-to-https arrows.
  • Listener → Route for the matching relation (every content listener reaches every route; the host filter is on the route node).
  • Route → Pool for ProxyTo.
  • Pool → Backend for membership; weighted edges show Weight.

The output is intentionally minimal — no fancy layout, no colour palette. Pipe it through dot with your preferred styling.

func JSONLog

func JSONLog(dest LogWriter) *jsonLog

JSONLog returns a structured (JSON) access log writing to the given destination. By default every request is logged; use Sample to record only a fraction of requests at high traffic volumes.

func Main

func Main(cfg Config)

Main is a thin CLI wrapper around Run, Export, Lint, and Graph. It parses the standard process arguments and dispatches:

-export    Write the resolved configuration as JSON to stdout and exit.
-validate  Validate the configuration and exit. Prints "ok" on success.
-graph     Write the resolved topology as Graphviz DOT to stdout and exit.
-lint      Audit the resolved configuration against the production-readiness
           rule set; exit non-zero if any error-severity finding fires.
(no flag)  Equivalent to Run.

The four operation flags are mutually exclusive. Programs that want a clean entry point without flag handling can call Run directly.

func OTLP

func OTLP(endpoint string) *otlpTracing

OTLP configures distributed tracing via OTLP/gRPC to the given collector endpoint (for example "otel-collector:4317"). Spans are produced for every incoming request with HTTP semantic conventions, and W3C trace context is propagated to upstream backends.

func RateLimit

func RateLimit(rate string) *rateLimitMW

RateLimit returns a rate-limit middleware. The rate string is of the form "N/unit" where unit is one of s, min, h. For example "100/min".

Example

ExampleRateLimit shows the rate limiter keyed on client IP.

_ = statute.RateLimit("100/s").Per(statute.ClientIP)

func RequestID

func RequestID() *requestIDMW

RequestID returns a middleware that ensures every request carries a stable identifier — useful for tracing through logs and propagating to upstream backends. The default response header is defaultRequestIDHeader; override with Header.

If an inbound header (configured via From) is present, its value is used verbatim. Otherwise a new ID is generated from 16 bytes of crypto/rand, hex-encoded.

When the access log is configured, the request ID surfaces as a "request_id" field on log lines.

func Resolve

func Resolve(cfg Config) (*resolved.Config, error)

Resolve validates the surface configuration, fills defaults, and produces the canonical resolved schema. Resolve is pure: it does not touch the network, the filesystem, or process state.

func Retry

func Retry(max int, opts ...RetryOption) *retryMW

Retry returns a retry middleware with the given maximum attempts and options.

func Run

func Run(cfg Config)

Run validates, resolves, and runs the configuration. It blocks until the process receives SIGINT or SIGTERM, then performs a graceful shutdown.

Any validation or startup error is fatal: Run logs and exits non-zero.

func SecurityHeaders

func SecurityHeaders() *securityHeadersMW

SecurityHeaders returns a middleware that emits common HTTP security response headers. Defaults are conservative for a public-facing edge proxy:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin

HSTS, CSP, and Permissions-Policy are strictly opt-in via the builder methods. Override or disable an individual header by passing "" to its setter (or by not calling the setter, for HSTS/CSP/Permissions-Policy).

The CSP string is passed through verbatim — no parsing, no validation. The framework cannot know what your application needs.

Example

ExampleSecurityHeaders shows the recommended baseline for a public-facing origin: HSTS, CSP, and the default conservative headers.

_ = statute.SecurityHeaders().
	HSTS("365d").
	CSP("default-src 'self'; img-src 'self' data:")

func Timeout

func Timeout(dur string) *timeoutMW

Timeout returns a per-request timeout middleware.

Types

type AccessLog

type AccessLog interface {
	// contains filtered or unexported methods
}

AccessLog is a marker for an access log destination.

type AutoTLSConfig

type AutoTLSConfig struct {
	Domains []string
	// contains filtered or unexported fields
}

AutoTLSConfig declares ACME-managed TLS material.

func AutoTLS

func AutoTLS(domains ...string) *AutoTLSConfig

AutoTLS configures ACME auto-provisioning for the given domains.

func (*AutoTLSConfig) CloudflareDNS01

func (a *AutoTLSConfig) CloudflareDNS01(apiToken string) *AutoTLSConfig

CloudflareDNS01 switches the ACME challenge from HTTP-01 to DNS-01 using Cloudflare's DNS API. Required for wildcard certificates and useful when :80 is not reachable from the public internet (private networks, Cloudflare-only origins, etc.).

The token must be a Cloudflare API Token (not the legacy Global API Key) with the Zone.DNS:Edit permission for the zone(s) covering the listener's domains. Generate one at https://dash.cloudflare.com/profile/api-tokens.

The zone is auto-discovered from each domain by walking the DNS labels against the account's zone list. Use Zone() to pin a specific zone ID and skip discovery.

Returns the parent AutoTLSConfig so the call chain remains a single ListenerOption value usable as an argument to HTTPS.

func (*AutoTLSConfig) Email

func (a *AutoTLSConfig) Email(email string) *AutoTLSConfig

Email sets the contact email registered with the ACME directory.

func (*AutoTLSConfig) Storage

func (a *AutoTLSConfig) Storage(path string) *AutoTLSConfig

Storage sets the on-disk path where issued certificates and ACME state are persisted. Required for production use.

func (*AutoTLSConfig) Zone

func (a *AutoTLSConfig) Zone(id string) *AutoTLSConfig

Zone pins the Cloudflare zone ID for DNS-01 challenges. Must be called after CloudflareDNS01. When unset the zone is discovered by querying Cloudflare for the zone whose name is a suffix of each domain.

type Backend

type Backend struct {
	// Address is the host:port of the backend.
	Address string
	// Weight is the relative weight for weighted strategies. Defaults to 1.
	Weight int
	// Backup is true for failover-only backends; they receive traffic only
	// when all primary backends are unhealthy.
	Backup bool
}

Backend is a single upstream target.

type CompressAlgo

type CompressAlgo int

CompressAlgo identifies a content-encoding algorithm.

const (
	// Gzip compression.
	Gzip CompressAlgo = iota
	// Brotli compression.
	Brotli
)

func (CompressAlgo) String

func (a CompressAlgo) String() string

String returns the canonical name of the algorithm.

type Config

type Config struct {
	Listeners     Listeners
	Upstreams     Upstreams
	Routes        Routes
	Defaults      Defaults
	Observability Observability
	Shutdown      Shutdown
}

Config is the top-level surface configuration.

type Defaults

type Defaults struct {
	// ReadHeaderTimeout caps how long a client may take to send the request
	// headers. The Go standard library has no default; setting this is the
	// primary mitigation for Slowloris denial-of-service.
	ReadHeaderTimeout string

	// ReadTimeout caps the entire request read, including body. Use with care
	// for streaming and long-poll endpoints.
	ReadTimeout string

	// WriteTimeout caps how long the server may take to write the response.
	WriteTimeout string

	// IdleTimeout caps how long an idle keep-alive connection may sit between
	// requests before being closed.
	IdleTimeout string

	// MaxHeaderBytes caps the size of the request header block. Defaults to
	// the Go standard library default of 1MB when unset.
	MaxHeaderBytes int
}

Defaults sets the conservative production baseline for all listeners. Routes may override individual values via middleware.

type Finding

type Finding struct {
	// Severity is "warning" or "error".
	Severity Severity
	// Code is the stable rule identifier (e.g. "RHT001"). Use this in
	// suppress directives once those exist.
	Code string
	// Message is a one-line human-readable description.
	Message string
	// Path is a config-pointer-style string identifying the offending
	// element (e.g. `listeners[0]`, `upstreams["api"]`).
	Path string
}

Finding describes a single rule hit during a Lint pass.

func Lint

func Lint(cfg Config) ([]Finding, error)

Lint validates the surface config (via Resolve) and then runs the production-readiness rule set against the resolved schema. Returns the findings in declaration order; structural validation errors come back as the error return.

The rule set is intentionally small and stable for v0.2.0. Additional rules will be added in later releases.

func (Finding) String

func (f Finding) String() string

type HealthCheck

type HealthCheck struct {
	Path      string // HTTP path to probe; empty disables active health checks
	Interval  string // how often to probe; e.g. "10s"
	Timeout   string // probe timeout; e.g. "2s"
	Healthy   int    // consecutive successes to mark healthy; defaults to 2
	Unhealthy int    // consecutive failures to mark unhealthy; defaults to 3
}

HealthCheck configures active health checks against backends.

type Listener

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

Listener is a surface listener declaration. Construct via HTTP or HTTPS.

func HTTP

func HTTP(addr string) *Listener

HTTP starts an HTTP/1.1 listener declaration on the given address.

Example

ExampleHTTP shows the minimal config: one HTTP listener proxying to an upstream pool. Compile and run as a standalone binary.

statute.Main(statute.Config{
	Listeners: statute.Listeners{
		statute.HTTP(":8080"),
	},
	Upstreams: statute.Upstreams{
		"api": statute.Pool{
			Backends: []statute.Backend{{Address: "127.0.0.1:9001"}},
		},
	},
	Routes: statute.Routes{
		statute.Match("/*").ProxyTo("api"),
	},
})

func HTTPS

func HTTPS(addr string, opts ...ListenerOption) *Listener

HTTPS starts an HTTPS listener declaration on the given address. Options configure TLS material, HTTP/2, and HTTP/3.

Example (AutoTLS)

ExampleHTTPS_autoTLS shows AutoTLS with Let's Encrypt HTTP-01. The :80 listener serves the ACME challenge automatically because AutoTLS is configured elsewhere in the config; in production it should also redirect non-challenge traffic to HTTPS.

statute.Main(statute.Config{
	Listeners: statute.Listeners{
		statute.HTTP(":80").RedirectTo("https"),
		statute.HTTPS(":443",
			statute.AutoTLS("example.com").
				Email("ops@example.com").
				Storage("/var/lib/statute/certs"),
			statute.HTTP2(),
		),
	},
	Upstreams: statute.Upstreams{
		"api": statute.Pool{
			Backends: []statute.Backend{{Address: "10.0.0.1:8080"}},
		},
	},
	Routes: statute.Routes{
		statute.Match("/*").ProxyTo("api"),
	},
	Defaults: statute.Defaults{ReadHeaderTimeout: "5s"},
})

func (*Listener) RedirectTo

func (l *Listener) RedirectTo(scheme string) *Listener

RedirectTo turns this listener into a permanent redirect to the named scheme. The listener will not serve content beyond the redirect.

type ListenerOption

type ListenerOption interface {
	// contains filtered or unexported methods
}

ListenerOption configures an HTTPS listener.

func BehindCloudflare

func BehindCloudflare() ListenerOption

BehindCloudflare marks the listener as sitting behind a Cloudflare proxy. This affects two things:

First, when AutoTLS is configured on the listener, the TLS-ALPN-01 challenge is suppressed (the "acme-tls/1" entry is dropped from ALPN). Cloudflare terminates TLS at its edge and does not forward custom ALPN protocols, so TLS-ALPN-01 cannot succeed. Provisioning falls back to HTTP-01, which is served by the redirect listener on :80 — Cloudflare proxies that path transparently provided "Always Use HTTPS" is disabled for /.well-known/acme-challenge/*.

Second, the request handling path trusts the CF-Connecting-IP and True-Client-IP headers as the originating client address. Other proxy headers (X-Forwarded-For) remain available but Cloudflare's are preferred because they are populated by the proxy and not user-controllable.

func HTTP2

func HTTP2() ListenerOption

HTTP2 enables HTTP/2 on the listener. Required for h2 ALPN negotiation.

func HTTP3

func HTTP3(addr string) ListenerOption

HTTP3 enables HTTP/3 (QUIC) on the listener at the given UDP address. The addr should typically match the HTTPS port suffixed with /udp, for example ":443/udp".

type Listeners

type Listeners []*Listener

Listeners is the list of listener declarations.

type LogWriter

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

LogWriter identifies a destination for structured logs.

func (LogWriter) Name

func (l LogWriter) Name() string

Name returns a human-readable name for the destination.

func (LogWriter) Writer

func (l LogWriter) Writer() io.Writer

Writer returns the underlying io.Writer. Useful when constructing the resolved configuration.

type Metrics

type Metrics interface {
	// contains filtered or unexported methods
}

Metrics is a marker for a metrics endpoint configuration.

func Prometheus

func Prometheus(addr, path string) Metrics

Prometheus exposes process and request metrics on the given address and path, formatted in the Prometheus exposition format.

type Middleware

type Middleware interface {
	// contains filtered or unexported methods
}

Middleware is a marker interface for surface middleware values. Concrete middleware constructors return values that satisfy this interface.

type Observability

type Observability struct {
	AccessLog AccessLog
	Metrics   Metrics
	Tracing   Tracing
}

Observability bundles the logging, metrics, and tracing configuration.

type Pool

type Pool struct {
	Backends    []Backend
	Strategy    Strategy
	HealthCheck HealthCheck
	Transport   Transport
}

Pool is the surface upstream pool definition.

type RateLimitKey

type RateLimitKey int

RateLimitKey selects what attribute of the request a rate limit is keyed on.

const (
	// ClientIP keys the rate limiter on the client's IP address.
	ClientIP RateLimitKey = iota
	// HostHeader keys the rate limiter on the Host header.
	HostHeader
)

func (RateLimitKey) String

func (k RateLimitKey) String() string

String returns the canonical name of the key.

type RetryOption

type RetryOption interface {
	// contains filtered or unexported methods
}

RetryOption configures the Retry middleware.

func OnStatus

func OnStatus(codes ...int) RetryOption

OnStatus retries when the upstream returns any of the given status codes.

type Route

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

Route is a surface route declaration. Construct via Match.

func Match

func Match(pattern string) *Route

Match begins a route declaration matching the given path pattern. Patterns support a trailing /* wildcard.

Example (ProxyTo)

ExampleMatch_proxyTo shows a host-scoped proxy route.

_ = statute.Routes{
	statute.Match("/api/v1/*").Host("api.example.com").ProxyTo("api").
		With(
			statute.Timeout("30s"),
			statute.RateLimit("100/min").Per(statute.ClientIP),
		),
}

func (*Route) Host

func (r *Route) Host(host string) *Route

Host scopes this route to the given Host header value. Empty means any host.

func (*Route) ProxyTo

func (r *Route) ProxyTo(upstream string) *Route

ProxyTo proxies matching requests to the named upstream pool.

func (*Route) Serve

func (r *Route) Serve(dir string) *Route

Serve serves matching requests as static files from the given directory.

func (*Route) With

func (r *Route) With(mws ...Middleware) *Route

With attaches middleware to the route. Middleware runs in declaration order before the upstream proxy or static file handler.

type Routes

type Routes []*Route

Routes is the list of route declarations, matched in declaration order.

type Severity

type Severity string

Severity of a lint finding.

const (
	SeverityWarning Severity = "warning"
	SeverityError   Severity = "error"
)

Standard severity levels. Findings of Error severity cause `-lint` to exit non-zero; Warnings are reported but do not fail the run.

type Shutdown

type Shutdown struct {
	// GracePeriod is the maximum time the server will wait for in-flight
	// requests to finish before forcibly closing connections. e.g. "30s".
	GracePeriod string

	// DrainListeners closes listeners (stops accepting new connections)
	// before waiting for in-flight requests. Recommended for production.
	DrainListeners bool
}

Shutdown configures graceful shutdown behaviour.

type StaticTLSConfig

type StaticTLSConfig struct {
	CertFile string
	KeyFile  string
}

StaticTLSConfig declares pre-provisioned TLS material.

func StaticTLS

func StaticTLS(certFile, keyFile string) *StaticTLSConfig

StaticTLS configures TLS using a static certificate and key on disk.

type Strategy

type Strategy int

Strategy selects how a request is routed across the backends in a pool.

const (
	// RoundRobin distributes requests evenly across backends in declaration order.
	RoundRobin Strategy = iota
	// LeastConnections sends each request to the backend with the fewest in-flight requests.
	LeastConnections
	// IPHash routes requests from the same client IP to the same backend (consistent hash).
	IPHash
	// Weighted distributes requests proportionally to each backend's Weight.
	Weighted
)

func (Strategy) String

func (s Strategy) String() string

String returns the canonical name of the strategy.

type Tracing

type Tracing interface {
	// contains filtered or unexported methods
}

Tracing is a marker for a distributed-tracing exporter configuration.

type Transport

type Transport struct {
	MaxIdleConnsPerHost int
	IdleConnTimeout     string // e.g. "90s"
	DialTimeout         string // e.g. "5s"
	TLSHandshakeTimeout string // e.g. "5s"
}

Transport tunes the HTTP transport used to reach backends.

type Upstreams

type Upstreams map[string]Pool

Upstreams maps an upstream name to its pool definition. Routes refer to upstreams by name.

Directories

Path Synopsis
examples
basic command
cloudflare command
Example: a statute deployment fronted by Cloudflare with origin AutoTLS.
Example: a statute deployment fronted by Cloudflare with origin AutoTLS.
cloudflare-wildcard command
Example: wildcard certificate via Cloudflare DNS-01.
Example: wildcard certificate via Cloudflare DNS-01.
dev command
dev is a runnable example demonstrating round-robin load balancing across three echo backends with active health checks.
dev is a runnable example demonstrating round-robin load balancing across three echo backends with active health checks.
http-only command
Package resolved is the canonical, fully-validated schema that the statute runtime operates on.
Package resolved is the canonical, fully-validated schema that the statute runtime operates on.

Jump to

Keyboard shortcuts

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