mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(electron): preserve playlist header defaults
Keep playlist-level request header overrides active when channel-scoped headers are cleared, and cover layered override behavior.
This commit is contained in:
1 parent
fc449ca73e
commit
3c5f7ab92e
7 files changed
+146
-35
No files matched your search
@@ -57,6 +57,9 @@ describe('Electron app security helpers', () => {
|
||||
expect(
|
||||
isTrustedRendererNavigationUrl('http://127.0.0.1:4200/home', true)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isTrustedRendererNavigationUrl('http://[::1]:4200/home', true)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isTrustedRendererNavigationUrl('http://localhost:4300/home', true)
|
||||
).toBe(false);
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
import { store, WINDOW_BOUNDS } from './services/store.service';
|
||||
|
||||
const externalBrowserProtocols = new Set(['http:', 'https:']);
|
||||
const trustedDevRendererHosts = new Set([
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'[::1]',
|
||||
'::1',
|
||||
]);
|
||||
|
||||
function parseUrl(url: string): URL | null {
|
||||
try {
|
||||
@@ -45,7 +51,7 @@ export function isTrustedRendererNavigationUrl(
|
||||
|
||||
return (
|
||||
parsedUrl.protocol === 'http:' &&
|
||||
['localhost', '127.0.0.1', '[::1]'].includes(parsedUrl.hostname) &&
|
||||
trustedDevRendererHosts.has(parsedUrl.hostname) &&
|
||||
parsedUrl.port === String(rendererAppPort)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import {
|
||||
clearRequestHeaderOverride,
|
||||
configureRequestHeaderOverride,
|
||||
} from '../services/request-header-overrides.service';
|
||||
import { configureRequestHeaderOverride } from '../services/request-header-overrides.service';
|
||||
|
||||
export default class SharedEvents {
|
||||
static bootstrapSharedEvents(): Electron.IpcMain {
|
||||
@@ -26,10 +23,5 @@ export function setUserAgent(
|
||||
referer?: string | null,
|
||||
scopeUrl?: string | null
|
||||
): void {
|
||||
if (!userAgent?.trim() && !referer?.trim()) {
|
||||
clearRequestHeaderOverride();
|
||||
return;
|
||||
}
|
||||
|
||||
configureRequestHeaderOverride(userAgent, referer, scopeUrl);
|
||||
}
|
||||
@@ -111,10 +111,97 @@ describe('request header overrides', () => {
|
||||
expect(headers).toEqual({ Accept: '*/*' });
|
||||
});
|
||||
|
||||
it('applies playlist-level headers broadly when no stream scope is provided', async () => {
|
||||
const { configureRequestHeaderOverride } =
|
||||
await import('./request-header-overrides.service');
|
||||
|
||||
configureRequestHeaderOverride(
|
||||
'PlaylistAgent/1.0',
|
||||
'https://portal.example/referrer'
|
||||
);
|
||||
|
||||
const listener = mockOnBeforeSendHeaders.mock.calls[0][1];
|
||||
const headers = runHeaderListener(
|
||||
listener,
|
||||
'https://cdn.example/segment.ts'
|
||||
);
|
||||
|
||||
expect(headers).toEqual({
|
||||
Origin: 'https://portal.example',
|
||||
Referer: 'https://portal.example/referrer',
|
||||
'User-Agent': 'PlaylistAgent/1.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps playlist headers when a channel without headers clears scoped overrides', async () => {
|
||||
const { configureRequestHeaderOverride } =
|
||||
await import('./request-header-overrides.service');
|
||||
|
||||
configureRequestHeaderOverride(
|
||||
'PlaylistAgent/1.0',
|
||||
'https://portal.example/referrer'
|
||||
);
|
||||
configureRequestHeaderOverride(
|
||||
'ChannelAgent/2.0',
|
||||
'https://channel.example/referrer',
|
||||
'https://stream.example/live.m3u8'
|
||||
);
|
||||
configureRequestHeaderOverride(
|
||||
null,
|
||||
null,
|
||||
'https://stream.example/next.m3u8'
|
||||
);
|
||||
|
||||
expect(mockOnBeforeSendHeaders).toHaveBeenCalledTimes(1);
|
||||
|
||||
const listener = mockOnBeforeSendHeaders.mock.calls[0][1];
|
||||
const headers = runHeaderListener(
|
||||
listener,
|
||||
'https://stream.example/segment.ts'
|
||||
);
|
||||
|
||||
expect(headers).toEqual({
|
||||
Origin: 'https://portal.example',
|
||||
Referer: 'https://portal.example/referrer',
|
||||
'User-Agent': 'PlaylistAgent/1.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('layers scoped channel headers over playlist defaults', async () => {
|
||||
const { configureRequestHeaderOverride } =
|
||||
await import('./request-header-overrides.service');
|
||||
|
||||
configureRequestHeaderOverride(
|
||||
'PlaylistAgent/1.0',
|
||||
'https://portal.example/referrer'
|
||||
);
|
||||
configureRequestHeaderOverride(
|
||||
'ChannelAgent/2.0',
|
||||
null,
|
||||
'https://stream.example/live.m3u8'
|
||||
);
|
||||
|
||||
const listener = mockOnBeforeSendHeaders.mock.calls[0][1];
|
||||
const headers = runHeaderListener(
|
||||
listener,
|
||||
'https://stream.example/segment.ts'
|
||||
);
|
||||
|
||||
expect(headers).toEqual({
|
||||
Origin: 'https://portal.example',
|
||||
Referer: 'https://portal.example/referrer',
|
||||
'User-Agent': 'ChannelAgent/2.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('clears active header overrides without registering another listener', async () => {
|
||||
const { clearRequestHeaderOverride, configureRequestHeaderOverride } =
|
||||
await import('./request-header-overrides.service');
|
||||
|
||||
configureRequestHeaderOverride(
|
||||
'PlaylistAgent/1.0',
|
||||
'https://portal.example/referrer'
|
||||
);
|
||||
configureRequestHeaderOverride(
|
||||
'ScopedAgent/1.0',
|
||||
'https://portal.example/referrer',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { session } from 'electron';
|
||||
type HeaderOverride = {
|
||||
origin?: string;
|
||||
referer?: string;
|
||||
scopeOrigins: Set<string>;
|
||||
scopeOrigins?: Set<string>;
|
||||
userAgent?: string;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ const headerOverrideUrlFilter = {
|
||||
};
|
||||
|
||||
let activeHeaderOverride: HeaderOverride | null = null;
|
||||
let activeScopedHeaderOverride: HeaderOverride | null = null;
|
||||
let listenerRegistered = false;
|
||||
|
||||
function normalizeHeaderValue(value?: string | null): string | undefined {
|
||||
@@ -34,7 +35,7 @@ function getOrigin(value?: string | null): string | undefined {
|
||||
}
|
||||
|
||||
function shouldApplyOverride(url: string, override: HeaderOverride): boolean {
|
||||
if (override.scopeOrigins.size === 0) {
|
||||
if (!override.scopeOrigins) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,23 +65,28 @@ function handleBeforeSendHeaders(
|
||||
callback: (beforeSendResponse: Electron.BeforeSendResponse) => void
|
||||
): void {
|
||||
const requestHeaders = { ...details.requestHeaders };
|
||||
const override = activeHeaderOverride;
|
||||
const overrides = [activeHeaderOverride, activeScopedHeaderOverride].filter(
|
||||
(override): override is HeaderOverride =>
|
||||
Boolean(override && shouldApplyOverride(details.url, override))
|
||||
);
|
||||
|
||||
if (!override || !shouldApplyOverride(details.url, override)) {
|
||||
if (overrides.length === 0) {
|
||||
callback({ requestHeaders });
|
||||
return;
|
||||
}
|
||||
|
||||
if (override.userAgent) {
|
||||
setRequestHeader(requestHeaders, 'User-Agent', override.userAgent);
|
||||
}
|
||||
for (const override of overrides) {
|
||||
if (override.userAgent) {
|
||||
setRequestHeader(requestHeaders, 'User-Agent', override.userAgent);
|
||||
}
|
||||
|
||||
if (override.referer) {
|
||||
setRequestHeader(requestHeaders, 'Referer', override.referer);
|
||||
}
|
||||
if (override.referer) {
|
||||
setRequestHeader(requestHeaders, 'Referer', override.referer);
|
||||
}
|
||||
|
||||
if (override.origin) {
|
||||
setRequestHeader(requestHeaders, 'Origin', override.origin);
|
||||
if (override.origin) {
|
||||
setRequestHeader(requestHeaders, 'Origin', override.origin);
|
||||
}
|
||||
}
|
||||
|
||||
callback({ requestHeaders });
|
||||
@@ -105,29 +111,44 @@ export function configureRequestHeaderOverride(
|
||||
): void {
|
||||
const normalizedUserAgent = normalizeHeaderValue(userAgent);
|
||||
const normalizedReferer = normalizeHeaderValue(referer);
|
||||
const isScopedOverride = scopeUrl !== undefined && scopeUrl !== null;
|
||||
|
||||
if (!normalizedUserAgent && !normalizedReferer) {
|
||||
clearRequestHeaderOverride();
|
||||
if (isScopedOverride) {
|
||||
clearScopedRequestHeaderOverride();
|
||||
} else {
|
||||
clearRequestHeaderOverride();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const refererOrigin = getOrigin(normalizedReferer);
|
||||
const scopeOrigin = getOrigin(scopeUrl);
|
||||
const scopeOrigins = new Set(
|
||||
[scopeOrigin, refererOrigin].filter((origin): origin is string =>
|
||||
Boolean(origin)
|
||||
)
|
||||
);
|
||||
|
||||
activeHeaderOverride = {
|
||||
const override: HeaderOverride = {
|
||||
origin: refererOrigin,
|
||||
referer: normalizedReferer,
|
||||
scopeOrigins,
|
||||
userAgent: normalizedUserAgent,
|
||||
};
|
||||
|
||||
if (isScopedOverride) {
|
||||
override.scopeOrigins = new Set(
|
||||
[scopeOrigin, refererOrigin].filter((origin): origin is string =>
|
||||
Boolean(origin)
|
||||
)
|
||||
);
|
||||
activeScopedHeaderOverride = override;
|
||||
} else {
|
||||
activeHeaderOverride = override;
|
||||
}
|
||||
|
||||
ensureHeaderOverrideListener();
|
||||
}
|
||||
|
||||
export function clearRequestHeaderOverride(): void {
|
||||
activeHeaderOverride = null;
|
||||
activeScopedHeaderOverride = null;
|
||||
}
|
||||
|
||||
function clearScopedRequestHeaderOverride(): void {
|
||||
activeScopedHeaderOverride = null;
|
||||
}
|
||||
@@ -58,12 +58,14 @@ scopeUrl)`.
|
||||
The Electron backend handles that IPC in `apps/electron-backend/src/app/events/shared.events.ts`
|
||||
and delegates to `apps/electron-backend/src/app/services/request-header-overrides.service.ts`.
|
||||
The service registers one `session.defaultSession.webRequest.onBeforeSendHeaders`
|
||||
listener and updates the active override in memory instead of stacking a new
|
||||
listener and updates layered in-memory overrides instead of stacking a new
|
||||
listener for every channel change.
|
||||
|
||||
Rules:
|
||||
|
||||
- empty `userAgent` and empty `referer` clear the active override
|
||||
- empty playlist-level `userAgent` and `referer` clear all active overrides
|
||||
- empty channel-level `userAgent` and `referer` with a `scopeUrl` clear only
|
||||
the scoped channel override, preserving playlist-level defaults
|
||||
- channel playback should pass the stream URL as `scopeUrl`
|
||||
- scoped overrides apply only to the active stream origin and referer origin
|
||||
- playlist-level user agents and referrers may call the bridge without a
|
||||
|
||||
@@ -161,8 +161,8 @@ export class PlaylistEffects {
|
||||
}
|
||||
|
||||
window.electron?.setUserAgent(
|
||||
channel.http['user-agent'],
|
||||
channel.http.referrer,
|
||||
channel.http?.['user-agent'],
|
||||
channel.http?.referrer,
|
||||
channel.url
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user