mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
[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:
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) {
|
||||
|
||||
Reference in new issue
Block a user