import { createHash, timingSafeEqual } from 'node:crypto'; import { createServer, type IncomingMessage, type ServerResponse, } from 'node:http'; import { ClusterCopilotClientConfigurationError, ClusterCopilotClientRemoteError, ClusterCopilotClientRequestError, type ClusterCopilotClientCommand, type ClusterCopilotClientResult, } from '../copilot-client/client'; import { type ClusterCopilotConsoleAssets, } from './assets'; import { CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA, InvalidClusterCopilotConsoleReadRequestError, clusterCopilotConsoleClientCommand, normalizeClusterCopilotConsoleReadRequest, } from './contracts'; export const CLUSTER_COPILOT_CONSOLE_LIMITS = Object.freeze({ maximumBodyBytes: 4 * 1024, maximumResponseBytes: 2 * 1024 * 1024 + 4 * 1024, maximumConcurrentRequests: 2, maximumConnections: 16, shutdownTimeoutMs: 2_000, }); export interface ClusterCopilotConsoleExecutor { execute( command: Readonly, ): Promise>; } export interface ClusterCopilotConsoleServerOptions { readonly assets: Readonly; readonly executor: ClusterCopilotConsoleExecutor; readonly port: number; readonly sessionDigest: Buffer; } export interface ClusterCopilotConsoleServer { readonly origin: string; close(): Promise; } export class ClusterCopilotConsoleConfigurationError extends TypeError { readonly code = 'QL3_CLUSTER_COPILOT_CONSOLE_CONFIG_INVALID'; constructor() { super('Cluster Copilot Console configuration is invalid'); this.name = 'ClusterCopilotConsoleConfigurationError'; } } const SESSION_TOKEN = /^[A-Za-z0-9_-]{43}$/; const SESSION_DIGEST_DOMAIN = Buffer.from( 'qinglong-cluster-copilot-console-session-v1\0', 'utf8', ); const CONTENT_SECURITY_POLICY = [ "default-src 'none'", "base-uri 'none'", "connect-src 'self'", "form-action 'self'", "frame-ancestors 'none'", "script-src 'self'", "style-src 'self'", "img-src 'none'", "font-src 'none'", "object-src 'none'", "media-src 'none'", "manifest-src 'none'", "worker-src 'none'", ].join('; '); function invalid(): never { throw new ClusterCopilotConsoleConfigurationError(); } function exactObject( value: unknown, keys: readonly string[], ): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { return invalid(); } const record = value as Record; const actual = Object.keys(record).sort(); const expected = [...keys].sort(); if ( actual.length !== expected.length || actual.some((key, index) => key !== expected[index]) ) { return invalid(); } return record; } export function clusterCopilotConsoleSessionDigest(value: string): Buffer { if (typeof value !== 'string' || !SESSION_TOKEN.test(value)) { return invalid(); } const decoded = Buffer.from(value, 'base64url'); if ( decoded.byteLength !== 32 || decoded.toString('base64url') !== value ) { decoded.fill(0); return invalid(); } decoded.fill(0); return createHash('sha256') .update(SESSION_DIGEST_DOMAIN) .update(value, 'ascii') .digest(); } function securityHeaders(contentType: string): Readonly> { return Object.freeze({ 'cache-control': 'no-store', 'content-security-policy': CONTENT_SECURITY_POLICY, 'content-type': contentType, 'cross-origin-opener-policy': 'same-origin', 'cross-origin-resource-policy': 'same-origin', 'origin-agent-cluster': '?1', 'permissions-policy': 'camera=(), display-capture=(), geolocation=(), microphone=(), payment=(), usb=()', 'referrer-policy': 'no-referrer', 'x-content-type-options': 'nosniff', 'x-frame-options': 'DENY', }); } function send( response: ServerResponse, statusCode: number, contentType: string, body: string, extraHeaders: Readonly> = {}, ): void { const bytes = Buffer.from(body, 'utf8'); response.writeHead(statusCode, { ...securityHeaders(contentType), ...extraHeaders, connection: 'close', 'content-length': String(bytes.byteLength), }); response.end(bytes, () => bytes.fill(0)); } function sendJson( response: ServerResponse, statusCode: number, body: Readonly>, extraHeaders: Readonly> = {}, ): void { send( response, statusCode, 'application/json; charset=utf-8', JSON.stringify(body), extraHeaders, ); } function headerCount(request: IncomingMessage, name: string): number { let count = 0; for (let index = 0; index < request.rawHeaders.length; index += 2) { if (request.rawHeaders[index]?.toLowerCase() === name) count += 1; } return count; } function targetPath(request: IncomingMessage): 'inspect' | 'output' | null { if (request.method !== 'POST') return null; if (request.url === '/api/v1/copilot/inspect') return 'inspect'; if (request.url === '/api/v1/copilot/output') return 'output'; return null; } function authorize( request: IncomingMessage, expectedOrigin: string, sessionDigest: Buffer, ): boolean { if ( headerCount(request, 'authorization') !== 1 || headerCount(request, 'origin') !== 1 || request.headers.origin !== expectedOrigin || request.headers.host !== expectedOrigin.slice('http://'.length) ) { return false; } const authorization = request.headers.authorization; if ( typeof authorization !== 'string' || !authorization.startsWith('QL3-Console ') ) { return false; } let candidate: Buffer | undefined; try { candidate = clusterCopilotConsoleSessionDigest( authorization.slice('QL3-Console '.length), ); return timingSafeEqual(candidate, sessionDigest); } catch { return false; } finally { candidate?.fill(0); } } async function readJsonBody(request: IncomingMessage): Promise { if ( headerCount(request, 'content-type') !== 1 || headerCount(request, 'content-length') !== 1 || request.headers['content-type'] !== 'application/json; charset=utf-8' || request.headers['content-encoding'] !== undefined || request.headers['transfer-encoding'] !== undefined || typeof request.headers['content-length'] !== 'string' || !/^[1-9][0-9]*$/.test(request.headers['content-length']) ) { throw new InvalidClusterCopilotConsoleReadRequestError(); } const expectedLength = Number(request.headers['content-length']); if ( !Number.isSafeInteger(expectedLength) || expectedLength < 2 || expectedLength > CLUSTER_COPILOT_CONSOLE_LIMITS.maximumBodyBytes ) { throw new InvalidClusterCopilotConsoleReadRequestError(); } const chunks: Buffer[] = []; let length = 0; try { for await (const chunk of request) { const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); length += bytes.byteLength; if ( length > expectedLength || length > CLUSTER_COPILOT_CONSOLE_LIMITS.maximumBodyBytes ) { throw new InvalidClusterCopilotConsoleReadRequestError(); } chunks.push(bytes); } if (request.aborted || length !== expectedLength) { throw new InvalidClusterCopilotConsoleReadRequestError(); } const body = Buffer.concat(chunks, length); try { return JSON.parse(body.toString('utf8')); } finally { body.fill(0); } } catch (error) { if (error instanceof InvalidClusterCopilotConsoleReadRequestError) { throw error; } throw new InvalidClusterCopilotConsoleReadRequestError(); } finally { for (const chunk of chunks) chunk.fill(0); } } function remoteFailure( response: ServerResponse, error: ClusterCopilotClientRemoteError, ): void { const statusCode = error.statusCode === 404 ? 404 : error.statusCode === 429 ? 429 : 502; sendJson( response, statusCode, Object.freeze({ schema: CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA, code: error.responseCode, requestId: error.requestId, retryAfterSeconds: error.retryAfterSeconds, }), error.retryAfterSeconds === null ? {} : { 'retry-after': String(error.retryAfterSeconds) }, ); } export async function startClusterCopilotConsoleServer( options: ClusterCopilotConsoleServerOptions, ): Promise> { const record = exactObject(options, [ 'assets', 'executor', 'port', 'sessionDigest', ]); const assets = exactObject(record.assets, ['css', 'html', 'javascript']); if ( typeof assets.html !== 'string' || assets.html.length < 1 || typeof assets.css !== 'string' || assets.css.length < 1 || typeof assets.javascript !== 'string' || assets.javascript.length < 1 || !record.executor || typeof (record.executor as ClusterCopilotConsoleExecutor).execute !== 'function' || !Number.isSafeInteger(record.port) || ((record.port as number) !== 0 && ((record.port as number) < 1_024 || (record.port as number) > 65_535)) || !Buffer.isBuffer(record.sessionDigest) || (record.sessionDigest as Buffer).byteLength !== 32 ) { return invalid(); } const sessionDigest = Buffer.from(record.sessionDigest as Buffer); const executor = record.executor as ClusterCopilotConsoleExecutor; let expectedOrigin = ''; let inFlight = 0; let closed = false; const server = createServer(async (request, response) => { response.shouldKeepAlive = false; const hostMatches = expectedOrigin !== '' && request.headers.host === expectedOrigin.slice('http://'.length); if (request.method === 'GET' && hostMatches) { if (request.url === '/') { send(response, 200, 'text/html; charset=utf-8', assets.html as string); return; } if (request.url === '/app.css') { send(response, 200, 'text/css; charset=utf-8', assets.css as string); return; } if (request.url === '/app.js') { send( response, 200, 'text/javascript; charset=utf-8', assets.javascript as string, ); return; } } const operation = targetPath(request); if ( !hostMatches || operation === null || !authorize(request, expectedOrigin, sessionDigest) ) { sendJson(response, 404, Object.freeze({ code: 'not_found' })); request.resume(); return; } if ( inFlight >= CLUSTER_COPILOT_CONSOLE_LIMITS.maximumConcurrentRequests ) { sendJson( response, 429, Object.freeze({ schema: CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA, code: 'cluster_copilot_console_busy', }), { 'retry-after': '1' }, ); request.resume(); return; } inFlight += 1; try { const body = await readJsonBody(request); const normalized = normalizeClusterCopilotConsoleReadRequest(body); if (normalized.operation !== operation) { throw new InvalidClusterCopilotConsoleReadRequestError(); } const result = await executor.execute( clusterCopilotConsoleClientCommand(normalized), ); const envelope = Object.freeze({ schema: CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA, operation, requestId: result.requestId, result, }); const encoded = JSON.stringify(envelope); if ( Buffer.byteLength(encoded, 'utf8') > CLUSTER_COPILOT_CONSOLE_LIMITS.maximumResponseBytes ) { throw new ClusterCopilotClientRequestError(); } send( response, 200, 'application/json; charset=utf-8', encoded, ); } catch (error) { if (response.headersSent) { response.destroy(); } else if ( error instanceof InvalidClusterCopilotConsoleReadRequestError ) { sendJson( response, 400, Object.freeze({ schema: CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA, code: 'invalid_cluster_copilot_console_read_request', }), ); } else if (error instanceof ClusterCopilotClientRemoteError) { remoteFailure(response, error); } else if ( error instanceof ClusterCopilotClientConfigurationError || error instanceof ClusterCopilotClientRequestError ) { sendJson( response, 503, Object.freeze({ schema: CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA, code: 'cluster_copilot_console_upstream_unavailable', }), ); } else { sendJson( response, 503, Object.freeze({ schema: CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA, code: 'cluster_copilot_console_unavailable', }), ); } } finally { inFlight -= 1; } }); server.maxConnections = CLUSTER_COPILOT_CONSOLE_LIMITS.maximumConnections; server.headersTimeout = 5_000; server.requestTimeout = 5_000; server.keepAliveTimeout = 1; server.maxRequestsPerSocket = 1; server.on('clientError', (_error, socket) => socket.destroy()); try { await new Promise((resolve, reject) => { server.once('error', reject); server.listen(record.port as number, '127.0.0.1', () => { server.off('error', reject); resolve(); }); }); const address = server.address(); if (!address || typeof address === 'string') return invalid(); expectedOrigin = 'http://127.0.0.1:' + String(address.port); } catch (error) { sessionDigest.fill(0); server.closeAllConnections(); if (error instanceof ClusterCopilotConsoleConfigurationError) throw error; throw new ClusterCopilotConsoleConfigurationError(); } return Object.freeze({ origin: expectedOrigin, async close(): Promise { if (closed) return; closed = true; await new Promise((resolve) => { const timeout = setTimeout(() => { server.closeAllConnections(); }, CLUSTER_COPILOT_CONSOLE_LIMITS.shutdownTimeoutMs); timeout.unref(); server.close(() => { clearTimeout(timeout); resolve(); }); server.closeIdleConnections(); }); sessionDigest.fill(0); }, }); }