fix(stalker): send cmd in the reference MAG wire format

A real MAG sends cmd unencoded and the portal decodes its query exactly
once, so a cmd that already contains percent sequences (%3A tokens,
pre-encoded path segments) must pass through untouched. The previous
encodeURIComponent transport (2c032cd3c, 0.22) double-encoded such cmds
(%3A -> %253A): strict portals and reseller panels that compare cmd
literally, and stock create_link handlers matching the decoded value,
saw a different string than a real STB sends.

The new shared encodeStalkerCmdValue() reproduces the reference wire
bytes: % passes through verbatim, characters the WHATWG URL serializer
keeps raw in a query stay raw (so the bytes survive the axios/new URL
transport unchanged), and everything else is percent-encoded. That
preserves the 0.22 injection protection - &, # (and ; for PHP setups
with a ; argument separator) inside cmd cannot append or truncate query
parameters; they decode back to the original byte server-side.

Both transports now share the format: the Electron query builder is
extracted to buildStalkerRequestUrl() and the web-backend /stalker
proxy appends cmd to the portal URL itself instead of letting axios
turn slashes into %2F (the opposite divergence).

Also unifies the two divergent response-side cmd normalizers: the
cross-portal collection resolver now uses the Stalker store's
normalizeStalkerPlaybackCommand/resolveStalkerPlaybackUrl, so playing
from Favorites/global collections resolves relative (/media/...) and
query-only (?token=...) create_link replies against the portal base
instead of handing the player a bare relative path.

The mock portal's create_link response gains mock-only cmd_received/
query_keys_received diagnostics; a new Electron e2e pins the contract
end-to-end (single decode, injection blocked). Unit corpus tests cover
the encoder, the Electron builder, the web-backend proxy, and the
resolver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-01 22:33:47 +02:00
1 parent 04b2f9b82e
commit 3326f73388
15 files changed
+600 -59

No files matched your search

+9 -1
View File
@@ -208,13 +208,21 @@ marker and an `ffrt4://radio/...` command.
"cmd": "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
"streamer_id": "1",
"load": "",
"error": ""
"error": "",
"cmd_received": "ffrt4://ch/live/1001/index.m3u8",
"query_keys_received": ["JsHttpRequest", "action", "cmd", "type"]
}
}
```
The stream URL is selected from a pool of 4 real public HLS test streams. The choice is deterministic based on the `cmd` field's character sum, so the same item always returns the same stream.
`cmd_received` and `query_keys_received` are mock-only diagnostics (a real
portal does not send them): they echo the request's `cmd` after Express' single
query decode — the same view a PHP portal gets from `$_GET` — plus the sorted
set of query keys. E2E uses them to pin the client's `cmd` wire contract: no
double-encoding, and no query-parameter injection through `cmd`.
### `get_short_epg`
```json
+45
View File
@@ -138,6 +138,51 @@ blank fields are not generated or forwarded to `get_profile`.
metadata is independent from M3U playlist EPG metadata and must not depend on
M3U-specific EPG fields.
## Request Transport and `cmd` Encoding
A real MAG/STB sends `cmd` unencoded: the portal's client JS concatenates raw
`key=value` pairs, the browser URL layer escapes only what a URL cannot carry,
and PHP's `$_GET` applies exactly one form-urldecode. The portal therefore sees
the stored `cmd` decoded **once** — a pre-encoded `%3A` arrives as `:` and a
literal `+` arrives as a space. IPTVnator reproduces that reference wire format
on both transports with the shared `encodeStalkerCmdValue()`
(`libs/shared/interfaces/src/lib/stalker-cmd-encoding.util.ts`):
- `%` passes through untouched, so a `cmd` that already contains percent
sequences is never double-encoded (the pre-0.23 `encodeURIComponent`
transport delivered `%253A` and strict panels no longer matched the string).
- Characters the WHATWG URL serializer keeps raw in a query stay raw
(`/ : ? = + , @ $ [ ]` …), so the emitted bytes survive the axios/`new URL`
transport unchanged.
- Everything else is percent-encoded. This keeps the injection protection from
the 0.22 hardening: `&`, `#` (and `;` for PHP setups with a `;` argument
separator) inside `cmd` cannot append or truncate query parameters — they
decode back to the original byte server-side, so the portal-visible value is
unaffected.
Consumers of the encoder:
- Electron: `buildStalkerRequestUrl()`
(`apps/electron-backend/src/app/events/stalker-request-url.ts`) assembles the
portal query for `STALKER_REQUEST`; `cmd` uses the reference encoding, every
other param stays fully `encodeURIComponent`-encoded, and `JsHttpRequest=1-xml`
is appended when missing.
- PWA: the web-backend `/stalker` proxy appends `cmd` to the portal URL with
the same encoder instead of letting axios' serializer turn slashes into
`%2F`. The renderer→proxy leg uses `URLSearchParams`, which Express decodes
losslessly, so the stored `cmd` string reaches the proxy intact.
The mock portal's `create_link` response carries mock-only `cmd_received` and
`query_keys_received` diagnostics so E2E can pin this contract
(`apps/electron-backend-e2e/src/providers.e2e.ts`).
Response-side `cmd` normalization is also shared: both the Stalker store and
the cross-portal collection resolver (`StreamResolverService`) use
`normalizeStalkerPlaybackCommand()` / `resolveStalkerPlaybackUrl()` from
`libs/portal/stalker/data-access`, which strip the `<solution> ` prefix and
resolve relative (`/media/...`) or query-only (`?token=...`) `create_link`
replies against the portal base URL.
## Live TV and Radio
The Stalker live route and radio route intentionally share