From 6e81d7034d77df10bf83aa61062347f2fbd04ccc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 19:53:41 +0000 Subject: [PATCH] fix(playlist): strip panel_api.php endpoints, path-aware portal ambiguity for tenant installs - normalizeXtreamServerUrl strips panel_api.php like the other API endpoints, so a panel_api.php handout (pasted or auto-detected) prefills the server base instead of a URL the transport would double-suffix. - The multi-MAC portal ambiguity key is origin plus the installation base path: tenant installs sharing one origin (/a/stalker_portal/c/ vs /b/...) no longer collapse, while alternate endpoints of one install (/c/, portal.php, server/load.php, Real/Panel pairs) still compare equal. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4 --- .../provider-import-detection.util.spec.ts | 50 +++++++++++++++++++ .../src/lib/provider-import-detection.util.ts | 25 ++++++++-- .../interfaces/src/lib/xtream-portal.utils.ts | 5 +- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts index 13af6d886..708a8f7ae 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts @@ -315,6 +315,20 @@ describe('detectProviderImportCandidates', () => { expect(xtream[0].serverUrl).toBe('http://panel.example.io:8080'); }); + it('strips panel_api.php from the derived server URL', () => { + const candidates = detectProviderImportCandidates( + [ + 'http://panel.example.org/panel_api.php', + 'User: alice', + 'Pass: s3cret', + ].join('\n') + ); + + const xtream = only(candidates, 'xtream'); + expect(xtream).toHaveLength(1); + expect(xtream[0].serverUrl).toBe('http://panel.example.org'); + }); + it('treats credentials in the query of a generic URL as xtream', () => { const candidates = detectProviderImportCandidates( 'http://portal.example.io/api?username=eve&password=pw&foo=bar' @@ -699,6 +713,42 @@ describe('detectProviderImportCandidates', () => { } }); + it('treats tenant installs under different base paths as different panels', () => { + const candidates = detectProviderImportCandidates( + [ + 'http://panel.example.com/a/stalker_portal/c/', + 'MAC: 00:1A:79:AA:AA:AA', + 'http://panel.example.com/b/stalker_portal/c/', + 'MAC: 00:1A:79:BB:BB:BB', + ].join('\n') + ); + + const stalker = only(candidates, 'stalker'); + expect(stalker).toHaveLength(2); + for (const candidate of stalker) { + expect(candidate.portalUrl).toBeUndefined(); + } + }); + + it('keeps alternate endpoints of one install unambiguous for a MAC list', () => { + // /c/ and portal.php are two doors into the same installation — + // a MAC list next to both must still get the shared portal. + const candidates = detectProviderImportCandidates( + [ + 'http://panel.example.com/c/', + 'http://panel.example.com/portal.php', + 'MAC: 00:1A:79:AA:AA:AA', + 'MAC: 00:1A:79:BB:BB:BB', + ].join('\n') + ); + + const stalker = only(candidates, 'stalker'); + expect(stalker).toHaveLength(2); + for (const candidate of stalker) { + expect(candidate.portalUrl).toBe('http://panel.example.com/c/'); + } + }); + it('treats same-host portals on different ports as different panels', () => { const candidates = detectProviderImportCandidates( [ diff --git a/libs/shared/interfaces/src/lib/provider-import-detection.util.ts b/libs/shared/interfaces/src/lib/provider-import-detection.util.ts index a3a96db18..77efa4021 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.ts @@ -135,13 +135,9 @@ export function detectProviderImportCandidates( : labeledHostUrl(labeled) !== undefined ? [] : urls.filter((url) => url.role === 'generic'); - // Compared by ORIGIN, not hostname: two panels on one DNS name but - // different ports are different panels, while URL normalization drops a - // default port, so the Real/Panel `:80`-vs-bare pairs of scanner dumps - // still compare equal. const portalAmbiguous = macs.length > 1 && - new Set(portalPool.map((url) => url.parsed.origin)).size > 1; + new Set(portalPool.map(portalInstallationKey)).size > 1; const portal = portalAmbiguous ? null : pickStalkerPortalUrl(urls, labeled); @@ -276,6 +272,25 @@ export function detectProviderImportCandidates( return sortByConfidence(dedupe(candidates)).slice(0, MAX_CANDIDATES); } +/** + * Ambiguity key for a portal candidate: origin plus the installation's base + * path. Origin (not hostname) separates panels on different ports while URL + * normalization drops default ports, so the Real/Panel `:80`-vs-bare pairs + * of scanner dumps compare equal. The base path separates tenant installs + * sharing one origin (`/a/stalker_portal/c/` vs `/b/…`), while the known + * endpoint suffixes of ONE install (`/c/`, `portal.php`, `server/load.php`) + * are stripped so its alternate endpoints reduce to the same key. + */ +function portalInstallationKey(url: DetectedUrl): string { + const base = url.parsed.pathname + .replace(/\/(?:stalker_portal\/)?c\/?$/i, '') + .replace(/\/stalker_portal\/?$/i, '') + .replace(/\/portal\.php$/i, '') + .replace(/\/(?:stalker_portal\/)?server\/load\.php$/i, '') + .replace(/\/+$/, ''); + return `${url.parsed.origin}${base}`; +} + function pickStalkerPortalUrl( urls: DetectedUrl[], labeled: LabeledFields diff --git a/libs/shared/interfaces/src/lib/xtream-portal.utils.ts b/libs/shared/interfaces/src/lib/xtream-portal.utils.ts index d158b0a8a..7843e640a 100644 --- a/libs/shared/interfaces/src/lib/xtream-portal.utils.ts +++ b/libs/shared/interfaces/src/lib/xtream-portal.utils.ts @@ -14,7 +14,10 @@ export interface XtreamCredentialsFromUrl { username: string; } -const XTREAM_API_ENDPOINT_PATTERN = /\/(?:get|player_api)\.php$/i; +// panel_api.php is the legacy admin-flavored endpoint some panels hand out; +// it identifies an Xtream server just like the other two and must be +// stripped the same way, or the transport would append /player_api.php to it. +const XTREAM_API_ENDPOINT_PATTERN = /\/(?:get|player_api|panel_api)\.php$/i; // Some Xtream panels challenge generic Node HTTP clients while allowing // established IPTV players. Keep API, probe, and download requests aligned.