mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
[codex] Add scoped EPG security trust controls (#1054)
* Add scoped EPG security trust controls * fix: address scoped trust review feedback --------- Co-authored-by: 4gray <fourgray@proton.me>
This commit is contained in:
1 parent
9043116ebd
commit
e8aa7c3a34
56 files changed
+1683
-243
No files matched your search
@@ -25,6 +25,7 @@ export * from './lib/playlist.interface';
|
||||
export * from './lib/portal-activity-item.interface';
|
||||
export * from './lib/portal-debug.interface';
|
||||
export * from './lib/portal-playback.interface';
|
||||
export * from './lib/security-policy-error.utils';
|
||||
export * from './lib/settings.interface';
|
||||
export * from './lib/stalker-portal-actions.enum';
|
||||
export * from './lib/store-keys.enum';
|
||||
|
||||
@@ -67,6 +67,14 @@ export const ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES = {
|
||||
export type ElectronBridgeEpgProgressStatus =
|
||||
(typeof ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES)[keyof typeof ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES];
|
||||
|
||||
export const ELECTRON_BRIDGE_SECURITY_ERROR_CODES = {
|
||||
EpgPrivateNetworkBlocked: 'epg-private-network-blocked',
|
||||
InvalidTlsCertificate: 'invalid-tls-certificate',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeSecurityErrorCode =
|
||||
(typeof ELECTRON_BRIDGE_SECURITY_ERROR_CODES)[keyof typeof ELECTRON_BRIDGE_SECURITY_ERROR_CODES];
|
||||
|
||||
export const ELECTRON_BRIDGE_DB_OPERATION_STATUSES = {
|
||||
Cancelled: 'cancelled',
|
||||
Completed: 'completed',
|
||||
@@ -182,6 +190,11 @@ export interface ElectronBridgeEpgFetchResult extends ElectronBridgeResult {
|
||||
skipped?: string[];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeTrustOptions {
|
||||
trustedPrivateNetworkEpgUrls?: string[];
|
||||
trustedInsecureTlsHosts?: string[];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgFreshnessResult {
|
||||
staleUrls: string[];
|
||||
freshUrls: string[];
|
||||
@@ -197,6 +210,8 @@ export interface ElectronBridgeEpgProgress {
|
||||
status: ElectronBridgeEpgProgressStatus;
|
||||
stats?: ElectronBridgeEpgProgressStats;
|
||||
error?: string;
|
||||
errorCode?: ElectronBridgeSecurityErrorCode;
|
||||
errorHost?: string;
|
||||
queuePosition?: number;
|
||||
}
|
||||
|
||||
@@ -437,7 +452,11 @@ export interface ElectronBridgeApi {
|
||||
onWindowStateChange: (
|
||||
callback: (state: ElectronBridgeWindowState) => void
|
||||
) => () => void;
|
||||
fetchPlaylistByUrl: (url: string, title?: string) => Promise<Playlist>;
|
||||
fetchPlaylistByUrl: (
|
||||
url: string,
|
||||
title?: string,
|
||||
options?: ElectronBridgeTrustOptions
|
||||
) => Promise<Playlist>;
|
||||
updatePlaylistFromFilePath: (
|
||||
filePath: string,
|
||||
title: string
|
||||
@@ -479,8 +498,14 @@ export interface ElectronBridgeApi {
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => Promise<ExternalPlayerSession>;
|
||||
autoUpdatePlaylists: (playlists: Playlist[]) => Promise<Playlist[]>;
|
||||
fetchEpg: (urls: string[]) => Promise<ElectronBridgeEpgFetchResult>;
|
||||
autoUpdatePlaylists: (
|
||||
playlists: Playlist[],
|
||||
options?: ElectronBridgeTrustOptions
|
||||
) => Promise<Playlist[]>;
|
||||
fetchEpg: (
|
||||
urls: string[],
|
||||
options?: ElectronBridgeTrustOptions
|
||||
) => Promise<ElectronBridgeEpgFetchResult>;
|
||||
getChannelPrograms: (channelId: string) => Promise<EpgProgram[]>;
|
||||
getCurrentProgramsBatch: (
|
||||
channelIds: string[]
|
||||
@@ -493,7 +518,10 @@ export interface ElectronBridgeApi {
|
||||
skip: number,
|
||||
limit: number
|
||||
) => Promise<ElectronBridgeEpgChannelWithPrograms[]>;
|
||||
forceFetchEpg: (url: string) => Promise<ElectronBridgeEpgFetchResult>;
|
||||
forceFetchEpg: (
|
||||
url: string,
|
||||
options?: ElectronBridgeTrustOptions
|
||||
) => Promise<ElectronBridgeEpgFetchResult>;
|
||||
clearEpgData: () => Promise<ElectronBridgeResult>;
|
||||
checkEpgFreshness: (
|
||||
urls: string[],
|
||||
|
||||
@@ -25,4 +25,5 @@ export interface PlaylistRefreshPayload {
|
||||
title: string;
|
||||
filePath?: string;
|
||||
url?: string;
|
||||
trustedInsecureTlsHosts?: string[];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
normalizeHost,
|
||||
parseSecurityPolicyError,
|
||||
SECURITY_ERROR_PREFIX,
|
||||
} from './security-policy-error.utils';
|
||||
|
||||
describe('security-policy-error utils', () => {
|
||||
it('parses plain serialized security errors', () => {
|
||||
expect(
|
||||
parseSecurityPolicyError(
|
||||
`${SECURITY_ERROR_PREFIX}${JSON.stringify({
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message: 'Certificate for this playlist host is invalid.',
|
||||
})}`
|
||||
)
|
||||
).toEqual({
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message: 'Certificate for this playlist host is invalid.',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses security errors wrapped by Electron ipcRenderer.invoke', () => {
|
||||
expect(
|
||||
parseSecurityPolicyError(
|
||||
new Error(
|
||||
`Error invoking remote method 'FETCH_PLAYLIST_BY_URL': Error: ${SECURITY_ERROR_PREFIX}${JSON.stringify(
|
||||
{
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message:
|
||||
'Certificate for this playlist host is invalid.',
|
||||
}
|
||||
)}`
|
||||
)
|
||||
)
|
||||
).toEqual({
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message: 'Certificate for this playlist host is invalid.',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores malformed security payloads', () => {
|
||||
expect(
|
||||
parseSecurityPolicyError(
|
||||
`${SECURITY_ERROR_PREFIX}{"code":"INVALID_TLS_CERTIFICATE"}`
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('normalizes host values for trust comparisons', () => {
|
||||
expect(normalizeHost(' [EXAMPLE.Local] ')).toBe('example.local');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
export const SECURITY_ERROR_PREFIX = 'IPTVNATOR_SECURITY_ERROR:';
|
||||
|
||||
export interface SerializedSecurityError {
|
||||
readonly code: string;
|
||||
readonly host?: string;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
function readErrorMessage(error: unknown): string | undefined {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error && typeof error === 'object') {
|
||||
const message = (error as { readonly message?: unknown }).message;
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseSecurityPayload(payload: string): SerializedSecurityError | null {
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
typeof parsed.code === 'string' &&
|
||||
typeof parsed.message === 'string'
|
||||
) {
|
||||
return {
|
||||
code: parsed.code,
|
||||
host: typeof parsed.host === 'string' ? parsed.host : undefined,
|
||||
message: parsed.message,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseSecurityPolicyError(
|
||||
error: unknown
|
||||
): SerializedSecurityError | null {
|
||||
const message = readErrorMessage(error);
|
||||
const prefixIndex = message?.indexOf(SECURITY_ERROR_PREFIX) ?? -1;
|
||||
if (!message || prefixIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = message.slice(prefixIndex + SECURITY_ERROR_PREFIX.length);
|
||||
const parsed = parseSecurityPayload(payload);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const payloadEnd = payload.lastIndexOf('}');
|
||||
return payloadEnd >= 0
|
||||
? parseSecurityPayload(payload.slice(0, payloadEnd + 1))
|
||||
: null;
|
||||
}
|
||||
|
||||
export function normalizeHost(host: string): string {
|
||||
return host
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
@@ -66,4 +66,16 @@ export interface Settings {
|
||||
* Only meaningful for Xtream playlists in Electron.
|
||||
*/
|
||||
preferUploadedEpgOverXtream?: boolean;
|
||||
/**
|
||||
* Exact EPG source URLs the user has allowed to resolve to private/LAN
|
||||
* network addresses. Kept source-scoped instead of disabling SSRF
|
||||
* protection globally.
|
||||
*/
|
||||
trustedPrivateNetworkEpgUrls?: string[];
|
||||
/**
|
||||
* Lowercase hostnames whose invalid TLS certificates the user has chosen
|
||||
* to trust. This is host-scoped and does not disable TLS validation for
|
||||
* unrelated playlist or EPG hosts.
|
||||
*/
|
||||
trustedInsecureTlsHosts?: string[];
|
||||
}
|
||||
Reference in new issue
Block a user