feat(playback): forward portal Cookie/Authorization to built-in players

The web players (HTML5/hls.js, Video.js, ArtPlayer, Shaka) could only ever
receive User-Agent/Referer/Origin, so any Stalker stream gated on the portal
session cookie or Bearer token played exclusively in external MPV/VLC — the
long-running "only VLC works" cluster (#849, #910, #732).

- request-header-overrides.service: the scoped override now carries Cookie
  and Authorization, attached only to requests on the exact stream origin,
  in-memory only, dropped on replace/clear. Unscoped (playlist-level) calls
  drop credentials fail-closed; control characters in header values are
  rejected. Chosen over session.cookies.set(): jar cookies attach only to
  credentialed requests, which would force withCredentials into every engine
  and break against the Access-Control-Allow-Origin:* IPTV panels send, and
  jar scoping is port-blind.
- WebPlayerViewComponent is now the single owner of the scoped override for
  every built-in player: it extracts the full header set from the resolved
  playback, configures the override BEFORE handing the source over (players
  render only once the source exists), and clears the scoped layer on
  destroy. HtmlVideoPlayerComponent's own three-header call is removed — it
  would overwrite the credentialed override.
- Stalker VOD, series episodes and radio now build the same portal header
  set ITV already had (they previously carried no portal headers at all);
  same-origin playback sends the real User-Agent alongside X-User-Agent.
- Stream classification is host-based via one shared predicate
  (isStalkerStreamCredentialSafe): same-host port changes and scheme
  upgrades keep the portal profile (the #1158 class), a foreign host or
  https->http downgrade keeps the credential-free KSPlayer profile. The
  main-process fallback context uses the same predicate so
  isStalkerDirectStreamProfile can no longer discard renderer headers.
- setUserAgent bridge gains an optional credentials parameter; preload,
  ipcMain handler and ElectronBridgeApi updated together.
- stalker-mock-server: gated-stream scenario (MAC 00:1A:79:00:00:09) whose
  create_link returns a local /stream/gated/video.mp4 that 403s without the
  mac cookie + current Bearer token; new Electron e2e proves a built-in
  player actually plays it (and that the gate refuses bare requests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-02 08:34:21 +02:00
1 parent 297e9fbef8
commit f196abd963
27 files changed
+1393 -136

No files matched your search

+41 -5
View File
@@ -122,8 +122,9 @@ policy.
## Scoped Request Header Overrides
Inline playback can request temporary `User-Agent`, `Referer`, and `Origin`
header overrides through `window.electron.setUserAgent(userAgent, referer,
scopeUrl)`.
header overrides — and, for auth-gated portal streams, `Cookie` and
`Authorization` credentials — through `window.electron.setUserAgent(userAgent,
referer, scopeUrl, credentials?)`.
The Electron backend handles that IPC in `apps/electron-backend/src/app/events/shared.events.ts`
and delegates to `apps/electron-backend/src/app/services/request-header-overrides.service.ts`.
@@ -131,11 +132,18 @@ The service registers one `session.defaultSession.webRequest.onBeforeSendHeaders
listener and updates layered in-memory overrides instead of stacking a new
listener for every channel change.
`WebPlayerViewComponent` is the single renderer owner of the scoped override:
it extracts the full header set from the resolved playback (including the
Stalker mac cookie and Bearer token), configures the override **before**
handing the source to any built-in player, and clears the scoped layer on
destroy. Individual player components must not call the bridge themselves — a
narrower call would overwrite the credentialed override.
Rules:
- empty playlist-level `userAgent` and `referer` clear all active overrides
- empty channel-level `userAgent` and `referer` with a `scopeUrl` clear only
the scoped channel override, preserving playlist-level defaults
- empty channel-level values with a `scopeUrl` clear only the scoped channel
override, preserving playlist-level defaults
- channel playback should pass the stream URL as `scopeUrl`
- scoped overrides apply only to the active stream origin and referer origin
- playlist-level user agents and referrers may call the bridge without a
@@ -143,10 +151,38 @@ Rules:
the whole M3U playlist
- header names are replaced case-insensitively before canonical `User-Agent`,
`Referer`, and `Origin` names are written
- header values containing control characters are rejected outright (header
smuggling)
Credential rules (`credentials.cookie` / `credentials.authorization`) are
deliberately stricter than the general scope:
- credentials are accepted **only** with a `scopeUrl` that parses to a
concrete origin; an unscoped (playlist-level) call silently drops them —
fail closed, never fail broad
- they are attached **only** to requests whose origin equals the stream URL's
exact origin — never to the referer-origin sibling that `User-Agent`/`Referer`
also cover, and never to third-party hosts an HLS manifest may point at
- they live only in the in-memory override: never in the session cookie jar,
never on disk, so they cannot outlive the app process
- they are dropped whenever the scoped override is replaced (channel change)
or cleared (playback end, `WebPlayerViewComponent` destroy)
The header-injection design was chosen over `session.cookies.set()`
deliberately: jar cookies only attach to credentialed requests, which would
force `withCredentials` into every web engine and break against the
`Access-Control-Allow-Origin: *` that IPTV panels typically send, and jar
scoping is domain-based (port-blind) — weaker than the exact-origin match
above. Injecting at `onBeforeSendHeaders` sits below the CORS/credentials
layer, so the request stays "uncredentialed" for the fetch spec while the
wire request carries the portal session.
When changing this flow, keep stale header cleanup covered. Switching from a
channel or playlist with custom headers to one without custom headers must clear
the previous override.
the previous override. The Electron e2e
`apps/electron-backend-e2e/src/stalker-playback-headers.e2e.ts` pins the
end-to-end contract against a mock stream that answers 403 without the portal
credentials.
## Main-Process Remote Requests
+43
View File
@@ -183,6 +183,49 @@ the cross-portal collection resolver (`StreamResolverService`) use
resolve relative (`/media/...`) or query-only (`?token=...`) `create_link`
replies against the portal base URL.
## Playback Header Contract
Every playback kind — ITV, VOD, series episodes, and radio — resolves its
stream and attaches the same portal header set through
`buildStalkerExternalPlaybackHeaders()`
(`libs/portal/stalker/data-access/src/lib/stalker-live-playback.utils.ts`).
The resolved `ResolvedPortalPlayback.headers` feed both the external players
(MPV/VLC/Embedded MPV via the launch IPC) and the built-in web players (via
the scoped Electron request-header override owned by `WebPlayerViewComponent`
— see `docs/architecture/electron-security.md`, "Scoped Request Header
Overrides").
Two stream profiles exist, selected by one shared predicate:
- **Portal-owned** (`isStalkerStreamCredentialSafe()` in
`@iptvnator/shared/interfaces`): the stream host equals the portal host —
including a different port or an http→https upgrade, the routine IPTV panel
shape (#1158 class). These streams get the full MAG profile: `Cookie`
(`mac=…` plus protocol cookies), `Authorization: Bearer <token>` when a
session token exists, `User-Agent` (playlist override or the MAG UA — the
API path always sent both, the playback set historically sent only
`X-User-Agent`), `X-User-Agent`, `SN` when a real serial exists, and
`Origin`/`Referer` set to the portal origin.
- **Foreign / direct** (different host, or an https→http downgrade): the
credential-free `KSPlayer` direct-stream profile (`User-Agent: KSPlayer`,
`Accept`, `Range`, `Icy-MetaData`, `Connection`). Portal credentials must
never reach a third-party host; direct stream URLs carry their access token
in the URL minted by `create_link`.
The Electron main process keeps a fallback header context per resolved
`create_link` URL (`stalker-playback-context.service.ts`) for external-player
launches that arrive without renderer headers. It classifies streams with the
same shared predicate — if the two ever diverged,
`isStalkerDirectStreamProfile` in the external-player path would discard the
renderer's credentialed headers for streams the main process misread as
direct.
The mock server's `gated-stream` scenario (MAC `00:1A:79:00:00:09`) makes
`create_link` return a local `/stream/gated/video.mp4` that answers 403
without the mac cookie and current Bearer token;
`apps/electron-backend-e2e/src/stalker-playback-headers.e2e.ts` uses it to
prove a built-in player's media requests really carry the credentials.
## Live TV and Radio
The Stalker live route and radio route intentionally share