Documentation
¶
Overview ¶
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Index ¶
- Constants
- Variables
- type Conn
- func Dial(ctx context.Context, rawURL string, opts DialOptions) (*Conn, error)
- func NewConn(c net.Conn, opts Options) *Conn
- func Upgrade(w http.ResponseWriter, r *http.Request, opts Options) (*Conn, error)deprecated
- func UpgradeStrict(w http.ResponseWriter, r *http.Request, opts Options) (*Conn, error)
- func (c *Conn) Close() error
- func (c *Conn) Context() context.Context
- func (c *Conn) MaxMessage() int
- func (c *Conn) NetConn() net.Conn
- func (c *Conn) ReadMessage() (byte, []byte, error)
- func (c *Conn) SetContext(ctx context.Context) *Conn
- func (c *Conn) WithContext(ctx context.Context) *Conndeprecated
- func (c *Conn) WriteMessage(op byte, payload []byte) error
- type DialOptions
- type Limiter
- type LimiterConfig
- type Options
- type Snapshot
Constants ¶
const ( DefaultMaxConnections = 1024 DefaultMaxConnectionsPerIP = 4 DefaultUpgradeRatePerMin = 60 )
Defaults exported so tests + per-service main.go bootstraps can reference them without re-spelling the numbers.
const ( OpContinuation byte = 0x0 OpText byte = 0x1 OpBinary byte = 0x2 OpClose byte = 0x8 OpPing byte = 0x9 OpPong byte = 0xA )
RFC 6455 §11.8 frame opcodes. Exposed so callers of ReadMessage / WriteMessage can compare/emit text/binary/control frames without hard-coding magic numbers.
const SubprotocolSentinel = "apic.ws.v1"
SubprotocolSentinel is a reserved, non-secret subprotocol the generated WS clients always offer so the server can echo a Sec-WebSocket-Protocol value on the 101 even for endpoints that configure no Protocols. Strict clients (notably node-24's undici WebSocket) FAIL the handshake when they offer any subprotocol but the server selects none (L-19 bug 2). Echoing this constant satisfies them without ever echoing the SEC-0011 apic.auth.* credential markers. It carries no secret and is safe to expose in logs/devtools.
Variables ¶
var ( ErrFrameTooLarge = errors.New("wsx: frame exceeds configured size") ErrInvalidOpcode = errors.New("wsx: invalid websocket opcode") ErrInvalidConfig = errors.New("wsx: invalid configuration") ErrAuthFailed = errors.New("wsx: authentication failed") // ErrOriginNotAllowed is returned by UpgradeStrict when the request // Origin (or the absence of one) is rejected by the configured // allowlist. SEC-0008: the strict upgrade path is fail-closed, so // callers that omit OriginAllowlist and AllowAnyOrigin always see // this error. ErrOriginNotAllowed = errors.New("wsx: origin not allowed") // ErrBadUpgradeHeaders is returned when the Connection or Upgrade // request headers do not match the values required by RFC 6455. ErrBadUpgradeHeaders = errors.New("wsx: bad upgrade headers") // ErrUnsupportedVersion is returned when Sec-WebSocket-Version is // not "13" (the only version supported by this implementation). ErrUnsupportedVersion = errors.New("wsx: unsupported websocket version") // ErrNoHijack is returned when the supplied http.ResponseWriter // does not implement http.Hijacker, so the upgrade cannot proceed. ErrNoHijack = errors.New("wsx: response writer does not support hijack") // ErrInvalidWebSocketKey is returned when the Sec-WebSocket-Key // header is missing, malformed, or not 16 bytes after base64 decode. ErrInvalidWebSocketKey = errors.New("wsx: invalid sec-websocket-key") // ErrForbiddenOrigin is returned by Upgrade (the legacy fail-open // entry point) when the Origin header is non-empty and does not // match the configured allowlist. ErrForbiddenOrigin = errors.New("wsx: forbidden origin") // ErrInvalidUTF8 is returned when a text frame payload (op 0x1) or // a reassembled text message contains invalid UTF-8. ErrInvalidUTF8 = errors.New("wsx: invalid utf-8 in text frame") // ErrReservedBitsSet is returned when a frame has any of the // reserved bits (RSV1/RSV2/RSV3) set; this implementation does not // negotiate any extensions, so they MUST be zero. ErrReservedBitsSet = errors.New("wsx: reserved bits set on frame") // ErrUnmaskedClientFrame is returned when a server-side reader // receives an unmasked frame and unmasked reads are not permitted // (per RFC 6455, client→server frames must be masked). ErrUnmaskedClientFrame = errors.New("wsx: client frames must be masked") // ErrHandshakeMissingUpgrade is returned by Dial when the server // response is missing the required Upgrade: websocket header. ErrHandshakeMissingUpgrade = errors.New("wsx: websocket handshake failed: missing upgrade header") // ErrHandshakeInvalidAccept is returned by Dial when the server's // Sec-WebSocket-Accept header does not match the expected value. ErrHandshakeInvalidAccept = errors.New("wsx: websocket handshake failed: invalid accept key") // ErrControlTooLarge is returned when a control frame (close/ping/pong) // declares a payload longer than 125 bytes, which RFC 6455 §5.5 // forbids. SEC-0055. ErrControlTooLarge = errors.New("wsx: control frame payload exceeds 125 bytes") // ErrFragmentedControl is returned when a control frame arrives with // FIN=0; RFC 6455 §5.5 forbids fragmenting control frames. SEC-0055. ErrFragmentedControl = errors.New("wsx: fragmented control frame") // ErrTooManyControlFrames is returned when more than maxPendingControl // ping/pong frames arrive interleaved within a single fragmented data // message; the pending queue is bounded so a peer cannot grow it // without limit mid-fragmentation. SEC-0055. ErrTooManyControlFrames = errors.New("wsx: too many interleaved control frames") // ErrTooManyFragments is returned when a single data message is split // across more than maxFragments continuation frames. The cumulative // byte budget (maxMessage) already bounds total size, but a peer can // stay under it while sending an unbounded number of tiny (even // zero-length) continuation frames, forcing per-frame parse work // indefinitely. The fragment cap fails closed (protocol error, 1009 // "message too big" semantics). T-02. ErrTooManyFragments = errors.New("wsx: too many continuation fragments in message") // ErrPingFlood is returned when a peer streams more than maxPingStream // standalone (non-interleaved) ping/pong control frames in a row // without any intervening data message. A standalone ping stream is // otherwise unbounded: ReadMessage returns each ping to the caller, but // a caller in a read loop that ignores pings would spin forever on a // flood. The cap fails closed. T-02. ErrPingFlood = errors.New("wsx: too many consecutive control frames") )
Sentinel errors for WebSocket runtime. 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().
var ( // ErrTotalCap is returned when the pod-wide concurrent-WS cap // (Config.MaxConnections / DefaultMaxConnections) is reached. ErrTotalCap = errors.New("ws upgrade rejected: pod connection cap") // ErrPerIPCap is returned when the per-remote-IP concurrent-WS // cap (Config.MaxConnectionsPerIP / DefaultMaxConnectionsPerIP) // is reached. ErrPerIPCap = errors.New("ws upgrade rejected: per-IP connection cap") // ErrRateCap is returned when the per-IP rolling-minute upgrade // rate cap (Config.UpgradeRatePerMin / DefaultUpgradeRatePerMin) // is exceeded. ErrRateCap = errors.New("ws upgrade rejected: per-IP rate cap") )
Sentinel errors returned by Limiter.Acquire on rejection. Each rejection class is independently identifiable via errors.Is so the apic wrapper can attribute the rejection (Retry-After value, OTel reason label, obsx counter dimension).
Functions ¶
This section is empty.
Types ¶
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn is a minimal WebSocket text/binary connection.
func NewConn ¶
NewConn wraps an existing net.Conn as a WebSocket connection. Unlike Upgrade, this skips the HTTP handshake — use it for in-memory pipes or pre-established connections.
func Upgrade
deprecated
Upgrade performs an RFC6455 upgrade and returns a Conn.
Deprecated: Upgrade defaults to fail-open when OriginAllowlist is nil and CheckOrigin is false, which historically caused SEC-0008 (cross-origin WS hijack on privileged surfaces). New code MUST use UpgradeStrict, which fails closed unless either OriginAllowlist is non-empty or Options.AllowAnyOrigin is true. The default for the next major version will flip to UpgradeStrict's semantics; until then Upgrade is retained for backward compatibility.
func UpgradeStrict ¶
UpgradeStrict is the SEC-0008 fail-closed entry point for RFC 6455 upgrades. Unlike Upgrade, the default behavior when OriginAllowlist is empty/nil is to REJECT the request with HTTP 403 and ErrOriginNotAllowed.
Origin policy:
- opts.AllowAnyOrigin == true : every Origin (including absent) is OK.
- opts.SameOriginOnly == true : the same-origin URL (synthesised from r.TLS + r.Host) is added to the effective allowlist. Composes additively with OriginAllowlist so an operator can broaden to cross-origin trusted deployments without losing the same-origin default.
- len(opts.OriginAllowlist) > 0 : Origin must match the list (case-insensitive). A literal "*" entry is NOT a wildcard here (T-05); match-all requires opts.AllowAnyOrigin. Empty Origin is permitted only when opts.AllowEmptyOrigin is true.
- all unset : ErrOriginNotAllowed (HTTP 403).
Privileged surfaces (MCP WS, REST WS, GraphQL WS) MUST use UpgradeStrict. The legacy Upgrade is retained for backward compatibility; a future major version will switch the default.
func (*Conn) Close ¶
Close sends a close frame then closes the connection.
Close is idempotent and safe to call concurrently: redundant calls are no-ops for the ping-loop shutdown and return the underlying conn's Close result. io.Closer permits repeated Close, and double-close is reachable in practice — pkg/mcpx/engine_ws.go closes on a write error AND again via a deferred Close, and handlers idiomatically write `defer conn.Close()` alongside an explicit close. N-01: this previously did a bare close(c.pingStop), which panicked ("close of closed channel") on the second call and killed the handler goroutine.
func (*Conn) Context ¶
Context returns the context attached to this connection. For server-side upgrades this is the originating *http.Request context, so handlers can observe cancellation/deadlines and read context values (tracing, request-id, auth claims) without needing the original *http.Request. Returns context.Background() when no context has been attached (e.g. NewConn-wrapped pipes without an explicit SetContext call).
func (*Conn) MaxMessage ¶
MaxMessage returns the configured maximum message size in bytes.
func (*Conn) NetConn ¶
NetConn exposes the underlying network connection for advanced integrations (e.g. handing the raw byte stream to a protocol handler such as the generated GraphQL subscription path).
Contract (QG-079): the returned net.Conn observes the byte stream from the connection's CURRENT read position. The RFC 6455 handshake hijacks the HTTP connection with a bufio.Reader that may already hold frames the client pipelined in the same TCP segment as the upgrade request; those buffered bytes are drained first, after which reads fall through to the underlying connection. When nothing is buffered the underlying net.Conn is returned unwrapped, preserving identity for callers that type-assert (e.g. *net.TCPConn). Writes, deadlines, Close, and the address methods always operate on the underlying connection. After calling NetConn the caller owns the read side: do not mix it with ReadMessage on the same Conn.
func (*Conn) ReadMessage ¶
ReadMessage reads a full message (reassembling fragments). Returns opcode and payload.
func (*Conn) SetContext ¶ added in v0.17.0
SetContext attaches ctx to the connection so future Context() calls return it. Intended for callers that build a Conn via NewConn (for pipes, tests) or that want to override the upgrade context with a derived/spanned one before invoking the business handler.
N-13: named Set, not With — unlike http.Request.WithContext (and the repo's own Go convention for WithX constructors/options), this MUTATES the receiver in place rather than returning an independent copy; it also returns c itself (not a new *Conn) purely for call-site chaining. A prior "WithContext" name invited exactly that (incorrect) copy-on-write assumption.
func (*Conn) WithContext
deprecated
WithContext is a back-compat alias for SetContext (Q-3).
Deprecated: use SetContext. The rename (N-13) made explicit that this method mutates the receiver in place and returns c itself rather than an independent copy -- the opposite of the WithX naming convention used elsewhere in this repo and of http.Request.WithContext, both of which return a copy. WithContext is retained, unmodified in behavior, so callers written against the pre-N-13 name still compile.
type DialOptions ¶
type DialOptions struct {
Headers http.Header
Origin string
Protocols []string
HandshakeTimeout time.Duration
MaxFrame int
MaxMessage int
ReadTimeout time.Duration
WriteTimeout time.Duration
}
DialOptions controls client-side WebSocket dialing.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is safe for concurrent use across many goroutines.
func NewLimiter ¶
func NewLimiter(cfg LimiterConfig) *Limiter
NewLimiter builds a Limiter. cfg is canonicalised in-place (zero-value fields are filled with defaults).
func ProcessLimiter ¶
func ProcessLimiter() *Limiter
ProcessLimiter returns the lazily-built process-singleton Limiter, configured from environment variables (LimiterConfigFromEnv). All WS endpoints in a given binary share the same limiter so a single misbehaving client cannot fan its WS retries across paths to multiply its effective cap.
func (*Limiter) Acquire ¶
Acquire records an upgrade attempt from remoteAddr (raw "host:port" or bare host). Returns nil if the upgrade is allowed; a sentinel error from {ErrTotalCap, ErrPerIPCap, ErrRateCap} otherwise. On non-nil return, the caller MUST NOT call Release.
Acquire is the only mutator in the hot path. Callers in the apic-emitted wrapper invoke Acquire BEFORE wsx.UpgradeStrict so rejection costs only an HTTP 503 response, not the full handshake.
func (*Limiter) Config ¶
func (l *Limiter) Config() LimiterConfig
Config returns the canonicalised LimiterConfig the limiter was built with. Convenience accessor for the template wrapper that needs to compute Retry-After seconds from the rate window.
func (*Limiter) RateWindow ¶
RateWindow returns the rolling window used by the rate cap. The apic-emitted wrapper uses this to compute a Retry-After hint when ErrRateCap fires.
type LimiterConfig ¶
type LimiterConfig struct {
// MaxConnections is the total concurrent-WS cap for this pod.
// Zero defaults to DefaultMaxConnections; negative disables.
MaxConnections int
// MaxConnectionsPerIP caps concurrent WS connections from a
// single remote IP. Zero defaults to DefaultMaxConnectionsPerIP;
// negative disables.
MaxConnectionsPerIP int
// UpgradeRatePerMin caps the number of NEW upgrades from a single
// remote IP per rolling minute. Defends against misbehaving
// clients that retry an upgrade in a tight loop. Zero defaults to
// DefaultUpgradeRatePerMin; negative disables.
UpgradeRatePerMin int
// RateWindow is the rolling window across which UpgradeRatePerMin
// is enforced. Zero defaults to one minute. Exported so tests can
// shrink the window without having to wait wall-clock time.
RateWindow time.Duration
}
LimiterConfig configures NewLimiter.
func LimiterConfigFromEnv ¶
func LimiterConfigFromEnv() LimiterConfig
LimiterConfigFromEnv reads WS_MAX_CONNECTIONS, WS_MAX_CONNECTIONS_PER_IP, and WS_UPGRADE_RATE_PER_MIN from the process environment. Missing vars (or invalid integers) fall through to the defaults declared above -- a misconfigured cap should not take a pod down at startup.
type Options ¶
type Options struct {
OriginAllowlist []string
CheckOrigin bool
// AllowAnyOrigin opts in to permitting any value of the Origin
// request header (including absent). Required by UpgradeStrict to
// disable the fail-closed default. Use only for non-browser
// surfaces (CLIs, server-to-server callers) where the origin gate
// is not the right protection. SEC-0008.
AllowAnyOrigin bool
// AllowEmptyOrigin opts in to permitting requests with no Origin
// header set. Useful for non-browser callers under UpgradeStrict
// that nevertheless want a strict allowlist for browsers. SEC-0008.
AllowEmptyOrigin bool
// SameOriginOnly tells UpgradeStrict to synthesise a same-origin
// entry at request time from r.TLS + r.Host and treat it as part of
// the effective allowlist. The synthesised origin is "https://r.Host"
// when r.TLS != nil and "http://r.Host" otherwise — matching the
// Origin header the browser computes for a same-origin upgrade. The
// flag composes additively with OriginAllowlist: an SPA hosted on
// the same origin as the API always works without configuration, AND
// an operator can broaden via OriginAllowlist for cross-origin
// deployments (CDN front-end on a separate host from the API). The
// CSWSH defense-in-depth pattern from geode-ui SEC issue 265.
SameOriginOnly bool
// TrustForwardedProxy opts the same-origin synthesiser into
// reading `X-Forwarded-Proto` / `X-Forwarded-Host` headers from
// the request. A-S4 hardening (2026-05-28): defaults to FALSE so
// a deployment without a header-scrubbing reverse proxy cannot be
// tricked into accepting an attacker-supplied `X-Forwarded-Host`
// as the same-origin URL. Set to true only when a TLS-terminating
// upstream proxy (nginx, ALB, Cloudflare, etc.) controls those
// headers — and only when that proxy strips ingress copies first.
// When false (the default), sameOriginURL derives proto from
// r.TLS and host from r.Host alone.
TrustForwardedProxy bool
Protocols []string // allowed subprotocols; first match is selected
MaxFrame int // max single frame payload
MaxMessage int // max reassembled message
PingInterval time.Duration
ReadTimeout time.Duration // per-read deadline; 0 = none
WriteTimeout time.Duration // per-write deadline; 0 = none
// MaxConnections is the per-pod concurrent-WS cap for this
// endpoint. Plumbed into the generated RegisterGeneratedWS
// wrapper (GAP-0075 part 2) so consumers can express the cap in
// the websocket[].max_connections JSON config field. Zero means
// "use DefaultMaxConnections"; negative disables the knob.
// pkg/wsx.UpgradeStrict itself does NOT consult this field --
// the wrapper builds a *Limiter at registration time and gates
// the upgrade before the handshake. The field lives on Options
// purely so the public API surface is consistent and a
// hand-built caller can read it. GAP-0075.
MaxConnections int
// MaxConnectionsPerIP is the per-remote-IP concurrent-WS cap.
// Same semantics as MaxConnections; see notes above. GAP-0075.
MaxConnectionsPerIP int
// UpgradeRatePerMin is the per-remote-IP rolling-minute upgrade
// rate cap. Same semantics as MaxConnections; see notes above.
// GAP-0075.
UpgradeRatePerMin int
}
Options controls WebSocket upgrade and connection behavior.