Documentation
¶
Overview ¶
Package camada is camada for Go: inline enforcement of your tenant snapshot (ordered custom rules, then allow, block, challenge), a first-party proof-of-work challenge and beacon, app-context events, and batched wire events shipped off the request path. Fails open by design.
Quickstart (env: CAMADA_KEY, plus CAMADA_INGEST_URL in dev):
mux := http.NewServeMux()
http.ListenAndServe(":8080", camada.Handler(mux))
The engine here is the host-neutral request handling the net/http adapter (handler.go) delegates to — the Go twin of camada-python's engine.py. An adapter turns its request into a Req, asks WantsBody and reads at most that many bytes, then calls Handle: an Answer means camada fully answered the request (block, challenge, verify, beacon endpoints); a Passed means run the app, stamp the rid header and session cookie on its response, and call OnFinish(status) once when it is done. Everything runs inside the fail-open envelope: a camada bug must never 5xx the customer, and CAMADA_DISABLED=1 bypasses the SDK entirely.
Index ¶
- Constants
- func BodyShape(obj any) map[string]int
- func Handler(next http.Handler) http.Handler
- func HashUserID(userID, ingestToken string) string
- func LogRateLimited(err any)
- func ParseIP4(s string) (uint32, bool)
- func ParseKey(key string) (ingest, snap string, ok bool)
- func ResolveClientIP(peer, xff string, cfg *TrustedProxy) string
- func ScriptTag(r *http.Request) string
- func ScrubQuery(query string) string
- func ScrubURL(url string) string
- func ServeChallenge(w http.ResponseWriter, r *http.Request) bool
- func SetLogger(l *log.Logger)
- func Track(r *http.Request, event, user string)
- type Answer
- type Camada
- func (c *Camada) Disabled() bool
- func (c *Camada) Handle(req *Req, body []byte) (a *Answer, p *Passed)
- func (c *Camada) Handler(next http.Handler) http.Handler
- func (c *Camada) NowMS() int64
- func (c *Camada) ScriptTag(ctx *Ctx) string
- func (c *Camada) ServeChallenge(ctx *Ctx) (a *Answer)
- func (c *Camada) Stop(ctx context.Context)
- func (c *Camada) Track(ctx *Ctx, event, user string)
- func (c *Camada) WantsBody(method, path string) (int, bool)
- type Ctx
- type Env
- type HTTPRequest
- type HTTPResponse
- type Header
- type Options
- type Passed
- type RemoteConfig
- type Req
- type Transport
- type TrustedProxy
- type Words
Constants ¶
const ( ScriptPath = "/_cam/b.js" FPPath = "/_cam/fp" FPMax = 32 * 1024 // matches the server's /fp cap: never accept what ingest will 413 ChallengePath = "/__camada/challenge" BodyMax = 4 * 1024 // the verify form is ~120 bytes; anything larger is not ours SessionCookie = "_sfp" // same cookie as the edge collector: sid/ns comparable across taps SessionMaxAge = 2592000 // 30 days )
const DefaultIngestURL = "https://in.camada.app"
DefaultIngestURL is the production ingest; CAMADA_INGEST_URL overrides it.
const DefaultRefresh = snapshot.DefaultRefresh
The snapshot client owns these defaults; the root re-exports them so one value exists.
const DefaultSnapshotVersion = snapshot.DefaultSnapshotVersion
5 carries the tenant's ordered custom rules (§D3); a tenant without one is answered with the next container down.
const KillSwitchEnv = "CAMADA_DISABLED"
const SDKID = "@camada/go/" + Version
SDKID rides every snapshot poll and event batch as x-camada-sdk.
const TAP = "sdk-go"
Tap identifier this SDK claims on the wire. The server validates against its own enum and derives the capability mask itself (edge-analyst src/capabilities.js): an SDK can never grant itself capability bits, only name its position — and an unknown name is silently read as a proxy, so this literal is load-bearing.
const Version = "0.2.0"
The SDK's wire identity (SDK-03): x-camada-sdk: @camada/go/<version>. This literal is the single source: the git tag is the release version, and the sibling drift guards (camada-backend, camada-web, camada-mkt) parse this file the way they parse a package.json. Plain X.Y.Z only, and @camada/go must stay in the analyst's SDK_NAMES: its sdkId drops anything else.
Variables ¶
This section is empty.
Functions ¶
func BodyShape ¶
BodyShape is a decoded JSON body's field names and sizes, never its values; nil for a non-object.
func Handler ¶
Handler wraps an http.Handler with the default engine, built from the environment on the first request (so nothing polls before the app serves).
func HashUserID ¶
HashUserID is the per-tenant pseudonym track() ships: HMAC-SHA256 under the ingest token, 32 hex chars.
func LogRateLimited ¶
func LogRateLimited(err any)
LogRateLimited reports a suppressed error the way the SDK does: at most one line a minute.
func ParseIP4 ¶
ParseIP4 reads a dotted quad into a uint32; ok is false when the string is not a plain IPv4 address.
func ParseKey ¶
ParseKey splits CAMADA_KEY, `<ingest_token>.<snap_token>`; ok is false without both halves.
func ResolveClientIP ¶
func ResolveClientIP(peer, xff string, cfg *TrustedProxy) string
ResolveClientIP is the client IP from the socket peer and X-Forwarded-For per the trusted-proxy config. The result is always a valid ip or "" (no address), in every mode (C6 v3). Anything unresolvable falls back to the peer.
XFF entries are trimmed of SP/HTAB only, read without a port (`ip:port`, `[v6]:port`, as Azure writes them) and with `::ffff:a.b.c.d` reduced to `a.b.c.d`, in every mode. The peer is unmap(peer) when that is a valid ip, else "": a non-empty peer that is not an ip (a socket path, `@`, `ip:port`, a zone id) could not be read, and the result falls back to "", never to the XFF walk. The peer is never trimmed or port-stripped.
`cidrs` (also what the dashboard's Cloudflare option stores) walks the chain `[...xff, peer]` from the right: if the peer is not a trusted proxy, the request did not come through one and the peer IS the client; XFF is ignored, since anyone hitting the origin directly can write it. A loopback peer (127.0.0.0/8, ::1, ::ffff:127.x.x.x) counts as a trusted hop: it is a local front end or sidecar (App Engine's is always 127.0.0.1), and no remote client can be one. Loopback is trusted only as the peer, never as an XFF entry; an XFF entry that is not an ip is never a trusted hop. With no peer at all (peer "": a unix-socket listener, see Handler) the walk runs over XFF alone: such a host trusts its network edge to have written XFF, and the app must not be reachable around that edge. `hops` and `vercel` trust XFF by definition and ignore the peer.
func ScriptTag ¶
ScriptTag is the first-party beacon tag for this request's HTML ("" when the beacon is off).
func ScrubQuery ¶
ScrubQuery replaces credential-looking query parts with ~r, preserving structure and order (C6 v3): names and values are percent-decoded before testing, a bare part or a value holding a nested query with a credential segment is replaced whole, and the output keeps the raw text.
func ScrubURL ¶ added in v0.1.4
ScrubURL is a URL (a Referer) with its query and fragment run through ScrubQuery; the path is kept.
func ServeChallenge ¶
func ServeChallenge(w http.ResponseWriter, r *http.Request) bool
ServeChallenge writes the proof-of-work page for a route you gate yourself and reports true; false — write your own response — once the browser holds a valid _cch, when the client cannot be identified (fail open), or when the request is not a page load (it is signalled instead: x-camada-challenge: 1, no-store, and a `chs: 1` event).
func SetLogger ¶
SetLogger routes the SDK's one-line-a-minute error report somewhere other than log.Default().
Types ¶
type Camada ¶
type Camada struct {
ScriptPath, FPPath, ChallengePath string
Env *Env // nil when unconfigured: the engine is inert
Snap *snapshot.Client // nil when inert or killed at boot
Queue *events.Queue
Kit *challenge.Kit
// contains filtered or unexported fields
}
Camada is the engine: one per process, built from the environment.
func Configure ¶
Configure replaces the default engine (stopping the old one) — for tests and explicit wiring.
func Default ¶
func Default() *Camada
Default is the lazy singleton wired from the process environment on first use: what Handler(next) and the app helpers share when no engine was handed in.
func New ¶
New builds an engine eagerly: the snapshot poll starts now (timer mode) and the request that arrives first is answered cold unless you wait for the boot poll (see the README's warm-up).
func (*Camada) Disabled ¶
Disabled is true when unconfigured or when CAMADA_DISABLED=1 (read per request).
func (*Camada) Handle ¶
Handle decides one request; exactly one of the results is non-nil, and it never panics. `body` is the request body when WantsBody asked for one; nil when the adapter refused to read it (declared or actual size over the cap) or was not asked; []byte{} when it was empty.
func (*Camada) ScriptTag ¶
ScriptTag is the first-party beacon tag with the request's rid, for HTML templates; "" when the beacon is off or the SDK is inert.
func (*Camada) ServeChallenge ¶
ServeChallenge serves the challenge for this request on demand — for a route the app wants to gate itself. nil when the client already holds a valid _cch (render your own page), or when the client cannot be identified (fail open). A non-HTML request is never challenged: it is signalled (header and `chs: 1` event) and nil is returned, so the app answers.
func (*Camada) Stop ¶
Stop ends the poll and flush goroutines and drains pending events, for at most 500 ms or until ctx is done, whichever comes first. No signal handlers are installed: call it from your own shutdown path (`defer cam.Stop(context.Background())`).
type Ctx ¶
type Ctx struct {
RID string
SID string
IP string // "" when the client could not be identified
// contains filtered or unexported fields
}
Ctx is the per-request context the app helpers read (ScriptTag, Track, ServeChallenge).
func FromRequest ¶
FromRequest is the camada context of a request the middleware ran for; nil otherwise.
type Env ¶
type Env struct {
IngestToken string
SnapToken string
Secret string // HMAC key for the challenge nonce/cookie — never leaves the process
IngestURL string
SnapshotURL string
Serverless bool
TrustedProxy *TrustedProxy // nil = defer to server-delivered config
}
Env is the resolved configuration; credentials come from the environment only.
func ResolveEnv ¶
ResolveEnv reads the CAMADA_* variables from a map (tests, or an app's own config); nil (the SDK stays inert, one log line) rather than an error on bad config.
type HTTPRequest ¶
HTTPRequest is one call to the analyst, as the Transport seam sees it.
type HTTPResponse ¶
HTTPResponse is the analyst's answer; Status 0 means the request never got one.
func HTTPTransport ¶
func HTTPTransport(req HTTPRequest) HTTPResponse
HTTPTransport is the production transport over net/http: 2-3 s deadlines, transparent gzip, never panics (a network failure is a status-0 response).
type Options ¶
type Options struct {
Env map[string]string // where CAMADA_* are read from; nil = the process environment
Transport Transport // the HTTP func that reaches the analyst (tests inject a fake)
Refresh time.Duration // poll cadence; 0 = server-steered, set = pinned
ScriptPath string // the beacon script endpoint; default /_cam/b.js
FPPath string // the beacon post endpoint; default /_cam/fp (same directory as ScriptPath)
Challenge *bool // serve the proof-of-work page for challenge verdicts; nil = true (CAMADA_CHALLENGE=0 also off)
ChallengePath string // where the page posts its solution; default /__camada/challenge
SnapshotVersion int // 5 (default) asks for your custom rules; 4 the sides only; 3 opts out of both
}
Options configure New; everything credential-shaped comes from the environment.
type Passed ¶
Passed: run the app. RID/SetCookie ride the response; Ctx is stored on the host request; OnFinish(status) is called once at the end. The inert Passed has every field zero.
type RemoteConfig ¶
type RemoteConfig = config.RemoteConfig
RemoteConfig is what GET /snapshot hands back in x-camada-config (whitelisted server-side).
type Req ¶
type Req struct {
Method string
Path string // no query
Query string // with the leading '?', or ""
Host string
HTTPVersion string // "1.1", "2.0"; "" when unknown
Peer string // the socket peer the host vouches for; "" when there is none (no socket, a unix-socket listener)
// PeerUnreadable: the host has a socket but could not read its peer for this request; the ip is then "" (C6 v3).
PeerUnreadable bool
HTTPS bool
Headers []Header
Route string // the matched route pattern, when the host knows it at finish time
}
Req is what an adapter hands the engine. Header names are lower-cased; the list keeps the order the host gave (net/http normalises headers into a map, so handler.go sorts them).
type Transport ¶
Transport performs one HTTPRequest. The engine, the snapshot client and the event queue all go through it, so tests inject an in-process fake and production uses HTTPTransport.
type TrustedProxy ¶
type TrustedProxy = config.TrustedProxy
TrustedProxy is the tenant's trusted-proxy config: {mode: none} | {mode: hops, hops: N} | {mode: cidrs, cidrs: [...]} | {mode: vercel}. CAMADA_TRUSTED_PROXY overrides it locally.
func ParseTrustedProxyEnv ¶
func ParseTrustedProxyEnv(v string) *TrustedProxy
ParseTrustedProxyEnv reads CAMADA_TRUSTED_PROXY: none | vercel | hops:N | cidrs:a,b. Unset or malformed is nil — defer to the server-delivered config, never trust.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package challenge is the SDK-served proof-of-work challenge (contracts §D2), ported from @camada/core src/challenge/{format,verify,page}.ts: wire constants and pure helpers here, the HMAC/SHA-256 kit in verify.go, the self-contained page in page.go.
|
Package challenge is the SDK-served proof-of-work challenge (contracts §D2), ported from @camada/core src/challenge/{format,verify,page}.ts: wire constants and pure helpers here, the HMAC/SHA-256 kit in verify.go, the self-contained page in page.go. |
|
Package events builds the wire events and ships them in batches.
|
Package events builds the wire events and ships them in batches. |
|
internal
|
|
|
beacon
Package beacon is the first-party beacon: @camada/browser's dist/auto.global.js vendored by `go run ./scripts/sync-beacon` and embedded, so the SDK serves it at /_cam/b.js with no runtime file read.
|
Package beacon is the first-party beacon: @camada/browser's dist/auto.global.js vendored by `go run ./scripts/sync-beacon` and embedded, so the SDK serves it at /_cam/b.js with no runtime file read. |
|
config
Package config holds the configuration shapes shared by the snapshot client and the engine: ParseKey splits CAMADA_KEY, and RemoteConfig is what GET /snapshot hands back in x-camada-config (whitelisted server-side).
|
Package config holds the configuration shapes shared by the snapshot client and the engine: ParseKey splits CAMADA_KEY, and RemoteConfig is what GET /snapshot hands back in x-camada-config (whitelisted server-side). |
|
guarded
Package guarded is the fail-open envelope: a camada bug must never 5xx the customer.
|
Package guarded is the fail-open envelope: a camada bug must never 5xx the customer. |
|
ipparse
Package ipparse holds the allocation-free IP parsers, ported 1:1 from edge-analyst src/blocklist.js through @camada/core src/snapshot/ipparse.ts (the reference the conformance fixtures are generated from).
|
Package ipparse holds the allocation-free IP parsers, ported 1:1 from edge-analyst src/blocklist.js through @camada/core src/snapshot/ipparse.ts (the reference the conformance fixtures are generated from). |
|
redact
Package redact is redaction, non-configurable-off.
|
Package redact is redaction, non-configurable-off. |
|
testutil
Package testutil holds the test doubles the suite shares: the golden fixture readers and the in-process fake analyst.
|
Package testutil holds the test doubles the suite shares: the golden fixture readers and the in-process fake analyst. |
|
transport
Package transport is the one HTTP seam.
|
Package transport is the one HTTP seam. |
|
scripts
|
|
|
sync-beacon
command
Re-vendors @camada/browser's auto build into internal/beacon (auto.global.js, version.txt, sha256.txt), which the SDK embeds and serves at /_cam/b.js with no runtime file read.
|
Re-vendors @camada/browser's auto build into internal/beacon (auto.global.js, version.txt, sha256.txt), which the SDK embeds and serves at /_cam/b.js with no runtime file read. |
|
Package snapshot is the BLK snapshot parser (v3, v4, v5) and matcher, ported from @camada/core src/snapshot/parse.ts and match.ts, themselves ports of edge-analyst src/blocklist.js (the reference implementation).
|
Package snapshot is the BLK snapshot parser (v3, v4, v5) and matcher, ported from @camada/core src/snapshot/parse.ts and match.ts, themselves ports of edge-analyst src/blocklist.js (the reference implementation). |