mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
feat(stalker): support ministra shows under VOD content type
Update translations and refactor StalkerPortalImportComponent to use Angular signals and readonly/dependency-injected readonly properties. - Add new i18n keys: LOADING_SEASONS and NO_EPISODES; adjust PLAYBACK_ERROR punctuation. - Replace EventEmitter @Output with a signals-based readonly output and mark form, constants and flags as readonly. - Switch to inject() style for services and mark many injected fields private/readonly to clarify intent. - Replace DataService import with STALKER_SERIAL_NUMBER token import. - Improve logging formatting, whitespace and minor code cleanup. - Minor template/style whitespace fixes. These changes modernize the component for better immutability, improve DX with clearer logging, and add missing user-facing translation strings.
This commit is contained in:
1 parent
61eef5e00d
commit
16baf54b16
9 files changed
+887
-379
No files matched your search
@@ -1,110 +1,27 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Playlist, STALKER_REQUEST } from 'shared-interfaces';
|
||||
import { DataService } from './data.service';
|
||||
import { STALKER_REQUEST, Playlist } from 'shared-interfaces';
|
||||
|
||||
/**
|
||||
* Pure JavaScript SHA1 implementation
|
||||
* SHA1 hash using native Web Crypto API
|
||||
* Produces correct 40-character hex hash matching real Stalker clients
|
||||
*/
|
||||
function sha1(str: string): string {
|
||||
function rotl(n: number, s: number): number {
|
||||
return (n << s) | (n >>> (32 - s));
|
||||
}
|
||||
|
||||
function toHex(n: number): string {
|
||||
let hex = '';
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
hex += ((n >>> (i * 4)) & 0xf).toString(16);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
// Convert string to bytes
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
bytes.push(str.charCodeAt(i) & 0xff);
|
||||
}
|
||||
|
||||
// Pre-processing: adding padding bits
|
||||
const originalLength = bytes.length * 8;
|
||||
bytes.push(0x80);
|
||||
while ((bytes.length % 64) !== 56) {
|
||||
bytes.push(0);
|
||||
}
|
||||
|
||||
// Append original length in bits as 64-bit big-endian
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
bytes.push((originalLength >>> (i * 8)) & 0xff);
|
||||
}
|
||||
|
||||
// Initialize hash values
|
||||
let h0 = 0x67452301;
|
||||
let h1 = 0xefcdab89;
|
||||
let h2 = 0x98badcfe;
|
||||
let h3 = 0x10325476;
|
||||
let h4 = 0xc3d2e1f0;
|
||||
|
||||
// Process each 64-byte chunk
|
||||
for (let chunkStart = 0; chunkStart < bytes.length; chunkStart += 64) {
|
||||
const w: number[] = [];
|
||||
|
||||
// Break chunk into sixteen 32-bit big-endian words
|
||||
for (let i = 0; i < 16; i++) {
|
||||
w[i] = (bytes[chunkStart + i * 4] << 24) |
|
||||
(bytes[chunkStart + i * 4 + 1] << 16) |
|
||||
(bytes[chunkStart + i * 4 + 2] << 8) |
|
||||
(bytes[chunkStart + i * 4 + 3]);
|
||||
}
|
||||
|
||||
// Extend to 80 words
|
||||
for (let i = 16; i < 80; i++) {
|
||||
w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
||||
}
|
||||
|
||||
let a = h0, b = h1, c = h2, d = h3, e = h4;
|
||||
|
||||
for (let i = 0; i < 80; i++) {
|
||||
let f: number, k: number;
|
||||
if (i < 20) {
|
||||
f = (b & c) | ((~b) & d);
|
||||
k = 0x5a827999;
|
||||
} else if (i < 40) {
|
||||
f = b ^ c ^ d;
|
||||
k = 0x6ed9eba1;
|
||||
} else if (i < 60) {
|
||||
f = (b & c) | (b & d) | (c & d);
|
||||
k = 0x8f1bbcdc;
|
||||
} else {
|
||||
f = b ^ c ^ d;
|
||||
k = 0xca62c1d6;
|
||||
}
|
||||
|
||||
const temp = (rotl(a, 5) + f + e + k + w[i]) >>> 0;
|
||||
e = d;
|
||||
d = c;
|
||||
c = rotl(b, 30) >>> 0;
|
||||
b = a;
|
||||
a = temp;
|
||||
}
|
||||
|
||||
h0 = (h0 + a) >>> 0;
|
||||
h1 = (h1 + b) >>> 0;
|
||||
h2 = (h2 + c) >>> 0;
|
||||
h3 = (h3 + d) >>> 0;
|
||||
h4 = (h4 + e) >>> 0;
|
||||
}
|
||||
|
||||
return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4);
|
||||
async function sha1(str: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SHA1 prehash from MAC address
|
||||
* This must match what real Stalker clients send
|
||||
*/
|
||||
function generatePrehash(macAddress: string): string {
|
||||
async function generatePrehash(macAddress: string): Promise<string> {
|
||||
// Use MAC address with colons, uppercase - this is what most clients use
|
||||
const str = macAddress.toUpperCase();
|
||||
return sha1(str).toUpperCase();
|
||||
return (await sha1(str)).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,17 +37,10 @@ function generateRandom(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random serial number - 13 hex characters
|
||||
* Note: This is auto-generated and stored in the playlist for consistency
|
||||
* Deterministic serial number - 13 hex characters
|
||||
* Constant value for consistency across all sessions
|
||||
*/
|
||||
function generateSerialNumber(): string {
|
||||
const chars = '0123456789ABCDEF';
|
||||
let result = '';
|
||||
for (let i = 0; i < 13; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export const STALKER_SERIAL_NUMBER = 'BEDACD4569BAF';
|
||||
|
||||
export interface StalkerHandshakeResponse {
|
||||
js: {
|
||||
@@ -167,14 +77,17 @@ export interface StalkerProfileResponse {
|
||||
})
|
||||
export class StalkerSessionService {
|
||||
private dataService = inject(DataService);
|
||||
|
||||
|
||||
// In-memory token cache for current session (keyed by playlist ID)
|
||||
private tokenCache = new Map<string, string>();
|
||||
|
||||
|
||||
// Pending authentication promises to prevent race conditions
|
||||
// When multiple requests need a token simultaneously, they all wait for the same auth
|
||||
private pendingAuth = new Map<string, Promise<{ token: string; serialNumber?: string }>>();
|
||||
|
||||
private pendingAuth = new Map<
|
||||
string,
|
||||
Promise<{ token: string; serialNumber?: string }>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Generates a consistent 64-character device ID from MAC address
|
||||
*/
|
||||
@@ -193,36 +106,38 @@ export class StalkerSessionService {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a URL is a full stalker portal URL (requires handshake)
|
||||
* Full stalker portal URLs contain /stalker_portal/ in the path
|
||||
*/
|
||||
isFullStalkerPortal(url: string): boolean {
|
||||
return url.includes('/stalker_portal/') || url.includes('/server/load.php');
|
||||
return (
|
||||
url.includes('/stalker_portal/') || url.includes('/server/load.php')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the cached token for a playlist, or null if not cached
|
||||
*/
|
||||
getCachedToken(playlistId: string): string | null {
|
||||
return this.tokenCache.get(playlistId) || null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets a token in the cache
|
||||
*/
|
||||
setCachedToken(playlistId: string, token: string): void {
|
||||
this.tokenCache.set(playlistId, token);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears the cached token for a playlist (e.g., on auth failure)
|
||||
*/
|
||||
clearCachedToken(playlistId: string): void {
|
||||
this.tokenCache.delete(playlistId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs handshake to get a session token for a full stalker portal
|
||||
* Returns both the token and the random value for use in subsequent requests
|
||||
@@ -231,8 +146,8 @@ export class StalkerSessionService {
|
||||
portalUrl: string,
|
||||
macAddress: string
|
||||
): Promise<{ token: string; random: string }> {
|
||||
const prehash = generatePrehash(macAddress);
|
||||
|
||||
const prehash = await generatePrehash(macAddress);
|
||||
|
||||
const params: Record<string, string> = {
|
||||
type: 'stb',
|
||||
action: 'handshake',
|
||||
@@ -240,34 +155,41 @@ export class StalkerSessionService {
|
||||
prehash,
|
||||
JsHttpRequest: '1-xml',
|
||||
};
|
||||
|
||||
|
||||
const fullUrl = `${portalUrl}?type=stb&action=handshake&token=&prehash=${prehash}&JsHttpRequest=1-xml`;
|
||||
console.log('[StalkerSession] Performing handshake...');
|
||||
console.log('[StalkerSession] Handshake URL:', fullUrl);
|
||||
console.log('[StalkerSession] MAC Address:', macAddress);
|
||||
console.log('[StalkerSession] Prehash:', prehash);
|
||||
|
||||
|
||||
try {
|
||||
const response: StalkerHandshakeResponse = await this.dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
{
|
||||
const response: StalkerHandshakeResponse =
|
||||
await this.dataService.sendIpcEvent(STALKER_REQUEST, {
|
||||
url: portalUrl,
|
||||
macAddress,
|
||||
params,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] Handshake response:',
|
||||
JSON.stringify(response, null, 2)
|
||||
);
|
||||
|
||||
console.log('[StalkerSession] Handshake response:', JSON.stringify(response, null, 2));
|
||||
|
||||
|
||||
if (response?.js?.token) {
|
||||
console.log('[StalkerSession] Token received:', response.js.token.substring(0, 10) + '...');
|
||||
console.log('[StalkerSession] Random received:', response.js.random);
|
||||
return {
|
||||
token: response.js.token,
|
||||
random: response.js.random || generateRandom()
|
||||
console.log(
|
||||
'[StalkerSession] Token received:',
|
||||
response.js.token.substring(0, 10) + '...'
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Random received:',
|
||||
response.js.random
|
||||
);
|
||||
return {
|
||||
token: response.js.token,
|
||||
random: response.js.random || generateRandom(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
console.error('[StalkerSession] No token in response');
|
||||
throw new Error('Handshake failed: No token received');
|
||||
} catch (error) {
|
||||
@@ -275,7 +197,7 @@ export class StalkerSessionService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets account profile information to validate the portal and check subscription
|
||||
* Based on working implementation from stalker-to-m3u repo
|
||||
@@ -292,13 +214,14 @@ export class StalkerSessionService {
|
||||
providedSignature2?: string
|
||||
): Promise<StalkerProfileResponse> {
|
||||
// Use provided device IDs or generate from MAC (64 hex chars each)
|
||||
const deviceId1 = providedDeviceId1?.trim() || this.generateDeviceId(macAddress);
|
||||
const deviceId1 =
|
||||
providedDeviceId1?.trim() || this.generateDeviceId(macAddress);
|
||||
const deviceId2 = providedDeviceId2?.trim() || deviceId1;
|
||||
|
||||
|
||||
// Use provided signatures or empty string (some portals don't require them)
|
||||
const signature1 = providedSignature1?.trim() || '';
|
||||
const signature2 = providedSignature2?.trim() || '';
|
||||
|
||||
|
||||
// Build metrics JSON matching working app
|
||||
const metrics = {
|
||||
mac: macAddress,
|
||||
@@ -307,10 +230,10 @@ export class StalkerSessionService {
|
||||
random: handshakeRandom,
|
||||
sn: serialNumber,
|
||||
};
|
||||
|
||||
|
||||
// Generate prehash for get_profile (same as handshake)
|
||||
const prehash = generatePrehash(macAddress);
|
||||
|
||||
const prehash = await generatePrehash(macAddress);
|
||||
|
||||
// Profile request matching working StalkerTV app
|
||||
// auth_second_step=1, includes metrics, prehash, and device_id params
|
||||
const params: Record<string, string> = {
|
||||
@@ -331,35 +254,42 @@ export class StalkerSessionService {
|
||||
stb_type: '',
|
||||
JsHttpRequest: '1-xml',
|
||||
};
|
||||
|
||||
|
||||
console.log('[StalkerSession] Getting profile...');
|
||||
console.log('[StalkerSession] Profile URL:', portalUrl);
|
||||
console.log('[StalkerSession] Token:', token.substring(0, 10) + '...');
|
||||
console.log('[StalkerSession] Serial Number:', serialNumber);
|
||||
console.log('[StalkerSession] Device ID:', deviceId1.substring(0, 16) + '...');
|
||||
console.log('[StalkerSession] Signature:', signature1 ? signature1.substring(0, 16) + '...' : '(empty)');
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] Device ID:',
|
||||
deviceId1.substring(0, 16) + '...'
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Signature:',
|
||||
signature1 ? signature1.substring(0, 16) + '...' : '(empty)'
|
||||
);
|
||||
|
||||
try {
|
||||
const response: StalkerProfileResponse = await this.dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
{
|
||||
const response: StalkerProfileResponse =
|
||||
await this.dataService.sendIpcEvent(STALKER_REQUEST, {
|
||||
url: portalUrl,
|
||||
macAddress,
|
||||
params,
|
||||
token,
|
||||
serialNumber,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] Profile response:',
|
||||
JSON.stringify(response, null, 2)
|
||||
);
|
||||
|
||||
console.log('[StalkerSession] Profile response:', JSON.stringify(response, null, 2));
|
||||
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[StalkerSession] Get profile error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs do_auth to authenticate the session after handshake
|
||||
*/
|
||||
@@ -377,11 +307,11 @@ export class StalkerSessionService {
|
||||
device_id2: '',
|
||||
JsHttpRequest: '1-xml',
|
||||
};
|
||||
|
||||
|
||||
console.log('[StalkerSession] Performing do_auth...');
|
||||
console.log('[StalkerSession] do_auth URL:', portalUrl);
|
||||
console.log('[StalkerSession] Token:', token.substring(0, 10) + '...');
|
||||
|
||||
|
||||
try {
|
||||
const response = await this.dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
@@ -392,15 +322,18 @@ export class StalkerSessionService {
|
||||
token,
|
||||
}
|
||||
);
|
||||
|
||||
console.log('[StalkerSession] do_auth response:', JSON.stringify(response, null, 2));
|
||||
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] do_auth response:',
|
||||
JSON.stringify(response, null, 2)
|
||||
);
|
||||
|
||||
// do_auth returns { js: true } on success
|
||||
if (response?.js === true) {
|
||||
console.log('[StalkerSession] do_auth successful');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
console.warn('[StalkerSession] do_auth returned:', response?.js);
|
||||
return false;
|
||||
} catch (error) {
|
||||
@@ -408,7 +341,7 @@ export class StalkerSessionService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs full authentication flow: handshake -> get_profile (NO do_auth based on working traces)
|
||||
* Returns the token and account info if successful
|
||||
@@ -421,35 +354,80 @@ export class StalkerSessionService {
|
||||
deviceId2?: string,
|
||||
signature1?: string,
|
||||
signature2?: string
|
||||
): Promise<{ token: string; accountInfo?: StalkerProfileResponse['js']['account_info'] }> {
|
||||
): Promise<{
|
||||
token: string;
|
||||
accountInfo?: StalkerProfileResponse['js']['account_info'];
|
||||
}> {
|
||||
console.log('[StalkerSession] Starting full authentication flow...');
|
||||
console.log('[StalkerSession] Portal URL:', portalUrl);
|
||||
console.log('[StalkerSession] MAC Address:', macAddress);
|
||||
console.log('[StalkerSession] Serial Number:', serialNumber);
|
||||
console.log('[StalkerSession] Device ID 1:', deviceId1 ? deviceId1.substring(0, 16) + '... (provided)' : '(will be auto-generated)');
|
||||
console.log('[StalkerSession] Device ID 2:', deviceId2 ? deviceId2.substring(0, 16) + '... (provided)' : '(will be auto-generated)');
|
||||
console.log('[StalkerSession] Signature 1:', signature1 ? signature1.substring(0, 16) + '... (provided)' : '(empty)');
|
||||
console.log('[StalkerSession] Signature 2:', signature2 ? signature2.substring(0, 16) + '... (provided)' : '(empty)');
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] Device ID 1:',
|
||||
deviceId1
|
||||
? deviceId1.substring(0, 16) + '... (provided)'
|
||||
: '(will be auto-generated)'
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Device ID 2:',
|
||||
deviceId2
|
||||
? deviceId2.substring(0, 16) + '... (provided)'
|
||||
: '(will be auto-generated)'
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Signature 1:',
|
||||
signature1
|
||||
? signature1.substring(0, 16) + '... (provided)'
|
||||
: '(empty)'
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Signature 2:',
|
||||
signature2
|
||||
? signature2.substring(0, 16) + '... (provided)'
|
||||
: '(empty)'
|
||||
);
|
||||
|
||||
// Step 1: Handshake to get token and random
|
||||
console.log('[StalkerSession] Step 1: Performing handshake...');
|
||||
const { token, random } = await this.performHandshake(portalUrl, macAddress);
|
||||
console.log('[StalkerSession] Step 1 complete: Token and random obtained');
|
||||
|
||||
const { token, random } = await this.performHandshake(
|
||||
portalUrl,
|
||||
macAddress
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Step 1 complete: Token and random obtained'
|
||||
);
|
||||
|
||||
// Step 2: Get profile to activate token and get account info
|
||||
// The random from handshake must be used in auth_second_step
|
||||
console.log('[StalkerSession] Step 2: Getting profile with handshake random (activates token)...');
|
||||
console.log(
|
||||
'[StalkerSession] Step 2: Getting profile with handshake random (activates token)...'
|
||||
);
|
||||
try {
|
||||
const profileResponse = await this.getProfile(portalUrl, macAddress, token, serialNumber, random, deviceId1, deviceId2, signature1, signature2);
|
||||
|
||||
const profileResponse = await this.getProfile(
|
||||
portalUrl,
|
||||
macAddress,
|
||||
token,
|
||||
serialNumber,
|
||||
random,
|
||||
deviceId1,
|
||||
deviceId2,
|
||||
signature1,
|
||||
signature2
|
||||
);
|
||||
|
||||
// Check for profile-level errors
|
||||
if (profileResponse?.js?.msg || profileResponse?.js?.block_msg) {
|
||||
const errorMsg = profileResponse.js.msg || profileResponse.js.block_msg || 'Unknown profile error';
|
||||
const errorMsg =
|
||||
profileResponse.js.msg ||
|
||||
profileResponse.js.block_msg ||
|
||||
'Unknown profile error';
|
||||
console.error('[StalkerSession] Profile error:', errorMsg);
|
||||
throw new Error(`Profile error: ${errorMsg}`);
|
||||
}
|
||||
|
||||
console.log('[StalkerSession] Step 2 complete: Profile obtained, token activated');
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] Step 2 complete: Profile obtained, token activated'
|
||||
);
|
||||
console.log('[StalkerSession] Authentication successful!');
|
||||
return {
|
||||
token,
|
||||
@@ -461,55 +439,78 @@ export class StalkerSessionService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensures a valid token exists for a playlist, performing full auth if needed
|
||||
* IMPORTANT: Based on working traces, each session needs handshake + get_profile
|
||||
* Returns the token to use for requests, and the serial number to store
|
||||
*/
|
||||
async ensureToken(playlist: Playlist): Promise<{ token: string | null; serialNumber?: string }> {
|
||||
console.log('[StalkerSession] Ensuring token for playlist:', playlist._id);
|
||||
console.log('[StalkerSession] Is full stalker portal:', playlist.isFullStalkerPortal);
|
||||
|
||||
async ensureToken(
|
||||
playlist: Playlist
|
||||
): Promise<{ token: string | null; serialNumber?: string }> {
|
||||
console.log(
|
||||
'[StalkerSession] Ensuring token for playlist:',
|
||||
playlist._id
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Is full stalker portal:',
|
||||
playlist.isFullStalkerPortal
|
||||
);
|
||||
|
||||
// If not a full stalker portal, no token needed
|
||||
if (!playlist.isFullStalkerPortal) {
|
||||
console.log('[StalkerSession] Not a full stalker portal, no token needed');
|
||||
console.log(
|
||||
'[StalkerSession] Not a full stalker portal, no token needed'
|
||||
);
|
||||
return { token: null };
|
||||
}
|
||||
|
||||
|
||||
// Check in-memory cache first (valid for current session only)
|
||||
const cachedToken = this.getCachedToken(playlist._id);
|
||||
if (cachedToken) {
|
||||
console.log('[StalkerSession] Using cached token:', cachedToken.substring(0, 10) + '...');
|
||||
console.log(
|
||||
'[StalkerSession] Using cached token:',
|
||||
cachedToken.substring(0, 10) + '...'
|
||||
);
|
||||
return { token: cachedToken };
|
||||
}
|
||||
|
||||
|
||||
// Check if there's already a pending authentication for this playlist
|
||||
// This prevents race conditions when multiple resources request a token simultaneously
|
||||
const pendingPromise = this.pendingAuth.get(playlist._id);
|
||||
if (pendingPromise) {
|
||||
console.log('[StalkerSession] Waiting for pending authentication...');
|
||||
console.log(
|
||||
'[StalkerSession] Waiting for pending authentication...'
|
||||
);
|
||||
return pendingPromise;
|
||||
}
|
||||
|
||||
|
||||
// No cached token - need to do full authentication (handshake + get_profile)
|
||||
// Don't trust stored tokens as they may be from a different session
|
||||
if (!playlist.portalUrl || !playlist.macAddress) {
|
||||
console.error('[StalkerSession] Missing portal URL or MAC address');
|
||||
throw new Error('Portal URL and MAC address are required');
|
||||
}
|
||||
|
||||
|
||||
// Get or generate serial number - must be consistent for the MAC
|
||||
let serialNumber = playlist.stalkerSerialNumber;
|
||||
if (!serialNumber) {
|
||||
serialNumber = generateSerialNumber();
|
||||
console.log('[StalkerSession] Generated new serial number:', serialNumber);
|
||||
serialNumber = STALKER_SERIAL_NUMBER;
|
||||
console.log(
|
||||
'[StalkerSession] Generated new serial number:',
|
||||
serialNumber
|
||||
);
|
||||
} else {
|
||||
console.log('[StalkerSession] Using stored serial number:', serialNumber);
|
||||
console.log(
|
||||
'[StalkerSession] Using stored serial number:',
|
||||
serialNumber
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[StalkerSession] No cached token, performing full authentication...');
|
||||
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] No cached token, performing full authentication...'
|
||||
);
|
||||
|
||||
// Create the authentication promise and store it to prevent concurrent auth attempts
|
||||
// Use async/await wrapper to properly clean up on both success and failure
|
||||
const authPromise = (async () => {
|
||||
@@ -517,96 +518,51 @@ export class StalkerSessionService {
|
||||
const { token } = await this.authenticate(
|
||||
playlist.portalUrl,
|
||||
playlist.macAddress,
|
||||
serialNumber,
|
||||
playlist.stalkerDeviceId1,
|
||||
playlist.stalkerDeviceId2,
|
||||
playlist.stalkerSignature1,
|
||||
playlist.stalkerSignature2
|
||||
serialNumber
|
||||
);
|
||||
console.log(
|
||||
'[StalkerSession] Authentication complete, token cached'
|
||||
);
|
||||
console.log('[StalkerSession] Authentication complete, token cached');
|
||||
this.setCachedToken(playlist._id, token);
|
||||
return { token, serialNumber };
|
||||
} catch (error) {
|
||||
// Re-throw the error after cleanup
|
||||
throw error;
|
||||
} finally {
|
||||
// Clean up pending promise regardless of success/failure
|
||||
this.pendingAuth.delete(playlist._id);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// Store the pending promise so other concurrent requests can wait on it
|
||||
this.pendingAuth.set(playlist._id, authPromise);
|
||||
|
||||
|
||||
return authPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for making stalker requests with automatic token handling and retry on auth failure
|
||||
*/
|
||||
async makeRequest<T>(
|
||||
playlist: Playlist,
|
||||
params: Record<string, string>,
|
||||
retryOnAuthFailure = true
|
||||
): Promise<T> {
|
||||
const { token } = await this.ensureToken(playlist);
|
||||
|
||||
try {
|
||||
const response = await this.dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
{
|
||||
url: playlist.portalUrl,
|
||||
macAddress: playlist.macAddress,
|
||||
params,
|
||||
token,
|
||||
}
|
||||
);
|
||||
|
||||
// Check for authorization failure in response
|
||||
if (this.isAuthorizationError(response)) {
|
||||
if (retryOnAuthFailure && playlist.isFullStalkerPortal) {
|
||||
// Clear cached token and retry with new handshake
|
||||
this.clearCachedToken(playlist._id);
|
||||
return this.makeRequest<T>(playlist, params, false);
|
||||
}
|
||||
throw new Error('Authorization failed');
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Check if error indicates auth failure
|
||||
if (this.isAuthorizationError(error) && retryOnAuthFailure && playlist.isFullStalkerPortal) {
|
||||
// Clear cached token and retry with new handshake
|
||||
this.clearCachedToken(playlist._id);
|
||||
return this.makeRequest<T>(playlist, params, false);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a response or error indicates an authorization failure
|
||||
*/
|
||||
private isAuthorizationError(responseOrError: unknown): boolean {
|
||||
if (!responseOrError) return false;
|
||||
|
||||
|
||||
const response = responseOrError as Record<string, unknown>;
|
||||
|
||||
|
||||
// Convert response to string for pattern matching
|
||||
const responseStr = JSON.stringify(responseOrError).toLowerCase();
|
||||
|
||||
|
||||
// Check for "Authorization failed. XX" pattern (like "Authorization failed. 75")
|
||||
if (/authorization\s*failed\.?\s*\d*/i.test(responseStr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for common auth failure indicators
|
||||
const jsData = response?.['js'] as Record<string, unknown>;
|
||||
const errorMessage = (response?.['message'] as string)?.toLowerCase?.() ||
|
||||
jsData?.['error']?.toString?.().toLowerCase?.() ||
|
||||
jsData?.['msg']?.toString?.().toLowerCase?.() ||
|
||||
'';
|
||||
|
||||
const errorMessage =
|
||||
(response?.['message'] as string)?.toLowerCase?.() ||
|
||||
jsData?.['error']?.toString?.().toLowerCase?.() ||
|
||||
jsData?.['msg']?.toString?.().toLowerCase?.() ||
|
||||
'';
|
||||
|
||||
return (
|
||||
errorMessage.includes('authorization') ||
|
||||
errorMessage.includes('unauthorized') ||
|
||||
@@ -616,7 +572,7 @@ export class StalkerSessionService {
|
||||
jsData?.['error'] === 'Authorization failed'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for making stalker requests with automatic token handling and retry on auth failure
|
||||
* This should be used by all stalker API calls to ensure proper auth handling
|
||||
@@ -629,35 +585,68 @@ export class StalkerSessionService {
|
||||
// Get token (will wait if auth is in progress)
|
||||
const { token } = await this.ensureToken(playlist);
|
||||
const serialNumber = playlist.stalkerSerialNumber;
|
||||
|
||||
console.log('[StalkerSession] Making authenticated request:', params['action']);
|
||||
|
||||
const response = await this.dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
{
|
||||
url: playlist.portalUrl,
|
||||
macAddress: playlist.macAddress,
|
||||
params,
|
||||
token,
|
||||
serialNumber,
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[StalkerSession] Making authenticated request:',
|
||||
params['action']
|
||||
);
|
||||
|
||||
// Check for authorization failure in response
|
||||
if (this.isAuthorizationError(response)) {
|
||||
console.warn('[StalkerSession] Authorization error detected in response:', response);
|
||||
|
||||
if (retryOnAuthFailure && playlist.isFullStalkerPortal) {
|
||||
console.log('[StalkerSession] Clearing token and retrying with fresh auth...');
|
||||
// Clear cached token to force re-authentication
|
||||
this.clearCachedToken(playlist._id);
|
||||
// Retry once with fresh authentication
|
||||
return this.makeAuthenticatedRequest<T>(playlist, params, false);
|
||||
|
||||
try {
|
||||
const response = await this.dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
{
|
||||
url: playlist.portalUrl,
|
||||
macAddress: playlist.macAddress,
|
||||
params,
|
||||
token,
|
||||
serialNumber,
|
||||
}
|
||||
);
|
||||
|
||||
// Check for authorization failure in response
|
||||
if (this.isAuthorizationError(response)) {
|
||||
console.warn(
|
||||
'[StalkerSession] Authorization error detected in response:',
|
||||
response
|
||||
);
|
||||
|
||||
if (retryOnAuthFailure && playlist.isFullStalkerPortal) {
|
||||
console.log(
|
||||
'[StalkerSession] Clearing token and retrying with fresh auth...'
|
||||
);
|
||||
// Clear cached token to force re-authentication
|
||||
this.clearCachedToken(playlist._id);
|
||||
// Retry once with fresh authentication
|
||||
return this.makeAuthenticatedRequest<T>(
|
||||
playlist,
|
||||
params,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('Authorization failed after retry');
|
||||
}
|
||||
|
||||
throw new Error('Authorization failed after retry');
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Check if error indicates auth failure
|
||||
if (
|
||||
this.isAuthorizationError(error) &&
|
||||
retryOnAuthFailure &&
|
||||
playlist.isFullStalkerPortal
|
||||
) {
|
||||
console.log(
|
||||
'[StalkerSession] Clearing token and retrying with fresh auth (caught error)...'
|
||||
);
|
||||
// Clear cached token and retry with new handshake
|
||||
this.clearCachedToken(playlist._id);
|
||||
return this.makeAuthenticatedRequest<T>(
|
||||
playlist,
|
||||
params,
|
||||
false
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user