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.
|
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
|
## [2.0.27] - 2026-08-12
|
||||||
|
|
||||||
### Startup reliability
|
### Startup reliability
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.27",
|
"version": "2.0.28",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.27",
|
"version": "2.0.28",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "0.6.0",
|
"adm-zip": "0.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.27",
|
"version": "2.0.28",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@@ -126,19 +126,19 @@ export function applyRemoteLoginSecurity(window: SecurityWindow, options: Remote
|
|||||||
|
|
||||||
function applyCommonSecurity(window: SecurityWindow, externalHosts: readonly HttpsHostRule[]): void {
|
function applyCommonSecurity(window: SecurityWindow, externalHosts: readonly HttpsHostRule[]): void {
|
||||||
const setWindowOpenHandler = window.webContents.setWindowOpenHandler as (handler: (details: { url: string }) => { action: "deny" }) => unknown;
|
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);
|
void openAllowedExternalUrl(details.url, externalHosts);
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
const setPermissionRequestHandler = window.webContents.session.setPermissionRequestHandler as (handler: (webContents: unknown, permission: string, callback: (allowed: boolean) => void) => void) => unknown;
|
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);
|
callback(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNavigation(window: SecurityWindow, event: "will-navigate" | "will-redirect", listener: (event: NavigationEvent, url: string) => void): void {
|
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;
|
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 {
|
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 senderPath = path.resolve(fileURLToPath(parsed));
|
||||||
const rendererPath = path.resolve(appPath, "build", "renderer");
|
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 {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -160,6 +160,10 @@ function createWindow(): BrowserWindow {
|
|||||||
webPreferences: createMainWindowWebPreferences(path.join(__dirname, "../preload/preload.js"))
|
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, {
|
applyMainWindowSecurity(window, {
|
||||||
rendererUrl: getTrustedRendererUrl(),
|
rendererUrl: getTrustedRendererUrl(),
|
||||||
externalHosts: MAIN_WINDOW_EXTERNAL_HOSTS
|
externalHosts: MAIN_WINDOW_EXTERNAL_HOSTS
|
||||||
@@ -182,9 +186,13 @@ function createWindow(): BrowserWindow {
|
|||||||
window.setAutoHideMenuBar(true);
|
window.setAutoHideMenuBar(true);
|
||||||
|
|
||||||
if (isDevMode()) {
|
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 {
|
} 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;
|
return window;
|
||||||
@@ -956,11 +964,13 @@ app.whenReady().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
logger.error(`App-Start fehlgeschlagen: ${String(error?.stack || error)}`);
|
||||||
console.error("App startup failed:", error);
|
console.error("App startup failed:", error);
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
|
logger.warn("Alle Hauptfenster geschlossen");
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== "darwin") {
|
||||||
app.quit();
|
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", () => {
|
it("denies main-window navigation to an untrusted origin", () => {
|
||||||
const harness = createWindow();
|
const harness = createWindow();
|
||||||
applyMainWindowSecurity(harness.window, {
|
applyMainWindowSecurity(harness.window, {
|
||||||
|
|||||||
@@ -40,6 +40,25 @@ describe("ipc-security", () => {
|
|||||||
})).not.toThrow();
|
})).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", () => {
|
it("rejects IPC from local files outside the packaged renderer tree", () => {
|
||||||
const appPath = path.join("C:", "Program Files", "MDD", "resources", "app.asar");
|
const appPath = path.join("C:", "Program Files", "MDD", "resources", "app.asar");
|
||||||
const attackerUrl = pathToFileURL(path.join("C:", "Users", "Public", "attacker.html")).toString();
|
const attackerUrl = pathToFileURL(path.join("C:", "Users", "Public", "attacker.html")).toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user