- remote-hosts.ts + DesktopPrefs (remoteHosts/selectedHostId, immutable, back-compat) + tray host-picker. - will-navigate lock is now dynamic to the selected remote origin, fails closed on null (no open-redirect). - select-client-certificate handler: event.preventDefault() first, pick by issuer CN, non-silent missing-cert dialog + cert-less callback -> clean nginx rejection (Chromium sources certs from OS store). Verified: npm run typecheck exit 0; pure-helper assertions pass.
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
/**
|
|
* desktop/src/window.ts — creates the single BrowserWindow that hosts the
|
|
* unchanged web frontend, loaded from either the embedded localhost server or a
|
|
* selected remote tunnel host (D-1, PLAN_NATIVE_TUNNEL / C-Desktop).
|
|
*
|
|
* Hardening (DESKTOP_PLAN §8 / TECH_DOC §7): contextIsolation on, nodeIntegration
|
|
* off, sandbox on, preload restricted to a minimal contextBridge. We deny every
|
|
* new-window request and block navigation to any foreign origin — a defence-in-
|
|
* depth guard against a hijacked page trying to escape the active origin.
|
|
*
|
|
* D-1 note: the origin lock is DYNAMIC. The window may be pointed (by main.ts,
|
|
* via a programmatic loadURL — which does NOT fire will-navigate) at exactly one
|
|
* host at a time: the embedded `http://127.0.0.1:<port>` origin OR the selected
|
|
* remote `https://<name>.terminal.yaojia.wang` origin. `getAllowedOrigin` returns
|
|
* whichever is active NOW, so renderer-initiated navigation stays locked to that
|
|
* single origin and everything else is still blocked.
|
|
*/
|
|
import { BrowserWindow } from 'electron'
|
|
|
|
const WINDOW_WIDTH = 1100
|
|
const WINDOW_HEIGHT = 720
|
|
const BACKGROUND_COLOR = '#0e0f13'
|
|
|
|
/** Parse the origin of a URL, returning null for anything malformed. */
|
|
export function originOf(url: string): string | null {
|
|
try {
|
|
return new URL(url).origin
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the main window. `url` is the initial page to load; `getAllowedOrigin`
|
|
* is queried live on every navigation attempt and must return the origin the
|
|
* window is currently allowed on (or null to allow nothing / fail closed).
|
|
*/
|
|
export function createMainWindow(
|
|
url: string,
|
|
preloadPath: string,
|
|
getAllowedOrigin: () => string | null,
|
|
): BrowserWindow {
|
|
const win = new BrowserWindow({
|
|
width: WINDOW_WIDTH,
|
|
height: WINDOW_HEIGHT,
|
|
backgroundColor: BACKGROUND_COLOR,
|
|
webPreferences: {
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
sandbox: true,
|
|
preload: preloadPath,
|
|
},
|
|
})
|
|
|
|
// Never spawn child windows; the frontend has no legitimate reason to.
|
|
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
|
|
|
|
// Block navigation away from the currently-active (local or remote) origin.
|
|
win.webContents.on('will-navigate', (event, targetUrl) => {
|
|
const allowedOrigin = getAllowedOrigin()
|
|
// Fail closed: if we can't determine the active origin, allow nothing.
|
|
if (allowedOrigin === null || originOf(targetUrl) !== allowedOrigin) {
|
|
event.preventDefault()
|
|
}
|
|
})
|
|
|
|
void win.loadURL(url)
|
|
return win
|
|
}
|