feat(mediaauth): playback authorization (token + IP/country/UA/referer chain) — B / S-13#21
Merged
Merged
Conversation
…referer chain) Open-Streamer had no media-plane access control: anyone who could reach a stream URL could watch it, the ?token= param was recorded but never enforced, and a tokened SRT streamid failed to resolve (audit S-13). Add a playback authorizer enforced across every delivery protocol. internal/mediaauth: a Flussonic-style chain — deny IP/country/UA wins, then allow-list gates (IP/country/UA + Referer allowed-domains), then a per-stream token-policy gate. Tokens are HMAC-SHA256, client-signed and server-verified (token = "<exp>.<base64url(HMAC(secret, code|exp))>"): constant-time compare, expiry- and stream-code-bound. The server only verifies — clients mint tokens with the shared secret via the documented SignToken format (no mint endpoint). Wiring (authorize before any bytes/state): - HTTP (HLS/DASH/MPEGTS/DVR) in dispatchMedia.mediaAllowed. - RTMP/SRT/RTSP play sites via publisher.playAllowed. Effective policy = per-stream Stream.PlaybackAuth (public/token, template- inherited) else global auth.media.default_policy; static rules are global. Country uses the existing sessions GeoIP. Disabled by default (no behaviour change); fail-closed when token policy has no secret. Config auth.media.*, hot-reloaded via the runtime diff (atomic snapshot swap). Per-stream policy is resolved from the publisher's in-memory table for live streams (O(1), no store read on the hot path) and from the store for stopped-stream DVR archives so a token stream isn't downgraded. Also fixes: SRT srtStreamCode strips the ?token= query so tokened streamids resolve; ABR renditions key auth on the parent stream code so a token for <code> covers /<code>/track_N/… (adversarial review found both — without the strip, token playback of transcoded/ABR streams was broken). Closes the media-plane half of S-13. Caveats (separate findings): IP/country rules trust RealIP/X-Forwarded-For, so they need a trusted proxy (S-15/S-17); RTMP play carries no token (IP/country still apply); the dynamic HTTP-callback backend and per-stream rule overrides are planned follow-ups. Tests: internal/mediaauth chain+token suite (deny/allow per dimension, token sign/verify/expiry/tamper/cross-stream, per-stream override, fail-closed, hot-reload), TestStripABRTrackSlug. Adversarially reviewed.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part B — media/playback auth plane
Open-Streamer had no playback access control: anyone reaching a stream URL could watch it,
?token=was recorded but never enforced, and a tokened SRT streamid failed to resolve (audit S-13). This adds a playback authorizer enforced on every delivery protocol.Design —
internal/mediaauthA Flussonic-style chain (deny wins → allow-list gates → policy gate):
Token = HMAC-SHA256, client-signed, server verify-only:
token = "<exp>.<base64url(HMAC(secret, "code|exp"))>"— constant-time compare, expiry- and stream-code-bound. Clients mint it with the shared secret via the documentedmediaauth.SignTokenformat; no server mint endpoint (per request — server only verifies).Wiring (authorize before any bytes/state)
dispatchMedia.mediaAllowed.publisher.playAllowed.Effective policy = per-stream
Stream.PlaybackAuth(public/token, template-inherited) else globalauth.media.default_policy; static rules (IP/country via the existing sessions GeoIP, UA, referer) are global. Disabled by default (no behaviour change). Fail-closed when token policy has no secret. Configauth.media.*, hot-reloaded (atomic snapshot). Per-stream policy resolves from the publisher's in-memory table for live streams (O(1), no store read on the hot path) and from the store for stopped-stream DVR archives.Adversarial review → 2 real issues found & fixed
<code>/track_N→ a token for<code>was rejected (broke transcoded/ABR token playback). Now keyed on the parent code.tokenstream stays protected.Plus the SRT
?token=parse bug (srtStreamCodestrips the query).Caveats (separate findings, documented)
r.RemoteAddrfromRealIP/X-Forwarded-For→ need a header-overwriting trusted proxy (S-15/S-17).on_play) backend + per-stream rule overrides — planned follow-up.Config
Per-stream override:
playback_auth: public|tokenon the stream/template.Test
internal/mediaauthsuite (deny/allow per dimension; token sign/verify/expiry/tamper/cross-stream; per-stream override; fail-closed; hot-reload),TestStripABRTrackSlug.go test -race ./internal/mediaauth/ ./internal/publisher/ ./internal/api/...green,golangci-lint0 issues, fullgo build ./...green.