feat(desktop): remote-host mode + OS-store client-cert mTLS (D-1/D-2)
- 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.
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
/**
|
||||
* desktop/src/window.ts — creates the single BrowserWindow that hosts the
|
||||
* unchanged web frontend, loaded from the embedded localhost server.
|
||||
* 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. Because the
|
||||
* only page ever loaded is the trusted embedded http://127.0.0.1:<port> origin,
|
||||
* 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 localhost.
|
||||
* 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'
|
||||
|
||||
@@ -15,7 +22,7 @@ const WINDOW_HEIGHT = 720
|
||||
const BACKGROUND_COLOR = '#0e0f13'
|
||||
|
||||
/** Parse the origin of a URL, returning null for anything malformed. */
|
||||
function originOf(url: string): string | null {
|
||||
export function originOf(url: string): string | null {
|
||||
try {
|
||||
return new URL(url).origin
|
||||
} catch {
|
||||
@@ -23,7 +30,16 @@ function originOf(url: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function createMainWindow(url: string, preloadPath: string): BrowserWindow {
|
||||
/**
|
||||
* 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,
|
||||
@@ -36,15 +52,14 @@ export function createMainWindow(url: string, preloadPath: string): BrowserWindo
|
||||
},
|
||||
})
|
||||
|
||||
const allowedOrigin = originOf(url)
|
||||
|
||||
// Never spawn child windows; the frontend has no legitimate reason to.
|
||||
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
|
||||
|
||||
// Block navigation away from the embedded localhost origin.
|
||||
// Block navigation away from the currently-active (local or remote) origin.
|
||||
win.webContents.on('will-navigate', (event, targetUrl) => {
|
||||
if (allowedOrigin === null) return
|
||||
if (originOf(targetUrl) !== allowedOrigin) {
|
||||
const allowedOrigin = getAllowedOrigin()
|
||||
// Fail closed: if we can't determine the active origin, allow nothing.
|
||||
if (allowedOrigin === null || originOf(targetUrl) !== allowedOrigin) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user