mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19: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>
* fix(pwa): sanitize portal URL before appending stalker cmd
A registered portal URL carrying a fragment would swallow the appended
cmd (everything after # is never transmitted), and a trailing bare '?'
produced '??cmd='. Drop the hash and pick the separator from the
sanitized href before appending. Flagged by Greptile/Codex on #1334.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(pwa): make stalker cmd append visibly query-only for CodeQL
Rebuild the /stalker request URL through the URL object and concatenate
the encoded cmd strictly behind a literal '?', so static analysis can
see the tainted value never reaches host or path (js/request-forgery
alert on the previous separator ternary). Behavior unchanged; the
fragment/bare-'?' regression tests still pin the wire format.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
import { encodeStalkerCmdValue } from './stalker-cmd-encoding.util';
|
|
|
|
/**
|
|
* One application-x-www-form-urlencoded decode, the way PHP's `$_GET` (and
|
|
* Express' query parser) sees the value: `+` is a space, `%XX` decodes once.
|
|
*/
|
|
function portalVisibleValue(encoded: string): string {
|
|
return decodeURIComponent(encoded.replace(/\+/g, '%20'));
|
|
}
|
|
|
|
describe('encodeStalkerCmdValue', () => {
|
|
it('keeps a plain solution-prefixed cmd readable (space only)', () => {
|
|
expect(encodeStalkerCmdValue('ffrt3 http://host/ch/123')).toBe(
|
|
'ffrt3%20http://host/ch/123'
|
|
);
|
|
});
|
|
|
|
it('keeps ?, = and : raw in a tokened cmd', () => {
|
|
expect(
|
|
encodeStalkerCmdValue('auto http://host/ch/123?token=abc')
|
|
).toBe('auto%20http://host/ch/123?token=abc');
|
|
});
|
|
|
|
it('leaves a bare media path untouched', () => {
|
|
expect(encodeStalkerCmdValue('/media/12345.mpg')).toBe(
|
|
'/media/12345.mpg'
|
|
);
|
|
});
|
|
|
|
it('never double-encodes pre-encoded sequences', () => {
|
|
expect(
|
|
encodeStalkerCmdValue('auto http://host/s/a%3Ab%20c.m3u8')
|
|
).toBe('auto%20http://host/s/a%3Ab%20c.m3u8');
|
|
});
|
|
|
|
it('passes a bare, malformed % through untouched', () => {
|
|
expect(encodeStalkerCmdValue('/media/100%.mpg')).toBe(
|
|
'/media/100%.mpg'
|
|
);
|
|
});
|
|
|
|
it('encodes the query-structure characters &, # and ;', () => {
|
|
expect(encodeStalkerCmdValue('a&b#c;d')).toBe('a%26b%23c%3Bd');
|
|
});
|
|
|
|
it('keeps + raw so the portal sees a space, like it does for a real STB', () => {
|
|
const encoded = encodeStalkerCmdValue('auto http://host/s/a+b.ts');
|
|
expect(encoded).toBe('auto%20http://host/s/a+b.ts');
|
|
expect(portalVisibleValue(encoded)).toBe('auto http://host/s/a b.ts');
|
|
});
|
|
|
|
it('keeps IPv6 literals byte-identical', () => {
|
|
expect(encodeStalkerCmdValue('ffmpeg http://[::1]:8080/ch/1')).toBe(
|
|
'ffmpeg%20http://[::1]:8080/ch/1'
|
|
);
|
|
});
|
|
|
|
it('encodes quotes, backslash and control characters', () => {
|
|
expect(encodeStalkerCmdValue(`a"b'c\\d\ne`)).toBe(
|
|
'a%22b%27c%5Cd%0Ae'
|
|
);
|
|
});
|
|
|
|
it('percent-encodes non-ASCII as UTF-8', () => {
|
|
expect(encodeStalkerCmdValue('auto http://host/канал/1')).toBe(
|
|
'auto%20http://host/%D0%BA%D0%B0%D0%BD%D0%B0%D0%BB/1'
|
|
);
|
|
});
|
|
|
|
it('survives WHATWG URL re-parsing byte-identically', () => {
|
|
const corpus = [
|
|
'ffrt3 http://host/ch/123',
|
|
'auto http://host/ch/123?token=abc',
|
|
'/media/12345.mpg',
|
|
'auto http://host/s/a%3Ab%20c.m3u8',
|
|
"a&b#c;d'e\"f",
|
|
'ffmpeg http://[::1]:8080/ch/1',
|
|
'auto http://host/канал/1',
|
|
];
|
|
for (const cmd of corpus) {
|
|
const query = `cmd=${encodeStalkerCmdValue(cmd)}`;
|
|
expect(new URL(`http://portal/load.php?${query}`).search).toBe(
|
|
`?${query}`
|
|
);
|
|
}
|
|
});
|
|
|
|
it('is value-preserving after one server-side decode for every escaped character', () => {
|
|
const original = `ffrt http://h/s?a=1&b=2#f;g "q" 'x' я`;
|
|
expect(portalVisibleValue(encodeStalkerCmdValue(original))).toBe(
|
|
original
|
|
);
|
|
});
|
|
});
|