import https from 'https'; import { promises as fs } from 'fs'; import { extname, join, normalize, resolve, sep } from 'path'; import { generate as generateCert } from 'selfsigned'; import { AUDIO_SURFACE_ENTRY_PATH } from './audio-window-policy'; import { log as loggerLog } from './logger'; import { trustPinnedCertificateForHost } from './local-https-cert'; const AUDIO_SURFACE_HTTPS_BASE_PORT = 56000; let audioSurfaceInstanceIndex = 0; let audioSurfaceServer: | { host: string; origin: string; port: number; server: https.Server; } | null = null; let audioSurfaceServerReady: Promise | null = null; export function setAudioSurfaceHttpsInstanceIndex(index: number): void { audioSurfaceInstanceIndex = Math.max(0, Math.trunc(index)); } export function getAudioSurfaceHttpsHost(): string { return `audio-surface-${audioSurfaceInstanceIndex}.localhost`; } export function getAudioSurfaceHttpsPort(): number { return AUDIO_SURFACE_HTTPS_BASE_PORT + audioSurfaceInstanceIndex; } export function getAudioSurfaceHttpsOrigin(): string { return `https://${getAudioSurfaceHttpsHost()}:${getAudioSurfaceHttpsPort()}`; } export async function ensureAudioSurfaceHttpsServer( directory: string ): Promise { if (audioSurfaceServer) { return audioSurfaceServer.origin; } if (audioSurfaceServerReady) { return audioSurfaceServerReady; } const host = getAudioSurfaceHttpsHost(); const port = getAudioSurfaceHttpsPort(); const origin = getAudioSurfaceHttpsOrigin(); audioSurfaceServerReady = new Promise(async (resolvePromise, rejectPromise) => { try { const notAfterDate = new Date(); notAfterDate.setFullYear(notAfterDate.getFullYear() + 10); const pems = await generateCert( [{ name: 'commonName', value: host }], { algorithm: 'sha256', keySize: 2048, notAfterDate, extensions: [ { name: 'basicConstraints', cA: false }, { name: 'keyUsage', digitalSignature: true, keyEncipherment: true, }, { name: 'subjectAltName', altNames: [ { type: 2, value: host }, { type: 2, value: 'localhost' }, { type: 7, ip: '127.0.0.1' }, ], }, ], } ); trustPinnedCertificateForHost(host, pems.cert); const server = https.createServer( { key: pems.private, cert: pems.cert, }, async (req, res) => { try { const requestUrl = new URL(req.url || '/', origin); const result = await buildStaticResponse( requestUrl.pathname, directory ); if (shouldTraceAudioSurfaceIsolationAsset(requestUrl.pathname)) { loggerLog('[GCall:audio-surface][https] response', { url: requestUrl.toString(), pathname: requestUrl.pathname, contentType: result.contentType, coop: result.headers['Cross-Origin-Opener-Policy'] ?? null, coep: result.headers['Cross-Origin-Embedder-Policy'] ?? null, corp: result.headers['Cross-Origin-Resource-Policy'] ?? null, }); } res.writeHead(result.status, { ...result.headers, 'content-type': result.contentType, }); res.end(result.body); } catch { res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' }); res.end('Internal Server Error'); } } ); server.once('error', (error) => { audioSurfaceServerReady = null; rejectPromise(error); }); server.listen(port, '127.0.0.1', () => { audioSurfaceServer = { host, origin, port, server }; loggerLog('[GCall:audio-surface][https] listening', { host, origin, port, }); resolvePromise(origin); }); } catch (error) { audioSurfaceServerReady = null; rejectPromise(error); } }); return audioSurfaceServerReady; } async function buildStaticResponse( requestPathname: string, directory: string ): Promise<{ status: number; headers: Record; contentType: string; body: Uint8Array | string; }> { const indexPath = join(directory, 'index.html'); const filePath = resolveStaticPath(directory, requestPathname); const fileExtension = extname(filePath); const resolvedPath = await getExistingFilePath(filePath); const shouldFallbackToIndex = !resolvedPath && (!fileExtension || fileExtension === '.html' || fileExtension === '.asar'); const finalPath = resolvedPath ?? (shouldFallbackToIndex ? indexPath : null); if (!finalPath) { return { status: 404, headers: {}, contentType: 'text/plain; charset=utf-8', body: 'Not Found', }; } const data = await fs.readFile(finalPath); return { status: 200, headers: staticHeadersForPath(requestPathname), contentType: mimeTypeForPath(finalPath), body: new Uint8Array(data), }; } function resolveStaticPath(directory: string, requestPathname: string): string { const decodedPath = decodeURIComponent(requestPathname); const sanitizedRelative = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, ''); const resolved = resolve(directory, `.${sep}${sanitizedRelative}`); const normalizedDirectory = ensureTrailingSep(resolve(directory)); if (!ensureTrailingSep(resolved).startsWith(normalizedDirectory)) { return resolve(directory, 'index.html'); } return resolved; } async function getExistingFilePath(filePath: string): Promise { try { const stat = await fs.stat(filePath); if (stat.isFile()) return filePath; if (stat.isDirectory()) { return getExistingFilePath(join(filePath, 'index.html')); } } catch { /* ignore */ } return null; } function staticHeadersForPath( requestPathname: string ): Record { const headers: Record = {}; if (requestPathname === AUDIO_SURFACE_ENTRY_PATH) { headers['Cross-Origin-Opener-Policy'] = 'same-origin'; headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; headers['Cross-Origin-Resource-Policy'] = 'same-origin'; return headers; } if (isEmbedderCriticalAsset(requestPathname)) { headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; headers['Cross-Origin-Resource-Policy'] = 'same-origin'; } return headers; } function isEmbedderCriticalAsset(requestPathname: string): boolean { return ( (requestPathname.startsWith('/assets/') && /\.(?:js|mjs|cjs|css|wasm)$/i.test(requestPathname)) || /^\/registerSW\.js$/i.test(requestPathname) || /^\/manifest\.webmanifest$/i.test(requestPathname) ); } function shouldTraceAudioSurfaceIsolationAsset(requestPathname: string): boolean { return ( requestPathname === AUDIO_SURFACE_ENTRY_PATH || /\/assets\/.*\.(?:js|mjs|cjs|css|wasm)$/i.test(requestPathname) || /\/(?:registerSW\.js|manifest\.webmanifest)$/i.test(requestPathname) || /\/assets\/(?:audio-decrypt\.worker|gcall-audio-encode\.worker|gcall-opus-fec\.worker|group-playout-processor|gcall-jitter-scheduler).*?\.(?:js|mjs|cjs|wasm)$/i.test( requestPathname ) ); } function mimeTypeForPath(filePath: string): string { switch (extname(filePath).toLowerCase()) { case '.html': return 'text/html; charset=utf-8'; case '.js': case '.mjs': case '.cjs': return 'text/javascript; charset=utf-8'; case '.css': return 'text/css; charset=utf-8'; case '.json': return 'application/json; charset=utf-8'; case '.webmanifest': return 'application/manifest+json; charset=utf-8'; case '.wasm': return 'application/wasm'; case '.svg': return 'image/svg+xml'; case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; case '.ico': return 'image/x-icon'; case '.map': return 'application/json; charset=utf-8'; default: return 'application/octet-stream'; } } function ensureTrailingSep(value: string): string { return value.endsWith(sep) ? value : `${value}${sep}`; }