diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5f2b2..b45185e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to Multi-Debrid Downloader are documented in this file. +## [2.0.28] - 2026-08-12 + +### Startup reliability + +- Fixed Electron security registration so native WebContents and session APIs retain their required receiver during application startup. +- Fixed packaged renderer IPC validation for case-insensitive Windows file paths. +- Added explicit startup and renderer-load error logging for future launch diagnostics. + ## [2.0.27] - 2026-08-12 ### Startup reliability diff --git a/package-lock.json b/package-lock.json index 20f1fa4..f395226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "2.0.27", + "version": "2.0.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "2.0.27", + "version": "2.0.28", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 009cf84..2459a4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "2.0.27", + "version": "2.0.28", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/browser-security.ts b/src/main/browser-security.ts index be7ac8a..d1257c8 100644 --- a/src/main/browser-security.ts +++ b/src/main/browser-security.ts @@ -126,19 +126,19 @@ export function applyRemoteLoginSecurity(window: SecurityWindow, options: Remote function applyCommonSecurity(window: SecurityWindow, externalHosts: readonly HttpsHostRule[]): void { const setWindowOpenHandler = window.webContents.setWindowOpenHandler as (handler: (details: { url: string }) => { action: "deny" }) => unknown; - setWindowOpenHandler((details) => { + setWindowOpenHandler.call(window.webContents, (details) => { void openAllowedExternalUrl(details.url, externalHosts); return { action: "deny" }; }); const setPermissionRequestHandler = window.webContents.session.setPermissionRequestHandler as (handler: (webContents: unknown, permission: string, callback: (allowed: boolean) => void) => void) => unknown; - setPermissionRequestHandler((_webContents, _permission, callback) => { + setPermissionRequestHandler.call(window.webContents.session, (_webContents, _permission, callback) => { callback(false); }); } function onNavigation(window: SecurityWindow, event: "will-navigate" | "will-redirect", listener: (event: NavigationEvent, url: string) => void): void { const on = window.webContents.on as (event: "will-navigate" | "will-redirect", listener: (event: NavigationEvent, url: string) => void) => unknown; - on(event, listener); + on.call(window.webContents, event, listener); } function isExpectedRendererUrl(rawUrl: string, expectedUrl: string): boolean { diff --git a/src/main/ipc-security.ts b/src/main/ipc-security.ts index 998ca1c..defef03 100644 --- a/src/main/ipc-security.ts +++ b/src/main/ipc-security.ts @@ -40,7 +40,9 @@ function isPackagedRendererUrl(senderUrl: string, appPath: string): boolean { } const senderPath = path.resolve(fileURLToPath(parsed)); const rendererPath = path.resolve(appPath, "build", "renderer"); - return senderPath === rendererPath || senderPath.startsWith(rendererPath + path.sep); + const actualPath = process.platform === "win32" ? senderPath.toLowerCase() : senderPath; + const trustedRendererPath = process.platform === "win32" ? rendererPath.toLowerCase() : rendererPath; + return actualPath === trustedRendererPath || actualPath.startsWith(trustedRendererPath + path.sep); } catch { return false; } diff --git a/src/main/main.ts b/src/main/main.ts index fc3cac7..656db10 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -148,8 +148,8 @@ function armScheduledStart(schedMs: number, opts: { startOnPast: boolean }): voi logger.info(`Geplanter Start gearmt: ${new Date(schedMs).toLocaleString()}`); } -function createWindow(): BrowserWindow { - const window = new BrowserWindow({ +function createWindow(): BrowserWindow { + const window = new BrowserWindow({ width: 1920, height: 1080, minWidth: 1120, @@ -160,6 +160,10 @@ function createWindow(): BrowserWindow { webPreferences: createMainWindowWebPreferences(path.join(__dirname, "../preload/preload.js")) }); + window.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { + logger.error(`Renderer-Laden fehlgeschlagen: id=${window.id} code=${errorCode} mainFrame=${isMainFrame} url=${validatedURL} error=${errorDescription}`); + }); + applyMainWindowSecurity(window, { rendererUrl: getTrustedRendererUrl(), externalHosts: MAIN_WINDOW_EXTERNAL_HOSTS @@ -181,10 +185,14 @@ function createWindow(): BrowserWindow { window.setMenuBarVisibility(false); window.setAutoHideMenuBar(true); - if (isDevMode()) { - void window.loadURL(DEV_SERVER_URL); - } else { - void window.loadFile(path.join(app.getAppPath(), "build", "renderer", "index.html")); + if (isDevMode()) { + void window.loadURL(DEV_SERVER_URL).catch((error) => { + logger.error(`Renderer-Start fehlgeschlagen: ${String(error?.stack || error)}`); + }); + } else { + void window.loadFile(path.join(app.getAppPath(), "build", "renderer", "index.html")).catch((error) => { + logger.error(`Renderer-Start fehlgeschlagen: ${String(error?.stack || error)}`); + }); } return window; @@ -955,19 +963,21 @@ app.whenReady().then(() => { bindMainWindowLifecycle(mainWindow); } }); -}).catch((error) => { - console.error("App startup failed:", error); - app.quit(); -}); - -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - app.quit(); - } -}); - -app.on("before-quit", () => { - if (updateQuitTimer) { clearTimeout(updateQuitTimer); updateQuitTimer = null; } +}).catch((error) => { + logger.error(`App-Start fehlgeschlagen: ${String(error?.stack || error)}`); + console.error("App startup failed:", error); + app.quit(); +}); + +app.on("window-all-closed", () => { + logger.warn("Alle Hauptfenster geschlossen"); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + if (updateQuitTimer) { clearTimeout(updateQuitTimer); updateQuitTimer = null; } stopClipboardWatcher(); destroyTray(); shutdownDaemon(); diff --git a/tests/browser-security.test.ts b/tests/browser-security.test.ts index b3637c8..c14cfa5 100644 --- a/tests/browser-security.test.ts +++ b/tests/browser-security.test.ts @@ -96,6 +96,37 @@ describe("browser-security", () => { }); }); + it("keeps native WebContents receivers when registering main-window security", () => { + const listeners = new Map(); + const webContents = { + on(this: unknown, event: string, listener: NavigationHandler) { + if (this !== webContents) { + throw new TypeError("WebContents receiver missing"); + } + listeners.set(event, listener); + }, + setWindowOpenHandler(this: unknown, _handler: WindowOpenHandler) { + if (this !== webContents) { + throw new TypeError("WebContents receiver missing"); + } + }, + session: { + setPermissionRequestHandler(this: unknown, _handler: PermissionHandler) { + if (this !== webContents.session) { + throw new TypeError("Session receiver missing"); + } + } + } + }; + + expect(() => applyMainWindowSecurity({ webContents }, { + rendererUrl: "http://localhost:5180", + externalHosts: githubOnly + })).not.toThrow(); + expect(listeners.has("will-navigate")).toBe(true); + expect(listeners.has("will-redirect")).toBe(true); + }); + it("denies main-window navigation to an untrusted origin", () => { const harness = createWindow(); applyMainWindowSecurity(harness.window, { diff --git a/tests/ipc-security.test.ts b/tests/ipc-security.test.ts index d47e800..e7a2ced 100644 --- a/tests/ipc-security.test.ts +++ b/tests/ipc-security.test.ts @@ -40,6 +40,25 @@ describe("ipc-security", () => { })).not.toThrow(); }); + it("accepts packaged renderer IPC despite Windows path casing differences", () => { + const platform = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); + try { + const appPath = path.join("C:", "Program Files", "MDD", "resources", "app.asar"); + const rendererUrl = "file:///c:/program%20files/mdd/resources/app.asar/build/renderer/index.html"; + + expect(() => assertTrustedIpcSender(eventFor(rendererUrl), { + isPackaged: true, + devServerUrl: "http://localhost:5180", + appPath + })).not.toThrow(); + } finally { + if (platform) { + Object.defineProperty(process, "platform", platform); + } + } + }); + it("rejects IPC from local files outside the packaged renderer tree", () => { const appPath = path.join("C:", "Program Files", "MDD", "resources", "app.asar"); const attackerUrl = pathToFileURL(path.join("C:", "Users", "Public", "attacker.html")).toString();