ws

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package ws is the WebSocket hub for live scoreboard updates: a connection registry, throttled latest-wins broadcast, ping/pong keepalive, and graceful drain on shutdown. It broadcasts apigen.Scoreboard — the SAME wire type the REST endpoint returns — so a client that reconciles WS pushes against a REST fetch sees byte-identical payloads by construction.

Index

Constants

View Source
const (
	FrameHello      = "hello"       // greeting; carries the current frozen flag
	FrameScoreboard = "scoreboard"  // a scoreboard snapshot (apigen.Scoreboard)
	FrameEventPhase = "event.phase" // an event phase transition
)

Frame types emitted on the scoreboard WebSocket. This is the single source of truth for the wire protocol's frame names — the hub references these constants instead of string literals, and frame_types.json is generated from FrameTypes.

The dashboard's ScoreboardSocket must know exactly this set: a frame added here that the client does not branch on is parsed and silently dropped. A contract test on each side asserts its known set against frame_types.json, so adding a backend frame without teaching the dashboard about it fails CI (4a-ii).

Variables

FrameTypes is the authoritative set of frame names, sorted for a stable contract file. Keep frame_types.json in sync (the ws contract test rewrites it with UPDATE_GOLDEN=1).

Functions

This section is empty.

Types

type Hub

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

Hub owns the set of live connections and fans out broadcasts.

func NewHub

func NewHub(log *slog.Logger) *Hub

NewHub builds a hub with the default connection limits. Call Run in a goroutine to start it; use SetLimits/SetKeyResolver before serving to override the defaults.

func (*Hub) BroadcastPhase

func (h *Hub) BroadcastPhase(phase string)

BroadcastPhase notifies clients of an event phase transition (not throttled; phase changes are rare and must arrive promptly).

func (*Hub) BroadcastScoreboard

func (h *Hub) BroadcastScoreboard(snap apigen.Scoreboard)

BroadcastScoreboard queues a scoreboard snapshot for delivery (throttled). It takes apigen.Scoreboard so the "data" it emits is byte-identical to the REST body.

func (*Hub) Handler

func (h *Hub) Handler() http.HandlerFunc

Handler upgrades the request to a WebSocket, registers the connection, and pumps broadcasts until the client disconnects or the server shuts down. The endpoint is public (no per-connection auth), so admission control — connection caps and a handshake rate limit — is what keeps an unauthenticated client from opening connections until the process dies. A rejected handshake returns 429 BEFORE the upgrade, so it never allocates a connection or touches the hub; existing clients are unaffected.

func (*Hub) Run

func (h *Hub) Run(ctx context.Context)

Run is the hub's single-goroutine event loop. It returns when ctx is cancelled, closing all connections with code 1001 (going away).

func (*Hub) SetKeyResolver

func (h *Hub) SetKeyResolver(fn func(*http.Request) string)

SetKeyResolver installs the admission-key resolver: authenticated connections should key on the user id and anonymous ones on the (proxy-aware) client IP, so a shared NAT of logged-in players is not throttled as one IP. Call before serving. Nil (the default) keys on the socket peer IP.

func (*Hub) SetLimits

func (h *Hub) SetLimits(l Limits)

SetLimits overrides the connection caps and handshake rate limit. Call before serving.

type Limits

type Limits struct {
	MaxConns        int           // global cap on live connections (0 = unlimited)
	MaxConnsPerKey  int           // cap on live connections per admission key (0 = unlimited)
	HandshakeBurst  int           // max handshake attempts per key within HandshakeWindow (0 = unlimited)
	HandshakeWindow time.Duration // sliding window for the handshake rate limit
}

Limits bounds the public, unauthenticated WebSocket endpoint. Without them a single client can open connections until the process dies — each live connection holds two goroutines (read/write pumps) and a send buffer — and CTF participants are exactly the population that will try. A zero field disables that particular limit.

The per-client caps and the handshake rate are keyed by an admission KEY, not raw IP: authenticated connections key on the user id (so a whole campus/venue NAT of logged-in players is not squeezed through one IP budget — GitHub issue #1's shared-IP class), and only anonymous connections fall back to the client IP.

Per-key keying keyed on the user is bypassable: an attacker who registers N accounts gets N × MaxConnsPerKey connections. Registration limits bound N only loosely (and issue #1 wants those raised for shared-IP venues), so the per-key cap is a fairness limit, not the DoS backstop. The GLOBAL cap (MaxConns, clamped to the shared file-descriptor budget by the fd accountant — internal/fdbudget — at startup) is the real backstop against process exhaustion.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits protects a single-server deployment without squeezing a large event. Generous per-key budgets plus per-user keying mean a NAT full of logged-in players is unaffected; the caps still stop any single key — and the global ceiling any fleet — from exhausting the process. Operators can override every value (OSCTF_WS_*).

Jump to

Keyboard shortcuts

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