* fix(stalker): keep session headers on same-host redirects Since 0.22 requestWithValidatedRedirects stripped Cookie/Authorization whenever a redirect changed the *origin*, so a portal answering with an http->https upgrade or a port move lost the MAC cookie and Bearer token mid-session. Real Stalker/Ministra servers then reply with a plain-text "Authorization failed." body: categories fail to load, create_link never resolves, and no player receives a stream URL (#1158 regression window). Scope credential stripping to the host instead: same-host scheme/port redirects keep headers, basic auth, params, and request bodies; a redirect to a different host still drops all of them, preserving the original hardening intent (no credential leaks to third-party hosts). Also adds the Stalker API compatibility roadmap produced by the 2026-08-01 protocol audit (.plans/, force-added like earlier plans). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(electron): strip credentials on same-host https-to-http downgrades Review follow-up (Greptile P1 + Codex on #1322): the host-only check kept Authorization/Cookie/basic auth/params/body when an https request was redirected to http on the same host, replaying a TLS-obtained session in cleartext. Treat that downgrade like a host change: strip credentials and refuse to replay request bodies. Scheme upgrades and port moves on the same host keep headers — the actual #1158 scenarios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
11 KiB
Stalker/Ministra API Compatibility — Roadmap
Date: 2026-08-01. Source: full audit (codebase map + reference implementations + GitHub issues). Status legend: [ ] planned · [~] in progress · [x] merged.
Reference facts (from Stalker 4.9.35 plaintext source zfix/stalker-portal-4.9.x, 5.1.1 client JS, Kodi pvr.stalker, stalkerhek)
These are the ground-truth rules a compatible client must follow. Re-verified 2026-08-01; do not re-research unless contradicted.
- Auth failure is HTTP 200 + text/html body — exact strings:
Authorization failed.,Access denied.,Unauthorized request.Never a 401/403. Cookie: mac=<URL-encoded, UPPERCASE>required on every request;Authorization: Bearer <token>required for everything excepthandshake,get_profile,get_localization,do_auth.- The only identity params the stock server enforces are
device_id/device_id2: first non-empty value is pinned to the MAC forever; mismatch →{status:1, msg:"device conflict …", block_msg:"Your STB is damaged…"}. Empty on a fresh MAC is fine; empty after a value was pinned = permanent lockout. signatureis read but never verified (only passed to optional operatoraccess_filter.php).metrics,prehash,timestamp,api_signatureare never read by the server.signatureon a real box =gSTB.GetUID(nonce)firmware call; 4.9.x signsaccess_token, 5.x signs handshakerandom. Not reproducible client-side;stalker-to-m3ufabricatesSHA256(mac)-based values and works.- MAC format validation is ON by default:
/^00:1A:79:[0-9A-F]{2}:…$/(Infomir OUI, uppercase). Failure → bare{status:1}. get_profilestatusdecoded: full profile = OK;1= blocked (seemsg/block_msg);2= login/password required →do_auth(paramslogin,password,device_id,device_id2; response{js:true|false}) thenget_profilewithauth_second_step=1.- Handshake token = random 32 uppercase hex; idempotent (re-presenting a valid token returns it) → tokens can be persisted across restarts. Tokens have no TTL; they are only invalidated when another device does
get_profileon the same MAC. - Watchdog
get_eventsexpected every 120 s (watchdog_timeoutechoed in profile + per-usertimeslotjitter). Not calling it does NOT invalidate auth (only admin-panel "online" status); calling every 25 s is ~5× too often. create_linkshould be called only when the channel row setsuse_http_tmp_link/use_load_balancing; otherwise the staticcmdplays directly. Temp links live 5 s (tv_tmp_link_ttl), not single-use → resolve immediately before playback, never cache.cmdformat:<solution> <url>where solution ∈ {ffrt, ffrt2, ffrt3, ffmpeg, auto…} — strip by splitting on the FIRST space if present; bare URL is legal. Real MAG sendscmdunencoded.portal.phpdoes not exist in official Stalker/Ministra — it's a reseller-panel alias. Canonical endpoint derives from/c/→<base>/server/load.php. Robust discovery: probeportal.php→server/load.php→stalker_portal/server/load.php(→ optionally parseajax_loaderout of/c/xpcom.common.js).- Timezone cookie must be a valid PHP timezone or omitted (server does
new DateTimeZone()on it). - Response envelope
{js, text}: ignoretext(carriesvar_dumpnoise).
Audit findings (what's wrong in our code)
- 0.22 regression (issue #1158):
requestWithValidatedRedirectsstripsCookie/AuthorizationwhennextUrl.origin !== validatedUrl.origin— scheme/port changes (http→https, :80→:8080) on the SAME host count as cross-origin, so redirected portals lose mac-cookie + token →Authorization failed.→ no categories, nocreate_link, MPV "won't even launch". Introduced in2c032cd3c(0.22). File:apps/electron-backend/src/app/util/validated-axios.ts(~line 240). - Same commit made
cmdpercent-encoded (encodeURIComponent+ slash restore); real MAG sends it raw → possible double-encoding when portal cmd already contains%and strict panels compare strings. Needs verification. - Portal mode (
isFullStalkerPortal) is guessed from URL shape, with TWO diverging predicates (/stalker_portal/w/ slash in session svc vs w/o slash at import); simple portals get NO handshake/token/watchdog;…/cis rewritten toportal.phpwhich official portals don't have; no endpoint probing. Issues #850, #686, #755, #910. - Built-in web players receive only User-Agent/Referer/Origin —
request-header-overrides.service.tscannot set Cookie/Authorization; token/mac-gated streams can never play in HTML5/Video.js/ArtPlayer/Shaka → the perennial "works only in VLC" cluster (#849, #910, #732). - Stalker playback headers built ONLY for ITV (
with-stalker-player.feature.ts:244); VOD/series/radio go to players with none. Cross-origin streams getUser-Agent: KSPlayerprofile with no cookie/token and it overrides caller headers. Same-origin setsX-User-Agentbut notUser-Agent. - No
js.statushandling (status 2 → do_auth is dead code, import form login/password commented out),msg/block_msgnever surfaced, onlyAuthorization failedstring caught (notAccess denied./Unauthorized request.),not_valid/keep_aliveignored, persistedstalkerTokennever reused despite idempotent handshake. - No MAC normalization/validation at import; raw string goes into cookie and
sha1(mac.toUpperCase())prehash. - Watchdog interval hardcoded 25 s (should be
watchdog_timeoutfrom profile, default 120). create_linkcalled unconditionally;use_http_tmp_link/use_load_balancingnever checked.- PWA path much weaker than Electron: no MAG UA/X-User-Agent, cookie only
mac=, noJsHttpRequest=1-xmlinjection,snnot stripped for non-get_profile, mac+token leak into portal query string,cmdslashes become%2F. - Never-sent profile params:
ver,hw_version,image_version,client_type;stb_typealways''. Free to send, someaccess_filter.phpdeployments inspect them. - Mock server implements neither
get_profilenorget_events, validates no auth at all; e2e usesportal.php→ full-portal auth surface has ZERO coverage. - Two divergent
cmdnormalizers (stalker-player-request.utils.tsstrong vsstream-resolver.service.tsweak, no base-path resolution). .claude/skills/stalker-portal/SKILL.md+.codextwin point at deletedapps/web/src/app/stalker/*paths.
PR series (ordered for safety)
PR 1 — fix(electron): keep auth headers on same-host redirects [PR #1322 open]
Fixes the #1158 regression. In validated-axios.ts, strip Cookie/Authorization/Proxy-Authorization only when the redirect changes host, not on scheme/port change of the same host (curl semantics). Regression tests: http→https same host keeps headers; host change strips. Release note required. Branch: claude/stalker-api-compatibility-7a1ecb → https://github.com/4gray/iptvnator/pull/1322
PR 2 — test(stalker): mock-server auth enforcement + e2e for the full-portal surface [~ in progress, branch claude/stalker-mock-auth-e2e]
Pull coverage FORWARD before risky refactors: mock get_profile (validates Bearer, returns profile w/ configurable status 0/1/2, msg/block_msg), get_events, token enforcement returning literal Authorization failed. (200 text/html!) for missing/invalid Bearer, scenario MACs for device-conflict and status-2. E2E: full-portal import → handshake → get_profile → content → create_link → re-auth after token invalidation. This is the regression safety net for PRs 3–7.
PR 3 — fix(stalker): verify/fix cmd encoding against reference behavior
Reproduce with a %-containing cmd; decide raw-with-safe-charset vs decode-before-encode. Keep the injection protection from 2c032cd3c but avoid double-encoding. Unit tests with real-world cmd corpus (ffrt3 http://…, /media/123.mpg, tokens with %3A).
PR 4 — feat(stalker): endpoint probing + behavior-based portal mode
Replace URL-shape guessing: probe portal.php → server/load.php → stalker_portal/server/load.php at import (and once at runtime for legacy records); classify full/simple by observed handshake success, persist the resolved endpoint + mode; unify the two predicates. Migration for existing playlists. Fixes #850/#686/#755 class.
PR 5 — feat(playback): forward Cookie/Authorization to built-in players + headers for VOD/series/radio
Extend request-header-overrides.service.ts (scoped per stream origin/URL, cleared on playback end) to set Cookie + Authorization; extract headers in web-player-view.component.ts; build Stalker header set for VOD/series/radio, not just ITV; fix same-origin User-Agent; revisit KSPlayer cross-origin profile override. Fixes the "only VLC works" cluster (#849/#910/#732 for Stalker).
PR 6 — feat(stalker): protocol-correct auth lifecycle
- Parse
js.status: 2 → revivedo_auth(uncomment import form login/password), thenget_profilewithauth_second_step=1. - Surface
msg/block_msgto the user (import dialog + portal error state). - Detect all three plain-text bodies (
Authorization failed.,Access denied.,Unauthorized request.) at the transport level instead of JSON.stringify regex. - Reuse persisted
stalkerToken(handshake is idempotent), propagatenot_valid_token. - Watchdog interval from profile
watchdog_timeout(+timeslot), default 120 s instead of 25 s.
PR 7 — feat(stalker): identity hardening
- MAC normalization (uppercase, colon format) + validation with OUI
00:1A:79hint in the import UI. - Optional deterministic device_id derivation (SHA256(mac), opt-in checkbox like StbEmu) with an explanation of the pinning/lockout semantics; never auto-change once set.
- Send the free plausible params:
ver,stb_type(real value),hw_version,image_version,client_type,num_banks,video_out,hd. - Device-conflict
msgmapped to a human-readable error.
PR 8 — fix(stalker): playback link semantics
Respect use_http_tmp_link/use_load_balancing (static cmd when unset); never cache resolved links (5 s TTL); unify the two cmd normalizers into one shared util.
PR 9 — fix(pwa): Stalker parity with Electron transport
MAG UA/X-User-Agent, full cookie, JsHttpRequest=1-xml injection, strip sn for non-get_profile, stop leaking mac/token into the portal query string, slash-preserving cmd encoding in web-backend proxy.
PR 10 — chore(docs/skills): sync
Update docs/architecture/stalker-portal.md (watchdog, endpoint probing, auth lifecycle), fix stale .claude/skills/stalker-portal/SKILL.md + .codex twin paths, document protocol reference facts.
Process
- One PR per numbered item; each in its own thread/worktree with this file as the handoff. Update the status legend here as PRs land (and renumber if scope shifts).
- Before opening each PR from a worktree: check
git log origin/master..HEADfor inherited local commits (known trap). - Every behavior PR:
.changes/release note; targeted tests per Regression Prevention policy.
Related issues
#1158 (0.22 regression, PR 1), #910/#849/#732 (VLC-only cluster, PR 5), #850/#686/#755 (URL/portal-mode, PR 4), #927/#860/#345/#448/#453 (identity fields, PR 7), #1146 (search, mostly shipped via #1209).