108 lines
3.9 KiB
JavaScript
108 lines
3.9 KiB
JavaScript
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 })
|
|
}
|