Documentation
¶
Overview ¶
Package rtc is a WebRTC signaling server: rooms of peers over one core/stream.StateChannel WebSocket per room, addressed relay of offer/answer/ICE frames, ICE server configuration with per-peer time-limited TURN credentials, and cross-replica relay over core/fanout. Signaling only: SDP and ICE candidates cross the Go process as uninterpreted bytes, media never does. The browser half is the `rtc` runtime module (`__gofastr.connectRoom`).
Identity is server-derived. A client names nothing about itself on the wire; every Join field comes from the host's own authorization (cookies, sessions, handler.GetUser) through Config.Authorize. The TURN static-auth-secret never leaves the process: each peer's credential is minted at snapshot time and marshaled for that peer only.
Usage:
app.RegisterPlugin(rtc.New(rtc.Config{
ICEServers: []rtc.ICEServer{{URLs: []string{"stun:stun.example.net:3478"}}},
Authorize: func(r *http.Request) (rtc.Join, error) {
// resolve the room and identity from the app's own auth
return rtc.Join{Room: "support-17", Role: "caller"}, nil
},
}))
Init mounts the endpoint (GET Config.Path, default /__gofastr/rtc) and wires Close into app shutdown; App.Mount wires SetFanout automatically when the app has WithFanout.
Serve skips Authorize when the caller has already authorized the request and passes its own Join.
Index ¶
- Constants
- func PermissionsPolicy(features ...Feature) string
- type Config
- type Feature
- type HTTPError
- type ICEServer
- type Join
- type PeerInfo
- type Signaler
- func (s *Signaler) Close()
- func (s *Signaler) Init(app *framework.App) error
- func (s *Signaler) Kick(room, peerID string) bool
- func (s *Signaler) Mount(rt *router.Router)
- func (s *Signaler) Name() string
- func (s *Signaler) Peers(room string) []PeerInfo
- func (s *Signaler) ReservedEmbedPrefixes() []string
- func (s *Signaler) Rooms() []string
- func (s *Signaler) Serve(w http.ResponseWriter, r *http.Request, join Join)
- func (s *Signaler) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (s *Signaler) SetFanout(f fanout.Fanout) (func(), error)
- type TURN
Constants ¶
const DefaultPath = "/__gofastr/rtc"
DefaultPath is the WebSocket endpoint Mount registers when Config.Path is empty.
Variables ¶
This section is empty.
Functions ¶
func PermissionsPolicy ¶
PermissionsPolicy returns a Permissions-Policy header value that closes geolocation, microphone, camera, and display-capture and opens the named features to the app's own origin ("(self)"). Every feature is written out: an omitted directive is not a closed one (the default allowlist for camera, microphone, and display-capture is self), so a header that only listed what it opened would open what it left out as well. Hand it to middleware.SecurityHeadersConfig.PermissionsPolicy. Example: PermissionsPolicy(Camera, Microphone) == "geolocation=(), microphone=(self), camera=(self), display-capture=()".
Types ¶
type Config ¶
type Config struct {
// ICEServers is the static list every peer receives (STUN servers,
// or TURN entries with long-lived credentials the host accepts).
// Only stun:, stuns:, turn:, turns: URL schemes are allowed; New
// refuses anything else.
ICEServers []ICEServer
// TURN, when set, appends a per-peer time-limited entry.
TURN *TURN
// Authorize decides who joins which room. Required for Mount and
// ServeHTTP; Serve callers pass their own Join and may leave it nil.
// A returned error refuses the upgrade with 403 (or the status
// carried by a *HTTPError). The Join it returns is trusted.
Authorize func(r *http.Request) (Join, error)
// Path is the WebSocket endpoint Mount registers. Default
// "/__gofastr/rtc". Validated like relay.Config.Path: absolute, no
// trailing slash, no traversal, no percent-encoding.
Path string
// MaxPeers caps members per room, local plus remote; default 8. A
// join past the cap is refused with 409 before the upgrade. Mesh
// cost is O(n²) connections; an SFU is a different product.
MaxPeers int
// MaxSignalBytes caps one signaling payload (SDP, ICE candidate);
// default 48 KiB. Larger frames are dropped and the peer closed.
// A camera plus a data channel offers in 3-8 KiB; many codecs,
// simulcast, and several tracks reach 15-25 KiB. The cap plus 1 KiB
// of envelope must fit WS.ReadLimit (at most 64 KiB), or New panics
// rather than accept a cap the socket would cut off first.
MaxSignalBytes int
// MaxStatusBytes caps a peer's status document; default 1 KiB.
MaxStatusBytes int
// MaxFramesPerSecond caps inbound frames per socket (token bucket,
// burst = 2× rate); default 128. Over the cap the socket is closed.
MaxFramesPerSecond int
// MaxSocketsPerUser caps concurrent signaling sockets per principal
// (Join.User) across every room; default 16, the core/stream seat
// parity (SSE broker, CRUD event stream). A principal's next socket
// past the cap displaces its oldest one — the replace-on-rejoin
// policy, applied across rooms — so a reconnecting client always
// wins and one principal can never park unbounded read loops and
// per-room channel goroutines. Zero keeps the default; sockets whose
// host derived no Join.User share one bucket.
MaxSocketsPerUser int
// RoomIdleTTL is how long an empty room's state survives before it
// is dropped; default 1 minute. Rooms are created on first join.
RoomIdleTTL time.Duration
// WS is applied to every upgrade. ConnectionID and OnClose are
// owned by the package and overwritten. Leave zero for defaults
// (ReadLimit is forced to at most 64 KiB either way).
WS stream.WSConfig
Logger *slog.Logger // nil means slog.Default()
}
type Feature ¶
type Feature string
Feature names a browser feature the Permissions-Policy helper opens.
type HTTPError ¶
HTTPError lets Config.Authorize choose the refusal status (401, 404, 410 …) instead of the default 403. ServeHTTP writes Status and Message as the response; anything else about the error is discarded so a refusal leaks nothing beyond what the host chose to say.
type ICEServer ¶
type ICEServer struct {
URLs []string `json:"urls"`
Username string `json:"username,omitempty"`
Credential string `json:"credential,omitempty"`
}
ICEServer is one entry of the RTCConfiguration.iceServers list handed to a peer. JSON tags match the browser's RTCIceServer dictionary.
type Join ¶
type Join struct {
Room string // required; 1-128 bytes, printable ASCII, no whitespace
Role string // optional app label ("caller", "viewer"); 0-32 bytes, same charset
PeerID string // optional stable id; empty means the server mints one per socket
User string // optional identity shown to the room (a user id); empty = anonymous
DisplayName string // optional; 0-64 bytes; control bytes refused
}
Join is the server-derived identity of one connecting peer. Every field comes from the host's own authorization (cookies, sessions, handler.GetUser), never from the wire: a client names nothing about itself except through the host's Authorize.
type PeerInfo ¶
type PeerInfo struct {
ID string `json:"id"`
Role string `json:"role,omitempty"`
User string `json:"user,omitempty"`
DisplayName string `json:"name,omitempty"`
Order int64 `json:"order"`
Status json.RawMessage `json:"status,omitempty"`
}
PeerInfo is a room member as every other member sees it. Order is the join instant in Unix nanoseconds and decides negotiation politeness (see the wire protocol's politeness rule); ties break on ID.
type Signaler ¶
type Signaler struct {
// contains filtered or unexported fields
}
Signaler is one signaling server: the room set, its sockets, and the optional cross-replica fanout lane. Construct with New, register with App.RegisterPlugin. Implements framework.Plugin.
func New ¶
New validates cfg and constructs the Signaler. It panics on invalid configuration with a message prefixed "rtc:": a bad rtc Config is a construction-time programmer error (same posture as relay.New and framework.NewApp's registration panics), not a runtime condition the process should limp along with. Call Close when done, or let Init wire it to app shutdown.
func (*Signaler) Close ¶
func (s *Signaler) Close()
Close stops every room, closes every socket, stops the fanout subscription. Safe to call twice.
func (*Signaler) Init ¶
Init is the lifecycle path: Mount on the App (which registers the route and, when the app has WithFanout, wires SetFanout) and Close into app shutdown. A nil Config.Authorize is returned as an error, not a panic: plugin wiring is a runtime step the host can react to. Mount keeps its panic for the direct-mount spelling of the same rule.
func (*Signaler) Kick ¶
Kick closes the local socket of one peer. Reports whether a local peer with that id was found; a peer held by another replica is not reached. The leave event is published when the socket's read loop exits. Kick is a nudge, not a revocation: the browser module reconnects with backoff, so a peer that must stay out is refused by Authorize on that reconnect, and a TURN credential it already holds keeps working until TURN.TTL runs out.
func (*Signaler) Mount ¶
Mount registers GET cfg.Path → ServeHTTP on the router. It panics if Config.Authorize is nil: a mounted signaling endpoint with no gate is a bug, not a default. The Signaler also satisfies the framework's Mountable seam, so App.Mount wires SetFanout automatically when the app has WithFanout.
func (*Signaler) Peers ¶
Peers returns the merged local plus live-remote roster of a room, sorted by Order then ID. Empty for an unknown room.
func (*Signaler) ReservedEmbedPrefixes ¶ added in v0.87.0
ReservedEmbedPrefixes reports the path this Signaler actually mounted. Authorize reads the request's user, and an embed grant installs its subject as that user, so a grant living in a stranger's page would join rooms as the subject. No embed surface may reach it. See framework.EmbedReserving.
func (*Signaler) Serve ¶
Serve upgrades r to a WebSocket, joins the peer to join.Room, and runs the read loop until the socket closes. The caller has already authorized the request; join is trusted. Blocks. A refused join (400 bad Join, 409 room full, 503 closed) is an HTTP status for a plain request and, for an upgrade request, an accepted handshake closed with code 4000+status (see refuse).
func (*Signaler) ServeHTTP ¶
func (s *Signaler) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP runs Authorize then Serve.
func (*Signaler) SetFanout ¶
SetFanout wires cross-replica relay. Duck-typed to the framework's Mountable wiring (see App.Mount): with WithFanout attached, mounting the Signaler calls this automatically. Returns the cancel that stops the subscription and heartbeat. An error if a fanout is already attached, f is nil, or the Signaler is closed.
type TURN ¶
type TURN struct {
URLs []string // "turn:host:3478?transport=udp", "turns:host:5349"
Secret string // static-auth-secret configured on the TURN server
TTL time.Duration // credential lifetime; 0 means 1h; minimum 1m
}
TURN mints per-peer, time-limited TURN credentials in the coturn static-auth-secret convention (also called the TURN REST API): username is "<unix-expiry>:<peer id>", credential is base64(HMAC-SHA1(secret, username)). The secret is shared with the TURN server and never leaves the Go process; each peer receives only its own credential, inside its own snapshot.
func (TURN) Credentials ¶
Credentials returns the ICEServer entry for one peer, valid until now+TTL, in the coturn static-auth-secret convention (the "TURN REST API"): username is "<unix-expiry>:<peer id>", credential is base64(HMAC-SHA1(secret, username)). The secret is shared with the TURN server and never leaves the Go process; each peer receives only its own credential, inside its own snapshot.