Files
iptvnator/libs/shared/interfaces/src/lib/stalker-request-identity.util.ts
T
4grayandClaude Fable 5 65f81b7110 fix(pwa): bring the Stalker transport to parity with Electron (#1348)
* fix(pwa): bring the Stalker transport to parity with Electron

The self-hosted PWA's /stalker proxy now derives its portal requests from
the same shared identity and URL builders as the Electron main process:
MAG User-Agent/X-User-Agent, full STB cookie (mac + stb_lang + timezone +
serial-derived __cfduid), SN header, JsHttpRequest=1-xml defaulting, and
the sn-only-on-get_profile rule. macAddress/token/serialNumber are control
params consumed into headers and never echoed into the portal's query
string (handshake keeps its candidate token — protocol content). The
stalker-mock-server /stalker route mirrors the new contract through the
same shared builder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(stalker): forward the full identity header set in the mock /stalker mirror

Greptile review: the synthetic portal request kept only the cookie and
Authorization from the generated identity, so mock handlers could never
validate the SN/MAG-UA/Accept/Language/Connection headers the real proxy
sends. Forward the complete set, lowercased the way Express normalizes
incoming headers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:51:36 +02:00

131 lines
3.9 KiB
TypeScript

import {
buildStalkerSerialCfduid,
normalizeStalkerSerialNumber,
} from './stalker-identity.utils';
/**
* User-Agent of a MAG250 set-top box (matches the stalker-to-m3u reference
* implementation). Sent as both `User-Agent` and `X-User-Agent` on every
* portal API request — some panels reject requests without it.
*/
export const STALKER_MAG_USER_AGENT =
'Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250';
export interface StalkerIdentityRequestInput {
macAddress: string;
params: Record<string, string | number>;
token?: string;
serialNumber?: string;
}
export interface StalkerIdentityRequestContext {
requestParams: Record<string, string | number>;
headers: Record<string, string>;
cookieString: string;
effectiveSerialNumber?: string;
}
/**
* Builds the portal-facing identity of a Stalker request: the cookie a real
* STB sends (`mac` + `stb_lang` + `timezone`, plus a serial-derived
* `__cfduid`), the MAG User-Agent pair, the `SN`/`Authorization` headers, and
* the serial-number parameter rules (`sn` travels only on `get_profile`).
*
* This is the single source of truth for BOTH transports — the Electron main
* process and the self-hosted PWA's web-backend `/stalker` proxy — so the two
* cannot drift apart again. The stock server reads the timezone cookie with
* `new DateTimeZone()`, so it must always be a valid PHP timezone.
*/
export function buildStalkerIdentityRequestContext({
macAddress,
params,
token,
serialNumber,
}: StalkerIdentityRequestInput): StalkerIdentityRequestContext {
const effectiveSerialNumber = normalizeStalkerSerialNumber(serialNumber);
const requestParams = buildRequestParams(params, effectiveSerialNumber);
const cookieString = buildCookieString(macAddress, effectiveSerialNumber);
const headers: Record<string, string> = {
Cookie: cookieString,
'User-Agent': STALKER_MAG_USER_AGENT,
'X-User-Agent': STALKER_MAG_USER_AGENT,
Accept: '*/*',
Connection: 'keep-alive',
'Accept-Language': 'en-US,en;q=0.9',
};
if (effectiveSerialNumber) {
headers['SN'] = effectiveSerialNumber;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return {
requestParams,
headers,
cookieString,
...(effectiveSerialNumber ? { effectiveSerialNumber } : {}),
};
}
function buildRequestParams(
params: Record<string, string | number>,
effectiveSerialNumber: string | undefined
): Record<string, string | number> {
const requestParams = { ...params };
const isProfileRequest =
requestParams['type'] === 'stb' &&
requestParams['action'] === 'get_profile';
if (!isProfileRequest) {
delete requestParams['sn'];
return requestParams;
}
if (effectiveSerialNumber) {
requestParams['sn'] = effectiveSerialNumber;
} else {
delete requestParams['sn'];
}
if (typeof requestParams['metrics'] === 'string') {
try {
const parsedMetrics = JSON.parse(requestParams['metrics']);
const nextMetrics = { ...(parsedMetrics ?? {}) };
if (effectiveSerialNumber) {
nextMetrics.sn = effectiveSerialNumber;
} else {
delete nextMetrics.sn;
}
requestParams['metrics'] = JSON.stringify(nextMetrics);
} catch {
// Keep original metrics payload when malformed.
}
}
return requestParams;
}
function buildCookieString(
macAddress: string,
effectiveSerialNumber: string | undefined
): string {
const cookieParts = [
`mac=${macAddress}`,
'stb_lang=en_US@rg=dezzzz',
'timezone=Europe/Berlin',
];
if (effectiveSerialNumber) {
cookieParts.push(
`__cfduid=${buildStalkerSerialCfduid(effectiveSerialNumber)}`
);
}
return cookieParts.join('; ');
}