diff --git a/apps/electron-backend/src/app/events/xtream.events.spec.ts b/apps/electron-backend/src/app/events/xtream.events.spec.ts index 837d7da2b..5a6840fe0 100644 --- a/apps/electron-backend/src/app/events/xtream.events.spec.ts +++ b/apps/electron-backend/src/app/events/xtream.events.spec.ts @@ -86,6 +86,45 @@ describe('XtreamEvents session cancellation', () => { expect(requestedUrl.searchParams.get('username')).toBe('user'); }); + it('sends a player-style User-Agent instead of a truncated browser string', async () => { + // Regression: some Xtream panels sit behind a WAF (e.g. Cloudflare) + // that challenges generic/incomplete browser User-Agents but + // allowlists known IPTV player clients. The previous hardcoded + // 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + // string was rejected by such panels even though curl/Safari worked. + const requestHandler = registeredHandlers.get('XTREAM_REQUEST'); + expect(requestHandler).toBeDefined(); + + axiosMock.mockResolvedValue({ + status: 200, + data: { user_info: { auth: 1, status: 'Active' } }, + headers: {}, + }); + + await requestHandler?.( + {}, + { + url: 'https://example.com', + params: { + action: 'get_account_info', + password: 'pass', + username: 'user', + }, + suppressErrorLog: true, + } + ); + + const requestConfig = axiosMock.mock.calls[0][0] as { + headers?: Record; + }; + const userAgent = requestConfig.headers?.['User-Agent']; + expect(userAgent).toBeDefined(); + expect(userAgent).not.toBe( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + ); + expect(userAgent).not.toMatch(/^Mozilla\/5\.0 /); + }); + it('follows validated redirects for range GET media probes', async () => { const probeHandler = registeredHandlers.get('XTREAM_PROBE_URL'); const destroyProbeBody = jest.fn(); @@ -129,6 +168,9 @@ describe('XtreamEvents session cancellation', () => { expect(firstRequest.headers).toEqual( expect.objectContaining({ Range: 'bytes=0-4095' }) ); + expect(firstRequest.headers?.['User-Agent']).not.toBe( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + ); expect(firstRequest.maxRedirects).toBe(0); expect(firstRequest.responseType).toBe('stream'); expect(destroyProbeBody).toHaveBeenCalledTimes(1); diff --git a/apps/electron-backend/src/app/events/xtream.events.ts b/apps/electron-backend/src/app/events/xtream.events.ts index c712d12f3..da96caa5e 100644 --- a/apps/electron-backend/src/app/events/xtream.events.ts +++ b/apps/electron-backend/src/app/events/xtream.events.ts @@ -14,6 +14,11 @@ import { emitPortalDebugEvent } from './portal-debug.events'; import { UnsafeUrlError } from './url-safety'; import { requestWithValidatedRedirects } from '../util/validated-axios'; +// Some Xtream panels sit behind Cloudflare (or similar WAFs) configured to +// challenge generic browser-looking User-Agents while allowlisting known +// IPTV player clients. A VLC-style User-Agent reliably passes those checks. +const XTREAM_CLIENT_USER_AGENT = 'VLC/3.0.18 LibVLC/3.0.18'; + export default class XtreamEvents { static bootstrapXtreamEvents(): Electron.IpcMain { return ipcMain; @@ -119,8 +124,7 @@ ipcMain.handle( method: 'GET', url: apiUrl.toString(), headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'User-Agent': XTREAM_CLIENT_USER_AGENT, Accept: 'application/json', }, timeout: 30000, // 30 seconds timeout for Xtream API @@ -194,8 +198,7 @@ ipcMain.handle( method: 'GET', url: apiUrl, headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'User-Agent': XTREAM_CLIENT_USER_AGENT, Accept: 'application/json', }, timeout: 30000, @@ -304,8 +307,7 @@ ipcMain.handle( method, url: payload.url, headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'User-Agent': XTREAM_CLIENT_USER_AGENT, ...(method === 'GET' ? { Range: 'bytes=0-4095' } : {}), }, timeout: 10000, diff --git a/docs/architecture/xtream-portal-compatibility.md b/docs/architecture/xtream-portal-compatibility.md index ec1f7b5e8..4572bc1cb 100644 --- a/docs/architecture/xtream-portal-compatibility.md +++ b/docs/architecture/xtream-portal-compatibility.md @@ -62,6 +62,19 @@ targets. Those targets are validated when registered and revalidated before the `/xtream` proxy request, including protocol, URL credentials, DNS resolution, and private-network checks. +## User-Agent + +Electron's `XTREAM_REQUEST` and `XTREAM_PROBE_URL` handlers +(`apps/electron-backend/src/app/events/xtream.events.ts`) send a shared +`XTREAM_CLIENT_USER_AGENT` constant on every outgoing request. Some Xtream +panels sit behind a WAF (e.g. Cloudflare) configured to challenge +generic/incomplete browser-looking User-Agents while allowlisting known IPTV +player clients; a player-style User-Agent (currently a VLC signature) avoids +that challenge page, whereas a browser-looking but non-browser TLS/HTTP client +(axios/curl with a Chrome or empty User-Agent) can be blocked even though a +real browser or a VLC-style client passes. Keep all three request sites using +the shared constant instead of inlining the string again. + ## Playback URL Formats When account info includes `user_info.allowed_output_formats`, the current