feat(ssl): ACME 自动签发与自动续期,实现证书全生命周期闭环
证书管家此前只做「探针」(查剩余天数),现补齐签发+部署两个环节,
参照 certimate(MIT)的 DNS-01 流程自行实现,不再依赖闭源 certd。
新增(纯 WebCrypto,零 npm 依赖):
- lib/acme.ts ACME v2 客户端:ES256 JWS(原始 r||s)、RFC7638
thumbprint、EAB、badNonce 重试、DNS-01、手写 DER CSR
- lib/dnsprovider.ts DNS-01 适配:DNSPod(TC3-HMAC-SHA256)、Cloudflare
- lib/deployer.ts 部署适配:多吉云 CDN、1Panel 站点(幂等换证书)
- lib/certissue.ts 编排:探针判剩余天数 → 注册/复用账户 → 签发 → 落库
→ 逐目标部署;RENEW_BEFORE_DAYS=30
- routes/ssl.ts 新增 POST /ssl/issue、GET /ssl/renew-check、
POST /ssl/selfcheck(环境自检,只读不签发)
- index.ts + cron 每日 04:10 自动续期检查;cpu_ms 提到 60s
与 certimate 的差异:certimate 每个 workflow 每天无条件重跑,
这里改为先探针查剩余天数、低于阈值才签,省 CA 限速额度。
实测修正(易误判,勿回退):
- 多吉云 bind 参数是 {id, domain},非 {cert_id}(用假 id 对照实验确认:
cert_id 回「域名不存在」= 参数被无视)
- 多吉云上传私钥字段是 private;列域名用 /cdn/domain/list.json
- 1Panel 必须用 /api/v2/(v1 返回 HTTP 200 但正文是 HTML 停用页)
- 1Panel HTTPS 配置字段是 SSL(大写),写错会导致每次续期都重绑
- LiteSSL ACME 目录须带 /v2:acme.trustasia.com/acme/v2/directory
测试:selftest-acme 16/16(CSR 过 openssl 验签、JWS 过 Node crypto 验签)、
selftest-deploy 18/18、selftest:ssl 117/117、UI 全过、tsc 干净
This commit is contained in:
1 parent
432cf5e398
commit
43dbc8b75f
14 files changed
+3010
-6
No files matched your search
@@ -56,6 +56,27 @@ const MOCK = {
|
||||
notAfter: Date.now() + 62 * DAY, notBefore: Date.now() - 28 * DAY,
|
||||
daysLeft: 62, notAfterText: '2026-12-07 06:59:59',
|
||||
},
|
||||
// 续期判定(只读)
|
||||
'/api/v2/ssl/renew-check': {
|
||||
threshold: 30,
|
||||
items: [
|
||||
{ domain: 'usj.cc', host: 'usj.cc', source: 'live', daysLeft: 62, action: 'ok', threshold: 30, issuer: 'TrustAsia Technologies, Inc.', error: null },
|
||||
{ domain: 't-t.live', host: 't-t.live', source: 'live', daysLeft: 83, action: 'ok', threshold: 30, issuer: "Let's Encrypt", error: null },
|
||||
{ domain: '200181.xyz', host: '200181.xyz', source: 'live', daysLeft: 19, action: 'renew', threshold: 30, issuer: 'LiteSSL', error: null },
|
||||
],
|
||||
},
|
||||
// 环境自检(只读)
|
||||
'/api/v2/ssl/selfcheck': {
|
||||
threshold: 30,
|
||||
summary: { total: 5, failed: 0 },
|
||||
items: [
|
||||
{ name: 'CA litessl', kind: 'ca', ok: true, detail: '目录可达,EAB 已配对' },
|
||||
{ name: 'DNS tencent-usj', kind: 'dns', ok: true, detail: 'tencentcloud 凭据可用,能读取 usj.cc 的 TXT(现存 0 条)' },
|
||||
{ name: 'DNS cloudflare', kind: 'dns', ok: true, detail: 'cloudflare 凭据可用,能读取 200181.xyz 的 TXT(现存 0 条)' },
|
||||
{ name: '部署 dogecloud', kind: 'deploy', ok: true, detail: '多吉云凭据可用' },
|
||||
{ name: '部署 1panel', kind: 'deploy', ok: true, detail: '1Panel 凭据可用,站点 usj.cc 已找到' },
|
||||
],
|
||||
},
|
||||
'/api/v2/ssl/access': {
|
||||
items: [
|
||||
{ name: 'tencent-usj', type: 'tencentcloud', note: '小赵腾讯云', fields: { secretId: 'AKID********3f2a', secretKey: 'Qk9Y****gAAA' } },
|
||||
@@ -229,6 +250,23 @@ try {
|
||||
t('「实测」按钮存在', view.includes('ssl-probe'));
|
||||
t('「登记」按钮存在', view.includes('ssl-import'));
|
||||
t('「+ 域名」按钮存在', view.includes('ssl-add-domain'));
|
||||
// 本轮新增的三个动作
|
||||
t('★ 「环境自检」按钮存在', view.includes('ssl-selfcheck'));
|
||||
t('★ 「该续期了吗」按钮存在', view.includes('ssl-renew-check'));
|
||||
t('★ 每行有「签发」按钮', view.includes('data-act="ssl-issue"'));
|
||||
|
||||
// ---------- 环境自检弹窗(本轮新增)
|
||||
{
|
||||
const before = String(await ev(`document.querySelector('#modal')?.innerHTML || ''`));
|
||||
await ev(`(() => { const b = document.querySelector('button[data-act="ssl-selfcheck"]'); if (b) b.click(); })()`);
|
||||
await sleep(1500);
|
||||
const m = String(await ev(`document.querySelector('#modal')?.innerHTML || ''`));
|
||||
t('★ 自检弹窗打开', m !== before && m.includes('环境自检'), m.slice(0, 80));
|
||||
t('★ 自检列出各项', m.includes('CA') && m.includes('DNS') && m.includes('部署'));
|
||||
t('★ 自检说明「不会签发证书」', m.includes('不会签发证书'));
|
||||
await ev(`(() => { const b = document.querySelector('#modal button[data-act="modal-close"]'); if (b) b.click(); })()`);
|
||||
await sleep(400);
|
||||
}
|
||||
|
||||
// ---------- 凭据区
|
||||
await sleep(600);
|
||||
@@ -302,8 +340,8 @@ try {
|
||||
await sleep(3000);
|
||||
await ev(`(() => { const b = [...document.querySelectorAll('#tabs .tab')].find(x => x.textContent.includes('证书管家')); if (b) b.click(); })()`);
|
||||
await sleep(2000);
|
||||
// 点一次「实测」——本轮改的就是探测链路,把结果弹窗截进画面才看得见。
|
||||
await ev(`(() => { const b = document.querySelector('button[data-act="ssl-probe"]'); if (b) b.click(); })()`);
|
||||
// 点一次「环境自检」——本轮新增的核心能力,把结果弹窗截进画面。
|
||||
await ev(`(() => { const b = document.querySelector('button[data-act="ssl-selfcheck"]'); if (b) b.click(); })()`);
|
||||
await sleep(2000);
|
||||
await ev(`document.documentElement.setAttribute('data-theme','light')`);
|
||||
await sleep(500);
|
||||
|
||||
Reference in new issue
Block a user