// SPDX-License-Identifier: AGPL-3.0-or-later import {randomBytes} from 'node:crypto'; import {DESKTOP_APP_ORIGIN} from '@electron/common/Constants'; import {isLocalAppRendererDocumentURL, isLocalAppURL} from '@electron/main/LocalAppURL'; import {webContents as electronWebContents, webFrameMain as electronWebFrameMain} from 'electron'; export const DESKTOP_PROTOCOL_AUTHORIZATION_HEADER = 'X-Fluxer-Desktop-Protocol-Authorization'; const DESKTOP_PROTOCOL_AUTHORIZATION_HEADER_LOWER = DESKTOP_PROTOCOL_AUTHORIZATION_HEADER.toLowerCase(); const AUTHORIZATION_SECRET_BYTES = 32; const MAIN_FRAME_RESOURCE_TYPE = 'mainFrame'; const BLANK_FRAME_URLS: ReadonlySet = new Set(['', 'about:blank']); interface LocalAppRequestSnapshot { readonly url: string; readonly resourceType: string; readonly frame: Electron.WebFrameMain | null; readonly webContents: Electron.WebContents | null; readonly webContentsId: number | null; } class InvalidLocalAppRequestDetailsError extends TypeError { constructor() { super('Electron request details have invalid or inconsistent fields'); this.name = 'InvalidLocalAppRequestDetailsError'; } } class InvalidLocalAppWebContentsError extends TypeError { constructor() { super('Local app authorization requires a live Electron WebContents reference'); this.name = 'InvalidLocalAppWebContentsError'; } } export class DesktopLocalAppAuthorization { private readonly authorizationSecret = randomBytes(AUTHORIZATION_SECRET_BYTES).toString('hex'); private readonly authorizedWebContents = new Map(); authorize(webContents: Electron.WebContents): void { if (!isWebContentsReference(webContents) || webContents.isDestroyed()) { throw new InvalidLocalAppWebContentsError(); } const webContentsId = webContents.id; const registered = this.authorizedWebContents.get(webContentsId); if (registered != null) { if (registered !== webContents) { throw new InvalidLocalAppWebContentsError(); } return; } this.authorizedWebContents.set(webContentsId, webContents); webContents.once('destroyed', () => { if (this.authorizedWebContents.get(webContentsId) === webContents) { this.authorizedWebContents.delete(webContentsId); } }); } hasValidRequestAuthorization(request: Request): boolean { return request.headers.get(DESKTOP_PROTOCOL_AUTHORIZATION_HEADER) === this.authorizationSecret; } applyRequestHeaders(details: unknown, headers: Record): Record { const next = withoutAuthorizationHeader(headers); if (!this.canAuthorizeRequest(details)) { return next; } next[DESKTOP_PROTOCOL_AUTHORIZATION_HEADER] = this.authorizationSecret; return next; } private canAuthorizeRequest(details: unknown): boolean { if (!isLocalAppURL(readRequestURL(details))) { return false; } let snapshot: LocalAppRequestSnapshot; try { snapshot = readRequestSnapshot(details); } catch { return false; } const owner = this.resolveAuthorizedWebContents(snapshot); if (owner == null) { return false; } const frame = snapshot.frame; if (frame == null || !isFrameOwnedByWebContents(frame, owner)) { return false; } if (isLocalAppMainFrameBootstrap(snapshot, frame, owner)) { return true; } return isTrustedLocalAppFrame(frame); } private resolveAuthorizedWebContents(snapshot: LocalAppRequestSnapshot): Electron.WebContents | null { const webContentsId = snapshot.webContentsId; if (webContentsId == null) { return null; } const registered = this.authorizedWebContents.get(webContentsId); if (registered == null) { return null; } try { if (registered.isDestroyed() || registered.id !== webContentsId) { return null; } } catch { return null; } if (snapshot.webContents != null && snapshot.webContents !== registered) { return null; } return registered; } } let authorization: DesktopLocalAppAuthorization | null = null; export function getDesktopLocalAppAuthorization(): DesktopLocalAppAuthorization { authorization ??= new DesktopLocalAppAuthorization(); return authorization; } function isLocalAppMainFrameBootstrap( snapshot: LocalAppRequestSnapshot, frame: Electron.WebFrameMain, owner: Electron.WebContents, ): boolean { if (snapshot.resourceType !== MAIN_FRAME_RESOURCE_TYPE) { return false; } if (!isLocalAppRendererDocumentURL(snapshot.url)) { return false; } try { if (frame.parent != null || frame.top !== frame) { return false; } return owner.mainFrame === frame; } catch { return false; } } function isTrustedLocalAppFrame(frame: Electron.WebFrameMain): boolean { try { const frameURL: unknown = frame.url; if (typeof frameURL !== 'string') { return false; } if (isLocalAppRendererDocumentURL(frameURL)) { return true; } if (!BLANK_FRAME_URLS.has(frameURL)) { return false; } return frame.origin === DESKTOP_APP_ORIGIN; } catch { return false; } } function isFrameOwnedByWebContents(frame: Electron.WebFrameMain, owner: Electron.WebContents): boolean { try { if (frame.isDestroyed() || frame.detached) { return false; } return electronWebContents.fromFrame(frame) === owner; } catch { return false; } } function withoutAuthorizationHeader(headers: Record): Record { const next: Record = Object.create(null) as Record; for (const [name, value] of Object.entries(headers)) { if (name.toLowerCase() !== DESKTOP_PROTOCOL_AUTHORIZATION_HEADER_LOWER) { next[name] = value; } } return next; } function readObjectRecord(value: unknown): Readonly> | null { if (typeof value !== 'object' || value === null) { return null; } return value as Readonly>; } function readRequestURL(details: unknown): string | null { const record = readObjectRecord(details); if (record == null || typeof record.url !== 'string') { return null; } return record.url; } function isWebContentsReference(value: unknown): value is Electron.WebContents { const record = readObjectRecord(value); if (record == null) { return false; } let id: unknown; try { id = record.id; } catch { return false; } if (typeof id !== 'number' || !Number.isSafeInteger(id) || id <= 0) { return false; } let resolved: unknown; try { resolved = electronWebContents.fromId(id); } catch { return false; } return resolved === value; } function isWebFrameMainReference(value: unknown): value is Electron.WebFrameMain { const record = readObjectRecord(value); if (record == null) { return false; } let processId: unknown; let routingId: unknown; try { processId = record.processId; routingId = record.routingId; } catch { return false; } if (typeof processId !== 'number' || !Number.isSafeInteger(processId) || processId < 0) { return false; } if (typeof routingId !== 'number' || !Number.isSafeInteger(routingId) || routingId < 0) { return false; } let resolved: unknown; try { resolved = electronWebFrameMain.fromId(processId, routingId); } catch { return false; } return resolved === value; } function readRequestSnapshot(details: unknown): LocalAppRequestSnapshot { const record = readObjectRecord(details); if (record == null) { throw new InvalidLocalAppRequestDetailsError(); } const url = record.url; const resourceType = record.resourceType; if (typeof url !== 'string' || typeof resourceType !== 'string') { throw new InvalidLocalAppRequestDetailsError(); } const frame = readOptionalWebFrameMain(record.frame); let webContents = readOptionalWebContents(record.webContents); let webContentsId = readOptionalWebContentsId(record.webContentsId); if (webContents != null) { const ownerId = readWebContentsId(webContents); if (webContentsId != null && webContentsId !== ownerId) { throw new InvalidLocalAppRequestDetailsError(); } webContentsId = ownerId; } if (frame != null) { let frameWebContents: unknown; try { frameWebContents = electronWebContents.fromFrame(frame); } catch { throw new InvalidLocalAppRequestDetailsError(); } if (!isWebContentsReference(frameWebContents)) { throw new InvalidLocalAppRequestDetailsError(); } const frameWebContentsId = readWebContentsId(frameWebContents); if (webContents != null && webContents !== frameWebContents) { throw new InvalidLocalAppRequestDetailsError(); } if (webContentsId != null && webContentsId !== frameWebContentsId) { throw new InvalidLocalAppRequestDetailsError(); } webContents = frameWebContents; webContentsId = frameWebContentsId; } return Object.freeze({url, resourceType, frame, webContents, webContentsId}); } function readOptionalWebContents(value: unknown): Electron.WebContents | null { if (value == null) { return null; } if (!isWebContentsReference(value)) { throw new InvalidLocalAppRequestDetailsError(); } return value; } function readOptionalWebFrameMain(value: unknown): Electron.WebFrameMain | null { if (value == null) { return null; } if (!isWebFrameMainReference(value)) { throw new InvalidLocalAppRequestDetailsError(); } return value; } function readOptionalWebContentsId(value: unknown): number | null { if (value == null) { return null; } if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) { throw new InvalidLocalAppRequestDetailsError(); } return value; } function readWebContentsId(webContents: Electron.WebContents): number { let value: unknown; try { value = webContents.id; } catch { throw new InvalidLocalAppRequestDetailsError(); } const webContentsId = readOptionalWebContentsId(value); if (webContentsId == null) { throw new InvalidLocalAppRequestDetailsError(); } return webContentsId; }