fix(startup): preserve Electron WebContents receivers

This commit is contained in:
Sucukdeluxe
2026-08-12 21:48:45 +02:00
parent d58c3a262f
commit ca015553f3
8 changed files with 96 additions and 26 deletions
+8
View File
@@ -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
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
+3 -3
View File
@@ -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 {
+3 -1
View File
@@ -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;
}
+29 -19
View File
@@ -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();
+31
View File
@@ -96,6 +96,37 @@ describe("browser-security", () => {
});
});
it("keeps native WebContents receivers when registering main-window security", () => {
const listeners = new Map<string, NavigationHandler>();
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, {
+19
View File
@@ -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();