server

package
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 40 Imported by: 0

Documentation

Overview

Package server is cadish's HTTP caching reverse proxy: the layer that turns the pure pipeline decisions (internal/pipeline), the two-tier cache (internal/cache) and the origin layer (internal/origin) into a live net/http handler.

The request lifecycle implemented here is:

site selection (Host) -> EvalRequest (respond/purge/pass/cache_key)
  -> LOOKUP (fresh HIT | stale HIT + bg revalidate | MISS)
  -> ORIGIN (single-flight coalesced fetch, serve-and-cache)
  -> EvalResponse (TTL/grace/hit-for-miss/storage)
  -> DELIVER (header ops, strip_cookies, CORS, cache-status header)

TLS and load balancing are wired in (M5c):

  • TLS: when any site declares `tls`, Server binds a hardened :443 listener (termination + ACME via internal/tlsacme) plus a :80 server for ACME HTTP-01 challenges and HTTPS redirects; otherwise it serves a single plain-HTTP listener. The choice is made from config.Config.TLS.
  • LB: an `upstream`/`cluster` is built as an internal/lb Upstream (round-robin / least-conn / sticky / shard, active health, dynamic dns:// resolution), which is itself an origin.Origin. The handler routes per request via originFor(routedUpstream) and attaches the {sticky} routing key with lb.WithRoutingKey for sticky / shard-by-key pools. lb background workers run for the server's lifetime (cancelled on Shutdown).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClientAcceptsEncoding added in v0.2.3

func ClientAcceptsEncoding(acceptEncoding, coding string) bool

ClientAcceptsEncoding reports whether a client's Accept-Encoding header accepts the given content-coding. identity is always acceptable. A coding is accepted when it is listed with a non-zero q-value, or a `*` is listed with non-zero q. An empty Accept-Encoding means the client expressed no preference, which per RFC 9110 §12.5.3 means identity only — so a non-identity coding is NOT accepted. Matching is case-insensitive on the token.

acceptEncoding must be the header's COMBINED field-value (every field line joined with ", " — RFC 9110 §5.3), the same view Headers.get gives the edge worker; passing only the first line hides codings a client split across lines. Exported so the conformance reference (test/conformance) calls THIS function instead of keeping a drifting copy; edge/runtime/interpreter.js clientAcceptsEncoding is its byte-for-byte JS mirror.

Types

type AccessHub

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

AccessHub is the in-memory access-log fan-out (the Varnish-VSL analogue). The hot path checks ONE atomic ("any subscribers?"); with none attached it does nothing — no record built, no formatting, no syscall, no disk. When consumers are attached it fan-outs each record to their buffered channels with a NON-BLOCKING send: a full channel drops + counts, so a slow consumer never slows the server.

When the hub is disabled (`access_log off`) it registers no subscribers and the count stays 0 forever, so the idle atomic check is the only cost the server ever pays.

func (*AccessHub) Enabled

func (h *AccessHub) Enabled() bool

Enabled reports whether the hub accepts subscribers (false under `access_log off`).

type AccessSocket

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

AccessSocket is a running unix-domain stream server: it accepts local consumer connections and streams each one the hub's live access records as NDJSON. It is the transport half of D44 — `cadish logs` dials this socket and reuses internal/logs (ParseLine/Filter/Render) verbatim on the stream.

The socket is local-only and created with 0600 permissions, so no auth is needed (filesystem-permissioned, loopback-equivalent). There is no backlog: a consumer receives only records that arrive after it connects.

func ListenAccessSocket

func ListenAccessSocket(ctx context.Context, hub *AccessHub, path string, log *slog.Logger) (*AccessSocket, error)

ListenAccessSocket binds the unix socket at path (removing a stale one first), chmods it to 0600, and starts accepting consumer connections until ctx is cancelled. It returns the running *AccessSocket (call Close to stop and unlink) or an error if the socket cannot be bound.

func (*AccessSocket) Close

func (s *AccessSocket) Close() error

Close stops accepting and unlinks the socket file. Idempotent.

func (*AccessSocket) Path

func (s *AccessSocket) Path() string

Path is the bound socket path (useful for tests and logging).

type AuditLog

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

AuditLog is the async, non-blocking security-audit sink. Records are enqueued on a buffered channel and serialized to the underlying writer by a single goroutine, so a slow/full disk NEVER blocks request serving: a full channel drops + counts.

It is NIL when no audit log is configured (the default — `security { audit_log off }` / no security block / no flag). A nil *AuditLog is a no-op on every method (Record drops nothing because Enabled() is false first), so the datapath pays the cost of exactly one nil/branch check when the audit log is off.

func NewAuditLog

func NewAuditLog(path string) (*AuditLog, error)

NewAuditLog builds an audit log from a configured path. The path is either:

  • "" or "off" → returns (nil, nil): the audit log is DISABLED (zero cost).
  • a directory → writes <dir>/security-audit.log (created/appended).
  • a file path → appends to that file (its parent dir must exist).

A directory is detected by an existing dir OR a trailing separator; otherwise the path is treated as a file. The returned log owns a background writer goroutine; the caller MUST Close it at shutdown to flush and release the file.

func (*AuditLog) Close

func (a *AuditLog) Close() error

Close stops accepting records, drains the queue, waits for the writer goroutine to flush, then closes any owned file. Safe to call once; nil-safe.

func (*AuditLog) Enabled

func (a *AuditLog) Enabled() bool

Enabled reports whether the audit log accepts records (false when nil / off).

func (*AuditLog) Record

func (a *AuditLog) Record(rec AuditRecord)

Record enqueues one audit record with a NON-BLOCKING send: a full channel drops the record and bumps the dropped counter rather than ever blocking the caller (the request hot path). A nil *AuditLog (audit off) is a no-op.

type AuditRecord

type AuditRecord struct {
	Time     time.Time
	Action   string // "deny" | "ratelimit"
	Enforced bool   // true = action taken; false = monitor mode (would-block)
	Rule     string // matched rule identity ("" when inline/unnamed)
	Method   string
	Host     string
	Path     string
	ClientIP string // resolved real client IP (trusted-proxy aware)
	Status   int    // HTTP status returned (or that WOULD have been, in monitor)
}

AuditRecord is one structured security-audit fact, emitted for each ENFORCED or MONITORED security-gate action (deny / ratelimit / monitored would-block). It is built on the request path but serialized off it, in the writer goroutine.

PRIVACY: unlike the access log (which deliberately OMITS the client IP and query — signed-URL signatures are sensitive, D18), the security audit log's whole point is recording WHO was blocked, so it MAY carry the real client IP. The query string (and its signed-URL signature) is NEVER recorded here either.

type Handler

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

Handler is the cadish http.Handler. It is safe for concurrent use and holds the per-request machinery (freshness index, request coalescer, background-fetch single-flight, origin stall sweeper) shared across all sites, plus the bound sites.

func NewHandler

func NewHandler(cfg *config.Config, opts Options) *Handler

NewHandler builds the cadish handler from a loaded config.

func (*Handler) AccessHub

func (h *Handler) AccessHub() *AccessHub

AccessHub exposes the in-memory access-log fan-out so the run command can bind the streaming unix socket consumers subscribe to. Never nil.

func (*Handler) LiveState

func (h *Handler) LiveState() []SiteState

LiveState returns the current per-site cache fill for every bound site. It is cheap (lock-free cache atomics) and safe for concurrent use; the admin dashboard calls it per scrape/tick.

func (*Handler) Reload

func (h *Handler) Reload(next *config.Config)

Reload atomically swaps the handler's routing to serve next, preserving the shared datapath machinery (freshness index, coalescer, single-flights, sweeper, metrics, tracer). In-flight requests finish on the routing they already loaded; new requests pick up the swap immediately after the atomic store.

The warm-cache transplant (so a reload does NOT cold the cache) is done by next.TransplantStoresFrom(old) BEFORE this call, which is why Server.Reload runs it first. Reload itself only rebuilds and swaps the routing table; config validation happens earlier in Server.Reload (a bad config never reaches here, so the old config keeps serving). Reload does not fail.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the cadish request lifecycle: site selection, EvalRequest (respond/purge/pass/key), LOOKUP (fresh/stale/miss), ORIGIN (coalesced fetch + serve-and-cache), and DELIVER (header ops, cache-status).

func (*Handler) Shutdown

func (h *Handler) Shutdown()

Shutdown stops the background stall sweeper. The cache stores are owned by the config.Config and closed there.

type Options

type Options struct {
	// Logger receives the per-request access log and warnings. Defaults to
	// slog.Default().
	Logger *slog.Logger
	// Now is an injectable clock for freshness timing (tests). Defaults to
	// time.Now.
	Now func() time.Time
	// IdleTimeout aborts an origin body that stalls for this long mid-stream. 0
	// disables the watchdog. Defaults to 0 (disabled).
	IdleTimeout time.Duration
	// BgRevalidateTimeout bounds a background (grace) revalidation fetch. Defaults
	// to 30s.
	BgRevalidateTimeout time.Duration
	// Metrics is the observability seam fed from the request datapath. Leave nil
	// (the default) when no admin/dashboard surface is configured: a nil *Metrics
	// is a no-op on every recorder, so the datapath pays nothing. The admin server
	// (internal/admin) constructs one and passes it here.
	Metrics *metrics.Metrics

	// AccessLogOff disables the in-memory access-log hub entirely (`access_log off`
	// / `-access-log off`). The DEFAULT (zero value, false) is hub ON — but idle-free
	// until a `cadish logs` consumer attaches. When true, the hub registers no
	// subscribers and the hot-path atomic check is the only cost ever paid (D44).
	AccessLogOff bool

	// AuditLog is the async, non-blocking security-audit sink (WAF v1c / D52). Leave
	// nil (the default) when no `security { audit_log … }` is configured — the audit
	// log is OFF by default and a nil sink is a no-op on every record (zero cost). The
	// run command constructs one from config/flag and passes it here; the Handler owns
	// closing it at Shutdown.
	AuditLog *AuditLog

	// Tracer is the varnishlog-style transaction-trace seam. Leave nil (the
	// default) for zero cost on the datapath; `cadish run -trace` (or CADISH_TRACE)
	// constructs one writing to stderr. When set, every request emits a multi-line
	// decision trace (matched route, cache key, EvalResponse ttl/grace/hit-for-miss,
	// pass reason, routed upstream, transforms).
	Tracer *Tracer

	// HTTPSAddr is the TLS listener address used when any site declares `tls`.
	// Defaults to ":443".
	HTTPSAddr string
	// ACMECacheDir overrides where issued ACME certificates are cached (empty uses
	// the tlsacme default resolution order).
	ACMECacheDir string
	// ACMEDirectoryURL overrides the ACME directory endpoint (e.g. a pebble/staging
	// server for tests). Empty uses Let's Encrypt production.
	ACMEDirectoryURL string

	// MaxRequestBodyBytes optionally caps the size of a CLIENT request body that the
	// proxy will read and forward to origin/peer, via http.MaxBytesReader at ingress.
	// 0 (the DEFAULT) means UNLIMITED — deliberately so: cadish is a media/cache edge
	// and a hard default cap would break large legitimate uploads/streaming. An
	// operator who wants to bound upload size sets a positive value; the cap then
	// applies only to body-carrying methods (anything but GET/HEAD), and a body that
	// exceeds it is rejected by net/http with a 413. When 0 the ingress path takes no
	// extra allocation and the body is streamed straight through (zero cost on the hot
	// GET path, which never has a body anyway).
	MaxRequestBodyBytes int64

	// ForceTLS makes the server bind a TLS-capable :443 (+ :80) listener even when the
	// startup config declares no `tls` — the Ingress-controller mode (D55): TLS hosts
	// arrive later via reconcile, so the listener and the ACME source must already exist
	// at startup (the autocert source is fixed at construction). The ACME HostPolicy
	// still starts empty (never an open issuer) and BYO Secret certs are injected live
	// via Server.SetDynamicCerts. Off (the default) keeps the legacy behavior: bind :443
	// only when a site declares `tls`.
	ForceTLS bool
	// ACMEEmail is the ACME account contact used when ForceTLS builds the autocert
	// source and the startup config has no ACME site to supply one. Optional.
	ACMEEmail string

	// UnmatchedHostStatus overrides the status returned for a request whose Host matched no
	// declared site AND the lenient single-site fallback was suppressed (the
	// genuinely-no-site case — NOT the strict_host 421 case, which is unaffected). 0 (the
	// DEFAULT) keeps the core server's 502 Bad Gateway. The Gateway data plane sets this to
	// 404 (GW-P1): a Gateway API client expects 404 for an unmatched host, and only the
	// gateway controller opts in — the core (non-gateway) server's 502 is unchanged.
	UnmatchedHostStatus int

	// ReloadOptions carries the run-time config.LoadOptions a SIGHUP reload must reuse so
	// the recompiled config keeps the flags the process started with — chiefly the
	// `--kubeconfig` path for out-of-cluster k8s:// resolution (without it, Reload would
	// recompile with the ZERO options and the rebuilt k8s client would silently fall back
	// to the default chain). Set by `cadish run` to LoadOptions{Kubeconfig: *kubeconfig};
	// the zero value (empty) preserves the default chain, so an unset --kubeconfig is
	// unchanged. EndpointResolver is deliberately NOT carried here: in `cadish run` the
	// k8s client is config-owned and a reload intentionally rebuilds it (the ingress
	// controller, which injects a resolver, drives ApplyConfig directly, not Reload).
	ReloadOptions config.LoadOptions
}

Options configures a Handler/Server. The zero value is usable; New fills sane defaults.

type Server

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

Server wraps a Handler with its net/http listeners and graceful shutdown. It runs in one of two modes, chosen by whether any site declared `tls`:

  • Plain HTTP: a single listener on the HTTP address.
  • TLS: a hardened :443 listener (TLS termination + ACME via tlsacme) plus a :80 server that answers ACME HTTP-01 challenges and 301-redirects to HTTPS.

On start it also launches every lb.Upstream's background workers (active health probing + dynamic re-resolution), bound to a serving context cancelled by Shutdown.

func NewServer

func NewServer(cfg *config.Config, httpAddr string, opts Options) (*Server, error)

NewServer builds a Server that serves cfg, with httpAddr the plain/redirect/ACME HTTP address (e.g. ":80"). The TLS listener address comes from Options.HTTPSAddr. It returns an error if the TLS configuration is invalid (e.g. a bad static keypair).

func (*Server) AccessHub

func (s *Server) AccessHub() *AccessHub

AccessHub exposes the server's in-memory access-log fan-out (D44), so the run command can start the unix-socket stream server `cadish logs` consumes.

func (*Server) ApplyConfig

func (s *Server) ApplyConfig(next *config.Config) error

ApplyConfig atomically swaps the server to serve next — an ALREADY-LOADED config — preserving the shared datapath machinery (freshness index, request coalescer, background single-flights, stall sweeper, metrics, tracer, access hub, audit log, rate limiter) and the warm cache stores of sites that survive the swap (matched by primary host). It is the seam both Reload (load Cadishfile from disk → ApplyConfig) and the ingress controller (translate Ingress objects → config.LoadString → ApplyConfig) drive.

It is fail-safe: next's pools are started FIRST, so if next cannot start (e.g. its k8s informer caches won't sync) the OLD config keeps serving UNCHANGED and the error is returned WITHOUT any swap. ApplyConfig takes ownership of next: on a start failure it closes next; on success it tears down the old config (preserving transplanted stores) in the background after a drain grace.

ApplyConfig is safe for concurrent calls (serialized by reloadMu).

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler exposes the underlying http.Handler (for httptest and embedding).

func (*Server) ListenAndServe

func (s *Server) ListenAndServe() error

ListenAndServe starts the server and blocks until Shutdown (or a listener error). When any site needs TLS it binds :80 (ACME challenge + HTTPS redirect) and :443 (hardened TLS); otherwise it binds a single plain-HTTP listener. A clean shutdown returns nil.

func (*Server) LiveState

func (s *Server) LiveState() []SiteState

LiveState returns the current per-site observability state (cache fill) for the admin/dashboard surface. See Handler.LiveState.

func (*Server) MarkWarm added in v0.2.1

func (s *Server) MarkWarm()

MarkWarm marks the data plane READY to serve (idempotent): the reserved /.cadish/readyz probe returns 200 instead of 503 once this is called. The ingress/gateway controllers call it after their FIRST successful reconcile builds the routing table from synced listers; `cadish run` calls it once the server is serving (its startup config was applied at construction). It is a single lock-free atomic store, safe for concurrent use, and a no-op on every call after the first.

func (*Server) NeedsTLS

func (s *Server) NeedsTLS() bool

NeedsTLS reports whether the server will bind a TLS listener (any site declares `tls` with acme or a static keypair, or ForceTLS / a BYO dynamic cert is set).

func (*Server) Reload

func (s *Server) Reload() error

Reload re-reads the Cadishfile from disk, re-parses + recompiles it, and — only if it is VALID — atomically swaps the new routing into the live handler. On any parse/compile/load error it returns that error and KEEPS SERVING the old config unchanged (fail-safe: a bad reload never drops the listener or colds the cache).

What is PRESERVED across a successful reload:

  • the listeners and all in-flight requests (they finish on the old routing);
  • the freshness index, request coalescer, background single-flight and stall sweeper (the Handler's shared machinery — never rebuilt);
  • per surviving site (matched by primary host): its warm cache.Store (so the hit ratio is not lost);
  • each UNCHANGED lb pool (same name + fingerprint): the live *lb.Upstream with its warm health FSM and goroutines (D58 — steady backends are not re-probed).

What is REBUILT: the compiled pipeline + routing, origins, CHANGED/added lb pools, clusters, classifiers, geo sources. The TLS HostPolicy (ACME allow-list + static keypairs + HSTS) is RELOADED LIVE via the Manager's atomic hostState (D58) — adding or removing a TLS hostname needs no restart; only first-time enabling of TLS/ACME on a server started without it does, since the autocert source + :443 listener are fixed at startup. The plain-HTTP/admin listeners are untouched.

Reload is safe for concurrent calls (serialized by reloadMu) but is normally driven by a single SIGHUP handler.

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

Serve serves plain HTTP on an already-open listener (used by tests and by ListenAndServe's no-TLS path). The lb/k8s background workers were started in NewServer (fail-fast), so Serve only begins accepting connections.

func (*Server) SetDynamicCerts

func (s *Server) SetDynamicCerts(certs []tlsacme.DynamicCert) error

SetDynamicCerts injects BYO / cert-manager keypairs (from kubernetes.io/tls Secrets) into the live TLS manager — the Ingress controller's typed side-channel for spec.tls Secrets (D55). It is a hot swap: the :443 listener, *tls.Config and autocert source are untouched, so a Secret rotation takes effect with no restart. Fail-safe: any bad PEM returns an error and the previous dynamic set keeps serving.

func (*Server) SetForceRedirectHosts

func (s *Server) SetForceRedirectHosts(hosts []string)

SetForceRedirectHosts injects the hosts that should be 301'd HTTP→HTTPS even without local TLS — the Ingress controller's side-channel for the `cadi.sh/ssl-redirect` annotation. A hot swap (no listener rebuild); an empty slice clears the set. Only effective when the redirect is gated (Ingress mode); a nil manager is a no-op.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown gracefully drains in-flight requests (up to ctx's deadline), stops the lb background workers and the origin stall sweeper, and closes the cache stores. In TLS mode the cancellation unblocks the tlsacme Servers, which drain themselves.

type SiteState

type SiteState struct {
	Name      string      `json:"name"`
	Addresses []string    `json:"addresses"`
	Cache     cache.Stats `json:"cache"`
}

SiteState is a point-in-time observability view of one bound site: its name, its addresses, and its two-tier cache fill. It is read on demand (never mirrored into counters) so it cannot drift from the live objects.

type Tracer

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

Tracer is the varnishlog-style transaction-trace seam. It is NIL by default; every method has a nil-safe receiver and the hot path holds a (possibly nil) *traceRecord whose recorders are all no-ops when nil, so a non-tracing cadish pays nothing — no allocation, no formatting — mirroring how the metrics seam is gated (see internal/metrics). A Tracer is constructed only when `-trace` (or the CADISH_TRACE env) is set.

One traceRecord accumulates the per-request DECISION as the request flows through the handler lifecycle (RECV -> KEY -> LOOKUP -> ORIGIN -> DELIVER), then the ServeHTTP defer flushes it as one multi-line transaction block, the way `varnishlog` groups a transaction. Safe for concurrent use: the shared Tracer only owns an io.Writer guarded by a mutex; each in-flight request owns its own traceRecord (no shared mutable state on the hot path).

func NewTracer

func NewTracer(w io.Writer, now func() time.Time) *Tracer

NewTracer builds a Tracer writing transaction blocks to w (e.g. os.Stderr). now is the clock used for the transaction timestamp; pass time.Now in production.

Jump to

Keyboard shortcuts

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