import type { IncomingHttpHeaders } from "node:http"; import type { ScreenProxyTarget } from "@rakazo/core/node/screen-capability"; import { isScreenProxyTarget, SCREEN_RECHECK_MS, SCREEN_TARGET_ENDPOINT, } from "@rakazo/core/node/screen-capability"; const SENSITIVE_FORWARD_HEADERS = new Set([ "authorization", "cookie", "host", "proxy-authenticate", "proxy-authorization", ]); /** Fail closed when the authoritative lifecycle check is unavailable. */ export async function resolveNovncTarget( url: string | undefined, secret: string, api: string, ): Promise { if (!url?.startsWith("/novnc/session/")) return null; try { const response = await fetch(new URL(SCREEN_TARGET_ENDPOINT, api), { method: "POST", redirect: "error", signal: AbortSignal.timeout(2_000), headers: { authorization: `Bearer ${secret}`, "content-type": "application/json" }, body: JSON.stringify({ path: url }), }); if (!response.ok) return null; const target: unknown = await response.json(); return isScreenProxyTarget(target) ? target : null; } catch { return null; } } function isHttp2PseudoHeader(key: string) { return key.startsWith(":"); } export function safeProxyHeaders(headers: IncomingHttpHeaders) { return Object.fromEntries( Object.entries(headers).filter(([key, value]) => { return ( value != null && !isHttp2PseudoHeader(key) && !SENSITIVE_FORWARD_HEADERS.has(key.toLowerCase()) ); }), ); } /** Recheck streams as well as new requests; no positive authorization cache. */ export function watchScreenAuthorization(check: () => Promise, revoke: () => void) { let stopped = false; let timer: ReturnType; const tick = async () => { let allowed = false; try { allowed = Boolean(await check()); } catch { /* Fail closed. */ } if (stopped) return; if (!allowed) { stopped = true; revoke(); return; } timer = setTimeout(tick, SCREEN_RECHECK_MS); timer.unref?.(); }; timer = setTimeout(tick, SCREEN_RECHECK_MS); timer.unref?.(); return () => { stopped = true; clearTimeout(timer); }; }