mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
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:
1 parent
04b2f9b82e
commit
3326f73388
15 files changed
+600
-59
No files matched your search
@@ -390,6 +390,56 @@ https://stream.example/live.m3u8`);
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards Stalker cmd in the reference wire format instead of axios encoding', async () => {
|
||||
const httpClient = new StubHttpClient();
|
||||
httpClient.queueResponse({ js: { cmd: 'http://cdn/stream.m3u8' } });
|
||||
|
||||
await withServer(
|
||||
createWebBackendApp({
|
||||
httpClient,
|
||||
resolveHostname: resolvePublicHost,
|
||||
}),
|
||||
async (baseUrl) => {
|
||||
const targetId = await registerProviderTarget(
|
||||
baseUrl,
|
||||
'http://stalker.example/portal.php'
|
||||
);
|
||||
// The PWA renderer sends fully URLSearchParams-encoded values;
|
||||
// Express decodes them back to the stored cmd string.
|
||||
const query = new URLSearchParams({
|
||||
targetId,
|
||||
macAddress: '00:1A:79:00:00:01',
|
||||
action: 'create_link',
|
||||
type: 'itv',
|
||||
cmd: 'ffrt3 http://host/ch/123?token=a%3Ab c&x=1',
|
||||
});
|
||||
const response = await fetch(
|
||||
`${baseUrl}/stalker?${query.toString()}`
|
||||
);
|
||||
await response.json();
|
||||
|
||||
// Slashes stay raw and pre-encoded sequences pass through
|
||||
// untouched (no %25 double-encoding); '&' inside cmd cannot
|
||||
// append query parameters. cmd is no longer in axios params.
|
||||
expect(httpClient.requests).toEqual([
|
||||
{
|
||||
headers: {
|
||||
Cookie: 'mac=00:1A:79:00:00:01',
|
||||
},
|
||||
params: {
|
||||
action: 'create_link',
|
||||
macAddress: '00:1A:79:00:00:01',
|
||||
type: 'itv',
|
||||
},
|
||||
url:
|
||||
'http://stalker.example/portal.php' +
|
||||
'?cmd=ffrt3%20http://host/ch/123?token=a%3Ab%20c%26x=1',
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes provider errors for portal proxy calls', async () => {
|
||||
const httpClient = new StubHttpClient();
|
||||
httpClient.queueFailure(403, 'Forbidden');
|
||||
|
||||
@@ -7,7 +7,10 @@ import zlib from 'node:zlib';
|
||||
import axios from 'axios';
|
||||
import epgParser from 'epg-parser';
|
||||
import parser from 'iptv-playlist-parser';
|
||||
import { normalizeXtreamServerUrl } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
encodeStalkerCmdValue,
|
||||
normalizeXtreamServerUrl,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { extractDrmFromRaw } from '@iptvnator/shared/m3u-utils';
|
||||
|
||||
export interface WebBackendHttpGetOptions {
|
||||
@@ -225,10 +228,20 @@ export function createWebBackendApp(
|
||||
}
|
||||
|
||||
try {
|
||||
// `cmd` must reach the portal in the reference wire format (raw
|
||||
// slashes, pre-encoded sequences untouched) — axios' default
|
||||
// serializer would fully percent-encode it, diverging from what a
|
||||
// real STB (and the Electron transport) sends. Append it to the
|
||||
// URL with the shared encoder and let axios serialize the rest.
|
||||
const { cmd, ...proxyParams } = getProxyParams(req, ['targetId']);
|
||||
const requestUrl = cmd
|
||||
? `${url.href}${url.search ? '&' : '?'}cmd=${encodeStalkerCmdValue(cmd)}`
|
||||
: url.href;
|
||||
|
||||
// Provider URLs are validated by /provider-targets before they enter the registry.
|
||||
// codeql[js/request-forgery]
|
||||
const response = await httpClient.get(url.href, {
|
||||
params: getProxyParams(req, ['targetId']),
|
||||
const response = await httpClient.get(requestUrl, {
|
||||
params: proxyParams,
|
||||
headers: {
|
||||
...(macAddress ? { Cookie: `mac=${macAddress}` } : {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
|
||||
Reference in new issue
Block a user