mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
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:
1 parent
297e9fbef8
commit
f196abd963
27 files changed
+1393
-136
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user