fix(startup): preserve Electron WebContents receivers
This commit is contained in:
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+12
-2
@@ -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
|
||||
@@ -182,9 +186,13 @@ function createWindow(): BrowserWindow {
|
||||
window.setAutoHideMenuBar(true);
|
||||
|
||||
if (isDevMode()) {
|
||||
void window.loadURL(DEV_SERVER_URL);
|
||||
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"));
|
||||
void window.loadFile(path.join(app.getAppPath(), "build", "renderer", "index.html")).catch((error) => {
|
||||
logger.error(`Renderer-Start fehlgeschlagen: ${String(error?.stack || error)}`);
|
||||
});
|
||||
}
|
||||
|
||||
return window;
|
||||
@@ -956,11 +964,13 @@ app.whenReady().then(() => {
|
||||
}
|
||||
});
|
||||
}).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();
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user