Smail/frontend/dev-tls.js
smail 9dc861d560
Some checks are pending
Security Checks / security (push) Waiting to run
角色权限收紧前保存证书直接引用与续期热切换改造
2026-09-09 17:42:19 +08:00

72 lines
2.7 KiB
JavaScript

import { createHash, createPrivateKey, createPublicKey, X509Certificate } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { createSecureContext } from 'node:tls'
import { readDevelopmentRuntime } from '../deploy/runtime-config.mjs'
// 开发 HTTPS 也读取同一部署配置;整套材料校验后再替换,不关闭现有连接。
export function readDevelopmentTLS(root) {
try {
return readTLSMaterial(readDevelopmentRuntime(root))
} catch (error) {
try {
return readTLSMaterial(readDevelopmentRuntime(root, { previous: true }))
} catch {
throw error
}
}
}
function readTLSMaterial(runtime) {
const cert = readFileSync(runtime.certificatePath)
const key = readFileSync(runtime.privateKeyPath)
const certificate = new X509Certificate(cert)
const now = Date.now()
if (now < Date.parse(certificate.validFrom) || now >= Date.parse(certificate.validTo)) {
throw new Error('开发证书尚未生效或已经过期')
}
const publicKey = createPublicKey(createPrivateKey(key))
if (!certificate.publicKey.equals(publicKey) || !certificate.checkHost(new URL(runtime.origin).hostname)) {
throw new Error('开发证书与私钥或 Web 域名不匹配')
}
createSecureContext({ cert, key, minVersion: 'TLSv1.2' })
const fingerprint = createHash('sha256').update(cert).update(key).digest('hex')
return { runtime, options: { cert, key, minVersion: 'TLSv1.2' }, fingerprint, expiresAt: Date.parse(certificate.validTo) }
}
export function developmentTLSReload(initial, root) {
return {
name: 'smail-development-tls-reload',
apply: 'serve',
configureServer(server) {
let active = initial
let warned = false
let expired = false
const timer = setInterval(() => {
try {
const next = readDevelopmentTLS(root)
if (expired || next.fingerprint !== active.fingerprint) {
server.httpServer.setSecureContext(next.options)
active = next
expired = false
}
warned = false
} catch {
if (!expired && Date.now() >= active.expiresAt) {
// 不再给新握手提供过期证书;已经建立的连接不关闭。
try {
server.httpServer.setSecureContext({ minVersion: 'TLSv1.2' })
expired = true
} catch {
server.config.logger.error('无法停用过期开发证书,请重启前端服务')
}
}
if (!warned) server.config.logger.warn('证书热加载失败,暂时保留上一套有效证书,请检查 yunx-test/ssl')
warned = true
}
}, 1000)
timer.unref()
server.httpServer.once('close', () => clearInterval(timer))
}
}
}