Harden Electron trust boundaries

Add centralized browser security helpers for restrictive BrowserWindow profiles, navigation denial, popup denial, permission denial, and exact HTTPS host allowlists. Apply those policies to the main renderer window and to the Real-Debrid and AllDebrid browser-login windows without weakening the credential renderer boundary.

Add shared IPC sender validation for renderer handlers so development accepts only the local Vite origin and non-development accepts only the built file renderer tree. Route the registered renderer IPC handlers through the shared guard and restrict app:open-external to the same exact HTTPS allowlist.

Cover the hardening with focused red-green tests for untrusted navigation, popup handling, permission requests, exact host/subdomain matching, packaged file renderer boundaries, untrusted IPC senders, and both browser-login window profiles.
This commit is contained in:
Sucukdeluxe
2026-08-12 00:30:49 +02:00
parent bfb901903d
commit 265498d629
9 changed files with 765 additions and 165 deletions
+10 -9
View File
@@ -1,7 +1,8 @@
import { BrowserWindow, session } from "electron";
import { AllDebridHostInfo } from "../shared/types";
import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils";
import { AllDebridHostInfo } from "../shared/types";
import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils";
import { ALLDEBRID_LOGIN_HOSTS, applyRemoteLoginSecurity, createRemoteLoginWebPreferences } from "./browser-security";
const ALLDEBRID_BASE_URL = "https://alldebrid.com";
const ALLDEBRID_LOGIN_URL = `${ALLDEBRID_BASE_URL}/register/?from=de`;
@@ -302,12 +303,12 @@ export class AllDebridWebFallback {
minHeight: 760,
autoHideMenuBar: true,
title: "AllDebrid Web-Login",
webPreferences: {
partition,
contextIsolation: true,
nodeIntegration: false
}
});
webPreferences: createRemoteLoginWebPreferences(partition)
});
applyRemoteLoginSecurity(window, {
providerHosts: ALLDEBRID_LOGIN_HOSTS,
externalHosts: ALLDEBRID_LOGIN_HOSTS
});
window.setMenuBarVisibility(false);
window.on("closed", () => {
if (this.loginWindow === window) {
+136
View File
@@ -0,0 +1,136 @@
import { shell, type WebPreferences } from "electron";
export type HttpsHostRule = {
hostname: string;
includeSubdomains?: boolean;
};
type NavigationEvent = {
preventDefault: () => void;
};
type SecurityWindow = {
webContents: {
on: (event: "will-navigate", listener: (event: NavigationEvent, url: string) => void) => unknown;
setWindowOpenHandler: (handler: (details: { url: string }) => { action: "deny" }) => unknown;
session: {
setPermissionRequestHandler: (handler: (webContents: unknown, permission: string, callback: (allowed: boolean) => void) => void) => unknown;
};
};
};
export type MainWindowSecurityOptions = {
rendererUrl: string;
externalHosts: readonly HttpsHostRule[];
};
export type RemoteLoginSecurityOptions = {
providerHosts: readonly HttpsHostRule[];
externalHosts: readonly HttpsHostRule[];
};
export const MAIN_WINDOW_EXTERNAL_HOSTS: readonly HttpsHostRule[] = [
{ hostname: "github.com" },
{ hostname: "codeberg.org" },
{ hostname: "real-debrid.com", includeSubdomains: true },
{ hostname: "alldebrid.com", includeSubdomains: true },
{ hostname: "bestdebrid.com", includeSubdomains: true }
];
export const REALDEBRID_LOGIN_HOSTS: readonly HttpsHostRule[] = [
{ hostname: "real-debrid.com", includeSubdomains: true }
];
export const ALLDEBRID_LOGIN_HOSTS: readonly HttpsHostRule[] = [
{ hostname: "alldebrid.com", includeSubdomains: true }
];
export function createMainWindowWebPreferences(preload: string): WebPreferences {
return {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload
};
}
export function createRemoteLoginWebPreferences(partition: string): WebPreferences {
return {
partition,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false
};
}
export function isAllowedHttpsUrl(rawUrl: string, hosts: readonly HttpsHostRule[]): boolean {
let parsed: URL;
try {
parsed = new URL(String(rawUrl || ""));
} catch {
return false;
}
if (parsed.protocol !== "https:") {
return false;
}
const hostname = parsed.hostname.toLowerCase();
return hosts.some((host) => {
const expected = host.hostname.toLowerCase();
return hostname === expected || (host.includeSubdomains === true && hostname.endsWith(`.${expected}`));
});
}
export function openAllowedExternalUrl(rawUrl: string, hosts: readonly HttpsHostRule[]): boolean {
if (!isAllowedHttpsUrl(rawUrl, hosts)) {
return false;
}
void shell.openExternal(new URL(rawUrl).toString());
return true;
}
export function applyMainWindowSecurity(window: SecurityWindow, options: MainWindowSecurityOptions): void {
applyCommonSecurity(window, options.externalHosts);
window.webContents.on("will-navigate", (event, url) => {
if (isExpectedRendererUrl(url, options.rendererUrl)) {
return;
}
event.preventDefault();
openAllowedExternalUrl(url, options.externalHosts);
});
}
export function applyRemoteLoginSecurity(window: SecurityWindow, options: RemoteLoginSecurityOptions): void {
applyCommonSecurity(window, options.externalHosts);
window.webContents.on("will-navigate", (event, url) => {
if (isAllowedHttpsUrl(url, options.providerHosts)) {
return;
}
event.preventDefault();
openAllowedExternalUrl(url, options.externalHosts);
});
}
function applyCommonSecurity(window: SecurityWindow, externalHosts: readonly HttpsHostRule[]): void {
window.webContents.setWindowOpenHandler((details) => {
openAllowedExternalUrl(details.url, externalHosts);
return { action: "deny" };
});
window.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => {
callback(false);
});
}
function isExpectedRendererUrl(rawUrl: string, expectedUrl: string): boolean {
try {
const parsed = new URL(String(rawUrl || ""));
const expected = new URL(String(expectedUrl || ""));
if (expected.protocol === "file:") {
return parsed.protocol === "file:" && parsed.host === expected.host && parsed.pathname === expected.pathname;
}
return parsed.origin === expected.origin;
} catch {
return false;
}
}
+47
View File
@@ -0,0 +1,47 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
export type TrustedIpcOptions = {
isPackaged: boolean;
devServerUrl: string;
appPath: string;
};
type IpcSenderEvent = {
senderFrame?: { url: string } | null;
sender?: { getURL?: () => string };
};
export function assertTrustedIpcSender(event: IpcSenderEvent, options: TrustedIpcOptions): void {
const senderUrl = String(event.senderFrame?.url || event.sender?.getURL?.() || "");
if (!senderUrl || !isTrustedSenderUrl(senderUrl, options)) {
throw new Error("IPC-Absender ist nicht vertrauenswürdig");
}
}
function isTrustedSenderUrl(senderUrl: string, options: TrustedIpcOptions): boolean {
if (options.isPackaged) {
return isPackagedRendererUrl(senderUrl, options.appPath);
}
try {
const sender = new URL(senderUrl);
const expected = new URL(options.devServerUrl);
return sender.origin === expected.origin;
} catch {
return false;
}
}
function isPackagedRendererUrl(senderUrl: string, appPath: string): boolean {
try {
const parsed = new URL(senderUrl);
if (parsed.protocol !== "file:") {
return false;
}
const senderPath = path.resolve(fileURLToPath(parsed));
const rendererPath = path.resolve(appPath, "build", "renderer");
return senderPath === rendererPath || senderPath.startsWith(rendererPath + path.sep);
} catch {
return false;
}
}
+129 -104
View File
@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { app, BrowserWindow, clipboard, dialog, ipcMain, IpcMainInvokeEvent, Menu, safeStorage, shell, Tray } from "electron";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, safeStorage, shell, Tray, type IpcMainEvent, type IpcMainInvokeEvent } from "electron";
import { AddLinksPayload, AppSettings, DebridProvider, EnableRemoteDiagnosticsInput, RendererSettingsUpdate, UpdateInstallProgress } from "../shared/types";
import { AppController } from "./app-controller";
import { IPC_CHANNELS } from "../shared/ipc";
@@ -18,6 +19,8 @@ import { isMdd2Backup } from "./backup-crypto";
import { validateAccountCommand, validateAccountCredentialCheckInput } from "./account-commands";
import { createRendererSettings } from "./renderer-state";
import { validateRendererSettingsUpdate } from "./renderer-settings";
import { applyMainWindowSecurity, createMainWindowWebPreferences, MAIN_WINDOW_EXTERNAL_HOSTS, openAllowedExternalUrl } from "./browser-security";
import { assertTrustedIpcSender, type TrustedIpcOptions } from "./ipc-security";
function validateString(value: unknown, name: string): string {
if (typeof value !== "string") {
@@ -82,9 +85,39 @@ let controller: AppController;
let pendingBackupImport: Buffer | null = null;
const CLIPBOARD_MAX_TEXT_CHARS = 50_000;
function isDevMode(): boolean {
return process.env.NODE_ENV === "development";
}
function isDevMode(): boolean {
return process.env.NODE_ENV === "development";
}
function getRendererFileUrl(): string {
return pathToFileURL(path.join(app.getAppPath(), "build", "renderer", "index.html")).toString();
}
function getTrustedRendererUrl(): string {
return isDevMode() ? DEV_SERVER_URL : getRendererFileUrl();
}
function getTrustedIpcOptions(): TrustedIpcOptions {
return {
isPackaged: !isDevMode(),
devServerUrl: DEV_SERVER_URL,
appPath: app.getAppPath()
};
}
function handleTrusted<TArgs extends unknown[]>(channel: string, listener: (event: IpcMainInvokeEvent, ...args: TArgs) => unknown): void {
ipcMain.handle(channel, (event, ...args) => {
assertTrustedIpcSender(event, getTrustedIpcOptions());
return listener(event, ...(args as TArgs));
});
}
function onTrusted<TArgs extends unknown[]>(channel: string, listener: (event: IpcMainEvent, ...args: TArgs) => void): void {
ipcMain.on(channel, (event, ...args) => {
assertTrustedIpcSender(event, getTrustedIpcOptions());
listener(event, ...(args as TArgs));
});
}
// Single owner of the scheduled-start timer. startOnPast: a past time entered
// interactively starts right away; at boot a stale past time is cleared instead
@@ -124,12 +157,13 @@ function createWindow(): BrowserWindow {
backgroundColor: "#070b14",
title: `${APP_NAME} - v${controller.getVersion()}`,
icon: resolveAppIconPath(app.isPackaged, app.getAppPath(), process.resourcesPath),
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, "../preload/preload.js")
}
});
webPreferences: createMainWindowWebPreferences(path.join(__dirname, "../preload/preload.js"))
});
applyMainWindowSecurity(window, {
rendererUrl: getTrustedRendererUrl(),
externalHosts: MAIN_WINDOW_EXTERNAL_HOSTS
});
if (!isDevMode()) {
window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
@@ -332,10 +366,10 @@ function updateTray(): void {
}
function registerIpcHandlers(): void {
ipcMain.handle(IPC_CHANNELS.GET_SNAPSHOT, () => controller.getSnapshot());
ipcMain.handle(IPC_CHANNELS.GET_VERSION, () => controller.getVersion());
ipcMain.handle(IPC_CHANNELS.CHECK_UPDATES, async () => controller.checkUpdates());
ipcMain.handle(IPC_CHANNELS.INSTALL_UPDATE, async () => {
handleTrusted(IPC_CHANNELS.GET_SNAPSHOT, () => controller.getSnapshot());
handleTrusted(IPC_CHANNELS.GET_VERSION, () => controller.getVersion());
handleTrusted(IPC_CHANNELS.CHECK_UPDATES, async () => controller.checkUpdates());
handleTrusted(IPC_CHANNELS.INSTALL_UPDATE, async () => {
const result = await controller.installUpdate((progress: UpdateInstallProgress) => {
if (!mainWindow || mainWindow.isDestroyed()) {
return;
@@ -349,19 +383,10 @@ function registerIpcHandlers(): void {
}
return result;
});
ipcMain.handle(IPC_CHANNELS.OPEN_EXTERNAL, async (_event: IpcMainInvokeEvent, rawUrl: string) => {
try {
const parsed = new URL(String(rawUrl || "").trim());
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
return false;
}
await shell.openExternal(parsed.toString());
return true;
} catch {
return false;
}
});
ipcMain.handle(IPC_CHANNELS.UPDATE_SETTINGS, (_event: IpcMainInvokeEvent, partial: RendererSettingsUpdate) => {
handleTrusted(IPC_CHANNELS.OPEN_EXTERNAL, async (_event: IpcMainInvokeEvent, rawUrl: string) => {
return openAllowedExternalUrl(String(rawUrl || "").trim(), MAIN_WINDOW_EXTERNAL_HOSTS);
});
handleTrusted(IPC_CHANNELS.UPDATE_SETTINGS, (_event: IpcMainInvokeEvent, partial: RendererSettingsUpdate) => {
const validated = validateRendererSettingsUpdate(partial ?? {}, controller.getSettings());
const result = controller.updateSettings(validated as Partial<AppSettings>);
updateClipboardWatcher();
@@ -369,14 +394,14 @@ function registerIpcHandlers(): void {
armScheduledStart(result.scheduledStartEpochMs || 0, { startOnPast: true });
return createRendererSettings(result);
});
ipcMain.handle(IPC_CHANNELS.RESET_PROVIDER_DAILY_USAGE, (_event: IpcMainInvokeEvent, provider: string) => {
handleTrusted(IPC_CHANNELS.RESET_PROVIDER_DAILY_USAGE, (_event: IpcMainInvokeEvent, provider: string) => {
const validatedProvider = validateString(provider, "provider") as DebridProvider;
if (!RESETTABLE_PROVIDER_KEYS.has(validatedProvider)) {
throw new Error("provider ist ungültig");
}
return createRendererSettings(controller.resetProviderDailyUsage(validatedProvider));
});
ipcMain.handle(IPC_CHANNELS.RESET_DEBRID_LINK_API_KEY_DAILY_USAGE, (_event: IpcMainInvokeEvent, keyId: string) => {
handleTrusted(IPC_CHANNELS.RESET_DEBRID_LINK_API_KEY_DAILY_USAGE, (_event: IpcMainInvokeEvent, keyId: string) => {
const validatedKeyId = validateString(keyId, "keyId").trim();
if (!validatedKeyId) {
throw new Error("keyId ist ungültig");
@@ -384,30 +409,30 @@ function registerIpcHandlers(): void {
return createRendererSettings(controller.resetDebridLinkApiKeyDailyUsage(validatedKeyId));
});
ipcMain.handle(IPC_CHANNELS.CREATE_ACCOUNT, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
handleTrusted(IPC_CHANNELS.CREATE_ACCOUNT, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
const command = validateAccountCommand(rawCommand);
if (command.action !== "create") throw new Error("Account-Payload ist ungültig");
return controller.executeAccountCommand(command);
});
ipcMain.handle(IPC_CHANNELS.REPLACE_ACCOUNT, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
handleTrusted(IPC_CHANNELS.REPLACE_ACCOUNT, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
const command = validateAccountCommand(rawCommand);
if (command.action !== "replace") throw new Error("Account-Payload ist ungültig");
return controller.executeAccountCommand(command);
});
ipcMain.handle(IPC_CHANNELS.UPDATE_ACCOUNT_SECRET, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
handleTrusted(IPC_CHANNELS.UPDATE_ACCOUNT_SECRET, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
const command = validateAccountCommand(rawCommand);
if (command.action !== "update-secret") throw new Error("Account-Payload ist ungültig");
return controller.executeAccountCommand(command);
});
ipcMain.handle(IPC_CHANNELS.DELETE_ACCOUNT, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
handleTrusted(IPC_CHANNELS.DELETE_ACCOUNT, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
const command = validateAccountCommand(rawCommand);
if (command.action !== "delete") throw new Error("Account-Payload ist ungültig");
return controller.executeAccountCommand(command);
});
ipcMain.handle(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => {
handleTrusted(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => {
validatePlainObject(payload ?? {}, "payload");
validateString(payload?.rawText, "rawText");
if (payload.packageName !== undefined) {
@@ -418,13 +443,13 @@ function registerIpcHandlers(): void {
}
return controller.addLinks(payload);
});
ipcMain.handle(IPC_CHANNELS.ADD_CONTAINERS, async (_event: IpcMainInvokeEvent, filePaths: string[]) => {
handleTrusted(IPC_CHANNELS.ADD_CONTAINERS, async (_event: IpcMainInvokeEvent, filePaths: string[]) => {
const validPaths = validateStringArray(filePaths ?? [], "filePaths");
const safePaths = validPaths.filter((p) => path.isAbsolute(p));
return controller.addContainers(safePaths);
});
ipcMain.handle(IPC_CHANNELS.GET_START_CONFLICTS, () => controller.getStartConflicts());
ipcMain.handle(IPC_CHANNELS.RESOLVE_START_CONFLICT, (_event: IpcMainInvokeEvent, packageId: string, policy: "keep" | "skip" | "overwrite") => {
handleTrusted(IPC_CHANNELS.GET_START_CONFLICTS, () => controller.getStartConflicts());
handleTrusted(IPC_CHANNELS.RESOLVE_START_CONFLICT, (_event: IpcMainInvokeEvent, packageId: string, policy: "keep" | "skip" | "overwrite") => {
validateString(packageId, "packageId");
validateString(policy, "policy");
if (policy !== "keep" && policy !== "skip" && policy !== "overwrite") {
@@ -432,8 +457,8 @@ function registerIpcHandlers(): void {
}
return controller.resolveStartConflict(packageId, policy);
});
ipcMain.handle(IPC_CHANNELS.CLEAR_ALL, () => controller.clearAll());
ipcMain.handle(IPC_CHANNELS.START, () => {
handleTrusted(IPC_CHANNELS.CLEAR_ALL, () => controller.clearAll());
handleTrusted(IPC_CHANNELS.START, () => {
if (scheduledStartTimer !== null) {
clearTimeout(scheduledStartTimer);
scheduledStartTimer = null;
@@ -441,21 +466,21 @@ function registerIpcHandlers(): void {
}
return controller.start();
});
ipcMain.handle(IPC_CHANNELS.START_PACKAGES, (_event: IpcMainInvokeEvent, packageIds: string[]) => {
handleTrusted(IPC_CHANNELS.START_PACKAGES, (_event: IpcMainInvokeEvent, packageIds: string[]) => {
validateStringArray(packageIds ?? [], "packageIds");
return controller.startPackages(packageIds ?? []);
});
ipcMain.handle(IPC_CHANNELS.START_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
handleTrusted(IPC_CHANNELS.START_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
validateStringArray(itemIds ?? [], "itemIds");
return controller.startItems(itemIds ?? []);
});
ipcMain.handle(IPC_CHANNELS.STOP, () => controller.stop());
ipcMain.handle(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause());
ipcMain.handle(IPC_CHANNELS.CANCEL_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
handleTrusted(IPC_CHANNELS.STOP, () => controller.stop());
handleTrusted(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause());
handleTrusted(IPC_CHANNELS.CANCEL_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
validateString(packageId, "packageId");
return controller.cancelPackage(packageId);
});
ipcMain.handle(IPC_CHANNELS.RENAME_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string, newName: string) => {
handleTrusted(IPC_CHANNELS.RENAME_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string, newName: string) => {
validateString(packageId, "packageId");
validateString(newName, "newName");
if (newName.length > RENAME_PACKAGE_MAX_CHARS) {
@@ -463,19 +488,19 @@ function registerIpcHandlers(): void {
}
return controller.renamePackage(packageId, newName);
});
ipcMain.handle(IPC_CHANNELS.REORDER_PACKAGES, (_event: IpcMainInvokeEvent, packageIds: string[]) => {
handleTrusted(IPC_CHANNELS.REORDER_PACKAGES, (_event: IpcMainInvokeEvent, packageIds: string[]) => {
validateStringArray(packageIds, "packageIds");
return controller.reorderPackages(packageIds);
});
ipcMain.handle(IPC_CHANNELS.REMOVE_ITEM, (_event: IpcMainInvokeEvent, itemId: string) => {
handleTrusted(IPC_CHANNELS.REMOVE_ITEM, (_event: IpcMainInvokeEvent, itemId: string) => {
validateString(itemId, "itemId");
return controller.removeItem(itemId);
});
ipcMain.handle(IPC_CHANNELS.TOGGLE_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
handleTrusted(IPC_CHANNELS.TOGGLE_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
validateString(packageId, "packageId");
return controller.togglePackage(packageId);
});
ipcMain.handle(IPC_CHANNELS.EXPORT_PACKAGE_SELECTION, async (_event: IpcMainInvokeEvent, packageIds: string[]) => {
handleTrusted(IPC_CHANNELS.EXPORT_PACKAGE_SELECTION, async (_event: IpcMainInvokeEvent, packageIds: string[]) => {
const validPackageIds = validateStringArray(packageIds ?? [], "packageIds");
const exported = controller.exportPackageSelection(validPackageIds);
if (exported.packageCount === 0 || exported.linkCount === 0) {
@@ -492,7 +517,7 @@ function registerIpcHandlers(): void {
await fs.promises.writeFile(result.filePath, exported.text, "utf8");
return { saved: true, packageCount: exported.packageCount, linkCount: exported.linkCount, filePath: result.filePath };
});
ipcMain.handle(IPC_CHANNELS.EXPORT_ITEM_SELECTION, async (_event: IpcMainInvokeEvent, itemIds: string[]) => {
handleTrusted(IPC_CHANNELS.EXPORT_ITEM_SELECTION, async (_event: IpcMainInvokeEvent, itemIds: string[]) => {
const validItemIds = validateStringArray(itemIds ?? [], "itemIds");
const exported = controller.exportItemSelection(validItemIds);
if (exported.packageCount === 0 || exported.linkCount === 0) {
@@ -509,19 +534,19 @@ function registerIpcHandlers(): void {
await fs.promises.writeFile(result.filePath, exported.text, "utf8");
return { saved: true, packageCount: exported.packageCount, linkCount: exported.linkCount, filePath: result.filePath };
});
ipcMain.handle(IPC_CHANNELS.RETRY_EXTRACTION, (_event: IpcMainInvokeEvent, packageId: string) => {
handleTrusted(IPC_CHANNELS.RETRY_EXTRACTION, (_event: IpcMainInvokeEvent, packageId: string) => {
validateString(packageId, "packageId");
return controller.retryExtraction(packageId);
});
ipcMain.handle(IPC_CHANNELS.EXTRACT_NOW, (_event: IpcMainInvokeEvent, packageId: string) => {
handleTrusted(IPC_CHANNELS.EXTRACT_NOW, (_event: IpcMainInvokeEvent, packageId: string) => {
validateString(packageId, "packageId");
return controller.extractNow(packageId);
});
ipcMain.handle(IPC_CHANNELS.RESET_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
handleTrusted(IPC_CHANNELS.RESET_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
validateString(packageId, "packageId");
return controller.resetPackage(packageId);
});
ipcMain.handle(IPC_CHANNELS.SET_PACKAGE_PRIORITY, (_event: IpcMainInvokeEvent, packageId: string, priority: string) => {
handleTrusted(IPC_CHANNELS.SET_PACKAGE_PRIORITY, (_event: IpcMainInvokeEvent, packageId: string, priority: string) => {
validateString(packageId, "packageId");
validateString(priority, "priority");
if (priority !== "high" && priority !== "normal" && priority !== "low") {
@@ -529,28 +554,28 @@ function registerIpcHandlers(): void {
}
return controller.setPackagePriority(packageId, priority);
});
ipcMain.handle(IPC_CHANNELS.SKIP_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
handleTrusted(IPC_CHANNELS.SKIP_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
validateStringArray(itemIds ?? [], "itemIds");
return controller.skipItems(itemIds ?? []);
});
ipcMain.handle(IPC_CHANNELS.RESET_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
handleTrusted(IPC_CHANNELS.RESET_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
validateStringArray(itemIds ?? [], "itemIds");
return controller.resetItems(itemIds ?? []);
});
ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory());
ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory());
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
handleTrusted(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory());
handleTrusted(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory());
handleTrusted(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
validateString(entryId, "entryId");
return controller.removeHistoryEntry(entryId);
});
ipcMain.handle(IPC_CHANNELS.REVEAL_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: unknown) => {
handleTrusted(IPC_CHANNELS.REVEAL_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: unknown) => {
return revealHistoryEntry({ entryId }, {
loadHistory: () => controller.getHistory(),
stat: (directory) => fs.promises.stat(directory),
openPath: (directory) => shell.openPath(directory)
});
});
ipcMain.handle(IPC_CHANNELS.EXPORT_QUEUE, async () => {
handleTrusted(IPC_CHANNELS.EXPORT_QUEUE, async () => {
const options = {
defaultPath: `rd-queue-export.json`,
filters: [{ name: "Queue Export", extensions: ["json"] }]
@@ -563,7 +588,7 @@ function registerIpcHandlers(): void {
await fs.promises.writeFile(result.filePath, json, "utf8");
return { saved: true };
});
ipcMain.handle(IPC_CHANNELS.IMPORT_QUEUE, (_event: IpcMainInvokeEvent, json: string) => {
handleTrusted(IPC_CHANNELS.IMPORT_QUEUE, (_event: IpcMainInvokeEvent, json: string) => {
validateString(json, "json");
const bytes = Buffer.byteLength(json, "utf8");
if (bytes > IMPORT_QUEUE_MAX_BYTES) {
@@ -571,21 +596,21 @@ function registerIpcHandlers(): void {
}
return controller.importQueue(json);
});
ipcMain.handle(IPC_CHANNELS.TOGGLE_CLIPBOARD, () => {
handleTrusted(IPC_CHANNELS.TOGGLE_CLIPBOARD, () => {
const settings = controller.getSettings();
const next = !settings.clipboardWatch;
controller.updateSettings({ clipboardWatch: next });
updateClipboardWatcher();
return next;
});
ipcMain.handle(IPC_CHANNELS.PICK_FOLDER, async () => {
handleTrusted(IPC_CHANNELS.PICK_FOLDER, async () => {
const options = {
properties: ["openDirectory", "createDirectory"] as Array<"openDirectory" | "createDirectory">
};
const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options);
return result.canceled ? null : result.filePaths[0] || null;
});
ipcMain.handle(IPC_CHANNELS.PICK_CONTAINERS, async () => {
handleTrusted(IPC_CHANNELS.PICK_CONTAINERS, async () => {
const options = {
properties: ["openFile", "multiSelections"] as Array<"openFile" | "multiSelections">,
filters: [
@@ -596,20 +621,20 @@ function registerIpcHandlers(): void {
const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options);
return result.canceled ? [] : result.filePaths;
});
ipcMain.handle(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats());
ipcMain.handle(IPC_CHANNELS.RESET_SESSION_STATS, () => controller.resetSessionStats());
ipcMain.handle(IPC_CHANNELS.RESET_DOWNLOAD_STATS, () => controller.resetDownloadStats());
handleTrusted(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats());
handleTrusted(IPC_CHANNELS.RESET_SESSION_STATS, () => controller.resetSessionStats());
handleTrusted(IPC_CHANNELS.RESET_DOWNLOAD_STATS, () => controller.resetDownloadStats());
ipcMain.handle(IPC_CHANNELS.RESTART, () => {
handleTrusted(IPC_CHANNELS.RESTART, () => {
app.relaunch();
app.quit();
});
ipcMain.handle(IPC_CHANNELS.QUIT, () => {
handleTrusted(IPC_CHANNELS.QUIT, () => {
app.quit();
});
ipcMain.handle(IPC_CHANNELS.EXPORT_BACKUP, async (_event: IpcMainInvokeEvent, rawPassphrase: unknown) => {
handleTrusted(IPC_CHANNELS.EXPORT_BACKUP, async (_event: IpcMainInvokeEvent, rawPassphrase: unknown) => {
const passphrase = validateString(rawPassphrase, "passphrase");
const options = {
defaultPath: `${new Date().toISOString().slice(0, 10).split("-").reverse().join("-")}-mdd-backup.mdd`,
@@ -624,9 +649,9 @@ function registerIpcHandlers(): void {
return { saved: true };
});
ipcMain.handle(IPC_CHANNELS.EXPORT_ONLINE_BACKUP, async () => controller.exportOnlineBackup());
handleTrusted(IPC_CHANNELS.EXPORT_ONLINE_BACKUP, async () => controller.exportOnlineBackup());
ipcMain.handle(IPC_CHANNELS.IMPORT_ONLINE_BACKUP, async (_event: IpcMainInvokeEvent, rawKey: unknown) => {
handleTrusted(IPC_CHANNELS.IMPORT_ONLINE_BACKUP, async (_event: IpcMainInvokeEvent, rawKey: unknown) => {
const key = validateString(rawKey, "key").trim();
if (key.length > 128) {
throw new Error("Online-Sicherungsschlüssel ist ungültig");
@@ -634,7 +659,7 @@ function registerIpcHandlers(): void {
return controller.importOnlineBackup(key);
});
ipcMain.handle(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE, async () => {
handleTrusted(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE, async () => {
const options = {
defaultPath: controller.getSupportBundleDefaultFileName(),
filters: [{ name: "Support Bundle", extensions: ["zip"] }]
@@ -648,40 +673,40 @@ function registerIpcHandlers(): void {
return { saved: true, filePath: result.filePath };
});
ipcMain.handle(IPC_CHANNELS.OPEN_LOG, async () => {
handleTrusted(IPC_CHANNELS.OPEN_LOG, async () => {
const logPath = getLogFilePath();
await shell.openPath(logPath);
});
ipcMain.handle(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
handleTrusted(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
const logPath = controller.getAuditLogPath();
if (logPath) {
await shell.openPath(logPath);
}
});
ipcMain.handle(IPC_CHANNELS.OPEN_RENAME_LOG, async () => {
handleTrusted(IPC_CHANNELS.OPEN_RENAME_LOG, async () => {
const logPath = controller.getRenameLogPath();
if (logPath) {
await shell.openPath(logPath);
}
});
ipcMain.handle(IPC_CHANNELS.OPEN_SESSION_LOG, async () => {
handleTrusted(IPC_CHANNELS.OPEN_SESSION_LOG, async () => {
const logPath = controller.getSessionLogPath();
if (logPath) {
await shell.openPath(logPath);
}
});
ipcMain.handle(IPC_CHANNELS.OPEN_TRACE_LOG, async () => {
handleTrusted(IPC_CHANNELS.OPEN_TRACE_LOG, async () => {
const logPath = controller.getTraceLogPath();
if (logPath) {
await shell.openPath(logPath);
}
});
ipcMain.handle(IPC_CHANNELS.OPEN_PACKAGE_LOG, async (_event: IpcMainInvokeEvent, packageId: string) => {
handleTrusted(IPC_CHANNELS.OPEN_PACKAGE_LOG, async (_event: IpcMainInvokeEvent, packageId: string) => {
validateString(packageId, "packageId");
const logPath = controller.getPackageLogPath(packageId);
if (logPath) {
@@ -689,11 +714,11 @@ function registerIpcHandlers(): void {
}
});
ipcMain.handle(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK, async () => controller.getDebugSetupCheck());
handleTrusted(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK, async () => controller.getDebugSetupCheck());
ipcMain.handle(IPC_CHANNELS.GET_RECENT_ERRORS, async () => getRecentErrors());
handleTrusted(IPC_CHANNELS.GET_RECENT_ERRORS, async () => getRecentErrors());
ipcMain.handle(IPC_CHANNELS.TEST_NOTIFY, async (_event: IpcMainInvokeEvent, url: string, mention: string) => {
handleTrusted(IPC_CHANNELS.TEST_NOTIFY, async (_event: IpcMainInvokeEvent, url: string, mention: string) => {
validateString(url, "url");
return sendNotification(url, {
title: "🔔 Test-Benachrichtigung",
@@ -702,9 +727,9 @@ function registerIpcHandlers(): void {
});
});
ipcMain.handle(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
handleTrusted(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string, durationMinutes?: number) => {
handleTrusted(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string, durationMinutes?: number) => {
if (typeof enabled !== "boolean") {
throw new Error("enabled muss ein Boolean sein");
}
@@ -717,16 +742,16 @@ function registerIpcHandlers(): void {
return controller.setTraceEnabled(enabled, note, durationMinutes ? durationMinutes * 60 * 1000 : undefined);
});
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
handleTrusted(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
const rotated = controller.rotateDebugToken();
return { path: rotated.path };
});
ipcMain.handle(IPC_CHANNELS.GET_REMOTE_DIAGNOSTICS, async () => {
handleTrusted(IPC_CHANNELS.GET_REMOTE_DIAGNOSTICS, async () => {
return controller.getRemoteDiagnostics();
});
ipcMain.handle(IPC_CHANNELS.ENABLE_REMOTE_DIAGNOSTICS, async (_event: IpcMainInvokeEvent, input: EnableRemoteDiagnosticsInput) => {
handleTrusted(IPC_CHANNELS.ENABLE_REMOTE_DIAGNOSTICS, async (_event: IpcMainInvokeEvent, input: EnableRemoteDiagnosticsInput) => {
if (!input || (input.hostMode !== "local" && input.hostMode !== "network")) {
throw new Error("hostMode muss 'local' oder 'network' sein");
}
@@ -741,15 +766,15 @@ function registerIpcHandlers(): void {
});
});
ipcMain.handle(IPC_CHANNELS.DISABLE_REMOTE_DIAGNOSTICS, async () => {
handleTrusted(IPC_CHANNELS.DISABLE_REMOTE_DIAGNOSTICS, async () => {
return controller.disableRemoteDiagnostics();
});
ipcMain.handle(IPC_CHANNELS.ROTATE_REMOTE_DIAGNOSTICS_TOKEN, async () => {
handleTrusted(IPC_CHANNELS.ROTATE_REMOTE_DIAGNOSTICS_TOKEN, async () => {
return controller.rotateRemoteDiagnosticsToken();
});
ipcMain.handle(IPC_CHANNELS.OPEN_ITEM_LOG, async (_event: IpcMainInvokeEvent, itemId: string) => {
handleTrusted(IPC_CHANNELS.OPEN_ITEM_LOG, async (_event: IpcMainInvokeEvent, itemId: string) => {
validateString(itemId, "itemId");
const logPath = controller.getItemLogPath(itemId);
if (logPath) {
@@ -757,15 +782,15 @@ function registerIpcHandlers(): void {
}
});
ipcMain.handle(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, async () => {
handleTrusted(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, async () => {
await controller.openRealDebridLoginWindow();
});
ipcMain.handle(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN, async () => {
handleTrusted(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN, async () => {
await controller.openAllDebridLoginWindow();
});
ipcMain.handle(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES, async () => {
handleTrusted(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES, async () => {
const options = {
properties: ["openFile"] as Array<"openFile">,
filters: [
@@ -780,23 +805,23 @@ function registerIpcHandlers(): void {
return controller.importBestDebridCookies(result.filePaths[0]);
});
ipcMain.handle(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO, async () => {
handleTrusted(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO, async () => {
return controller.getAllDebridHostInfo();
});
ipcMain.handle(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS, async () => {
handleTrusted(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS, async () => {
return controller.getDebridLinkHostLimits();
});
ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async () => {
handleTrusted(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async () => {
return controller.checkDebridAccounts();
});
ipcMain.handle(IPC_CHANNELS.CHECK_ACCOUNT_CREDENTIALS, async (_event, rawInput: unknown) => {
handleTrusted(IPC_CHANNELS.CHECK_ACCOUNT_CREDENTIALS, async (_event, rawInput: unknown) => {
return controller.checkAccountCredentials(validateAccountCredentialCheckInput(rawInput));
});
ipcMain.handle(IPC_CHANNELS.SELECT_BACKUP_IMPORT, async () => {
handleTrusted(IPC_CHANNELS.SELECT_BACKUP_IMPORT, async () => {
pendingBackupImport = null;
const options = {
properties: ["openFile"] as Array<"openFile">,
@@ -821,11 +846,11 @@ function registerIpcHandlers(): void {
return { selected: true, requiresPassphrase: isMdd2Backup(data) };
});
ipcMain.handle(IPC_CHANNELS.CANCEL_BACKUP_IMPORT, () => {
handleTrusted(IPC_CHANNELS.CANCEL_BACKUP_IMPORT, () => {
pendingBackupImport = null;
});
ipcMain.handle(IPC_CHANNELS.IMPORT_BACKUP, async (_event: IpcMainInvokeEvent, rawPassphrase?: unknown) => {
handleTrusted(IPC_CHANNELS.IMPORT_BACKUP, async (_event: IpcMainInvokeEvent, rawPassphrase?: unknown) => {
const data = pendingBackupImport;
pendingBackupImport = null;
if (!data) {
@@ -845,7 +870,7 @@ function registerIpcHandlers(): void {
return importResult;
});
ipcMain.on(IPC_CHANNELS.LOG_RENDERER_ERROR, (_event, rawReport: unknown) => {
onTrusted(IPC_CHANNELS.LOG_RENDERER_ERROR, (_event, rawReport: unknown) => {
try {
logger.error(formatRendererErrorReport(rawReport));
} catch (error) {
+10 -9
View File
@@ -1,7 +1,8 @@
import { BrowserWindow, session } from "electron";
import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils";
import { API_BASE_URL, REQUEST_RETRIES } from "./constants";
import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils";
import { API_BASE_URL, REQUEST_RETRIES } from "./constants";
import { applyRemoteLoginSecurity, createRemoteLoginWebPreferences, REALDEBRID_LOGIN_HOSTS } from "./browser-security";
const RD_BASE_URL = "https://real-debrid.com";
const RD_LOGIN_URL = RD_BASE_URL;
@@ -217,12 +218,12 @@ export class RealDebridWebFallback {
minHeight: 760,
autoHideMenuBar: true,
title: "Real-Debrid Web-Login",
webPreferences: {
partition,
contextIsolation: true,
nodeIntegration: false
}
});
webPreferences: createRemoteLoginWebPreferences(partition)
});
applyRemoteLoginSecurity(window, {
providerHosts: REALDEBRID_LOGIN_HOSTS,
externalHosts: REALDEBRID_LOGIN_HOSTS
});
window.setMenuBarVisibility(false);
window.webContents.setUserAgent(RD_USER_AGENT);
const primeFromWindow = (): void => {