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:
+129
-104
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user