Files
GO-3.0/electron/src/audio-surface-https.ts
2026-05-14 18:19:37 +03:00

271 lines
8.2 KiB
TypeScript

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<string> | 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<string> {
if (audioSurfaceServer) {
return audioSurfaceServer.origin;
}
if (audioSurfaceServerReady) {
return audioSurfaceServerReady;
}
const host = getAudioSurfaceHttpsHost();
const port = getAudioSurfaceHttpsPort();
const origin = getAudioSurfaceHttpsOrigin();
audioSurfaceServerReady = new Promise<string>(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<string, string>;
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<string | null> {
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<string, string> {
const headers: Record<string, string> = {};
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}`;
}