import { spawn } from 'node:child_process' import { mkdtemp, rm, stat } from 'node:fs/promises' import { createServer } from 'node:net' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import https from 'node:https' import { readFile } from 'node:fs/promises' const projectDir = resolve(import.meta.dirname, '..') function freePort() { return new Promise((resolvePort, reject) => { const server = createServer() server.once('error', reject) server.listen(0, '127.0.0.1', () => { const { port } = server.address() server.close((error) => error ? reject(error) : resolvePort(port)) }) }) } function requestJson(url, options, ca) { return new Promise((resolveRequest, reject) => { const request = https.request(url, { ...options, ca }, (response) => { const chunks = [] response.on('data', (chunk) => chunks.push(chunk)) response.on('end', () => { try { resolveRequest({ status: response.statusCode, headers: response.headers, body: JSON.parse(Buffer.concat(chunks).toString('utf8')), }) } catch (error) { reject(error) } }) }) request.once('error', reject) request.end() }) } async function waitForConnector(url, ca) { const deadline = Date.now() + 10_000 let lastError while (Date.now() < deadline) { try { const response = await requestJson(url, { headers: { Origin: 'https://safari.test' } }, ca) if (response.status === 200) return response lastError = new Error(`健康检查返回 ${response.status}`) } catch (error) { lastError = error } await new Promise((resolveDelay) => setTimeout(resolveDelay, 150)) } throw lastError || new Error('HTTPS 连接器启动超时') } const temp = await mkdtemp(join(tmpdir(), 'spine-connector-tls-test-')) const [httpPort, httpsPort] = await Promise.all([freePort(), freePort()]) const certificateDir = join(temp, 'tls') const processHandle = spawn(process.execPath, [join(projectDir, 'connector', 'spine-connector.mjs')], { cwd: projectDir, env: { ...process.env, SPINE_CONNECTOR_PORT: String(httpPort), SPINE_CONNECTOR_TLS_PORT: String(httpsPort), SPINE_CONNECTOR_CONFIG: join(temp, 'config.json'), SPINE_CONNECTOR_TLS_DIR: certificateDir, }, stdio: ['ignore', 'pipe', 'pipe'], }) let output = '' processHandle.stdout.on('data', (chunk) => { output += chunk }) processHandle.stderr.on('data', (chunk) => { output += chunk }) try { const ca = await readFile(join(certificateDir, 'ca.pem')).catch(() => null) || await new Promise((resolveCa, reject) => { const timer = setInterval(async () => { try { const value = await readFile(join(certificateDir, 'ca.pem')) clearInterval(timer) resolveCa(value) } catch { // The connector creates the certificate asynchronously during startup. } }, 100) setTimeout(() => { clearInterval(timer) reject(new Error(`TLS 证书未生成\n${output}`)) }, 10_000) }) const response = await waitForConnector(`https://127.0.0.1:${httpsPort}/v1/health`, ca) if (!response.body.secureAvailable) throw new Error('健康检查未报告 HTTPS 可用') if (response.headers['access-control-allow-origin'] !== 'https://safari.test') throw new Error('HTTPS CORS 来源不正确') if (!String(response.body.pairUrl).startsWith(`https://127.0.0.1:${httpsPort}/pair`)) throw new Error('授权链接未使用 HTTPS') for (const name of ['server-key.pem', 'identity.p12']) { if (((await stat(join(certificateDir, name))).mode & 0o077) !== 0) throw new Error(`${name} 权限过宽`) } console.log(`连接器 HTTPS 验证通过:https://127.0.0.1:${httpsPort}`) } finally { if (!processHandle.killed) processHandle.kill('SIGTERM') await new Promise((resolveExit) => processHandle.once('exit', resolveExit)).catch(() => {}) await rm(temp, { recursive: true, force: true }) }