[codex] fix Electron hardening follow-ups (#1053)

* fix: clean up Electron hardening follow-ups

* fix: remove duplicate Xtream probe comment

---------

Co-authored-by: 4gray <fourgray@proton.me>
This commit is contained in:
4grayand4gray authored and GitHub committed 2026-06-12 20:46:01 +02:00
1 parent 2c032cd3c8
commit 9043116ebd
5 files changed
+22 -18

No files matched your search

@@ -275,12 +275,10 @@ ipcMain.handle(
method?: 'GET' | 'HEAD';
}
) => {
// Guard against SSRF: a malicious portal/playlist could ask the main
// process to probe loopback/private/metadata addresses. Only allow
// public http(s) targets.
// Probe URLs must be http(s), but may target private/LAN Xtream hosts
// for self-hosted setups. Redirects stay disabled below so a validated
// URL cannot bounce to a different private target.
try {
// Private/LAN targets are allowed (users probe self-hosted Xtream
// servers); maxRedirects:0 below blocks redirect-based SSRF.
await assertRemoteUrlAllowed(payload.url, {
allowPrivateNetworks: true,
});
@@ -5,6 +5,7 @@ function createDatabaseMock(exec: jest.Mock) {
const database = {
close: jest.fn(),
exec,
pragma: jest.fn(),
};
const Database = jest.fn(() => database) as unknown as typeof BetterSqlite3;
@@ -14,10 +15,11 @@ function createDatabaseMock(exec: jest.Mock) {
describe('EpgDatabaseClearOperation', () => {
it('clears programs and channels in one transaction', () => {
const exec = jest.fn();
const { Database } = createDatabaseMock(exec);
const { Database, database } = createDatabaseMock(exec);
new EpgDatabaseClearOperation(Database).run();
expect(database.pragma).toHaveBeenCalledWith('busy_timeout = 5000');
expect(exec.mock.calls.map(([statement]) => statement)).toEqual([
'BEGIN',
'DELETE FROM epg_programs',
@@ -126,6 +126,7 @@ export class EpgDatabaseClearOperation {
constructor(Database: typeof BetterSqlite3) {
this.db = new Database(getIptvnatorDatabasePath());
this.db.pragma('busy_timeout = 5000');
}
run(): void {
@@ -12,7 +12,6 @@ import {
DownloadsService,
} from './downloads.service';
import { RuntimeCapabilitiesService } from './runtime-capabilities.service';
import { SettingsStore } from './settings-store.service';
type TestDownloadsService = {
downloads: WritableSignal<DownloadItem[]>;
@@ -22,16 +21,15 @@ type TestDownloadsService = {
hasLoadedDownloads: Signal<boolean>;
loadDownloads: DownloadsService['loadDownloads'];
loadDownloadFolder: DownloadsService['loadDownloadFolder'];
selectFolder: DownloadsService['selectFolder'];
_isLoadingDownloads: WritableSignal<boolean>;
_hasLoadedDownloads: WritableSignal<boolean>;
loadDownloadsRequestId: number;
settingsStore: {
getDownloadFolder: () => string;
};
};
type DownloadsElectronStub = {
downloadsGetDefaultFolder?: jest.Mock<Promise<string>, []>;
downloadsSelectFolder?: jest.Mock<Promise<string | null>, []>;
downloadsGetList: jest.Mock<Promise<DownloadItem[]>, [string?]>;
};
@@ -87,9 +85,6 @@ describe('DownloadsService', () => {
_hasLoadedDownloads: hasLoadedDownloads,
hasLoadedDownloads: hasLoadedDownloads.asReadonly(),
loadDownloadsRequestId: 0,
settingsStore: {
getDownloadFolder: () => '/renderer-controlled',
},
});
return service;
@@ -99,7 +94,6 @@ describe('DownloadsService', () => {
const injector = createEnvironmentInjector(
[
DownloadsService,
{ provide: SettingsStore, useValue: {} },
{
provide: RuntimeCapabilitiesService,
useValue: { supportsDownloads: false },
@@ -156,6 +150,19 @@ describe('DownloadsService', () => {
expect(electron.downloadsGetDefaultFolder).toHaveBeenCalledTimes(1);
});
it('stores a selected download folder returned by the main process', async () => {
const electron = {
downloadsSelectFolder: jest.fn(async () => '/selected'),
downloadsGetList: jest.fn(async () => []),
};
testWindow.electron = electron;
const service = createService();
await expect(service.selectFolder()).resolves.toBe('/selected');
expect(service.downloadFolder()).toBe('/selected');
expect(electron.downloadsSelectFolder).toHaveBeenCalledTimes(1);
});
it('marks downloads as loaded after a failed request while preserving existing data', async () => {
const existing = createDownload(1);
const error = new Error('download query failed');
@@ -1,6 +1,5 @@
import { computed, inject, Injectable, OnDestroy, signal } from '@angular/core';
import { RuntimeCapabilitiesService } from './runtime-capabilities.service';
import { SettingsStore } from './settings-store.service';
export type DownloadStatus =
| 'queued'
@@ -33,7 +32,6 @@ export interface DownloadItem {
@Injectable({ providedIn: 'root' })
export class DownloadsService implements OnDestroy {
private readonly runtime = inject(RuntimeCapabilitiesService);
private readonly settingsStore = inject(SettingsStore);
private unsubscribe?: () => void;
private loadDownloadsRequestId = 0;
@@ -330,8 +328,6 @@ export class DownloadsService implements OnDestroy {
const folder = await window.electron.downloadsSelectFolder();
if (folder) {
this.downloadFolder.set(folder);
// Save to settings
await this.settingsStore.updateSettings({ downloadFolder: folder });
}
return folder;
} catch (error) {