Files
blog/deploy/cn-certkeeper/src/rollback-dogecloud.mjs
T

90 lines
4.2 KiB
JavaScript
Raw Normal View History

/**
* 应急回退:把多吉云 CDN 的加速域名绑回**指定的证书 id**。
*
* 什么时候用:换了新证书之后 CDN 侧表现异常(比如最终端不吃 ECDSA),
* 需要立刻把域名绑回上一代证书恢复服务。
*
* 用法:
* docker exec cn-certkeeper node src/rollback-dogecloud.mjs usj.cc 40948
* docker exec cn-certkeeper node src/rollback-dogecloud.mjs usj.cc # 只列候选
*
* 绑定的域名取自**配置里该域名的 `dogecloud_domains`**,所以不会误伤别的域名。
*/
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { FileKV } from './kv-file.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const lib = (n) => require(path.resolve(HERE, '../lib', `${n}.js`));
const argv = process.argv.slice(2);
const NAME = argv.find((a) => !a.startsWith('--'));
const WANT_ID = argv.filter((a) => !a.startsWith('--'))[1];
if (!NAME) {
console.error('用法:node src/rollback-dogecloud.mjs <域名> [证书id]');
process.exit(2);
}
const kv = new FileKV(process.env.DATA_DIR || '/data');
const env = { RSS_KV: kv, TOKEN_SECRET: String(process.env.TOKEN_SECRET || '').trim(), EDITOR_API_BASE: '', EDITOR_TOKEN: '' };
const { loadConfig, getAccess } = lib('certstore');
const { makeDeployer } = lib('deployer');
const cfg = await loadConfig(env);
const d = cfg.domains.find((x) => x.name === NAME);
if (!d) { console.error(`配置里没有域名「${NAME}」`); process.exit(2); }
const domains = (d.dogecloud_domains || []).map((s) => s.trim()).filter(Boolean);
if (!domains.length) { console.error('该域名没配 dogecloud_domains'); process.exit(2); }
const cred = await getAccess(env, 'dogecloud');
const dp = makeDeployer(cred);
// 复用部署器的私有 api:通过 re-deploy 的 ping 拿不到列表,这里直接照签名规则自己调。
// (把 deployer 当黑盒的话拿不到 list,所以就地实现一次只读列举)
const nc = await import('node:crypto');
const enc = new TextEncoder();
async function dapi(path_, body) {
const bodyStr = body === null ? '' : JSON.stringify(body);
const key = await nc.subtle.importKey('raw', enc.encode(cred.secretKey), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const sig = Buffer.from(await nc.subtle.sign('HMAC', key, enc.encode(`${path_}\n${bodyStr}`))).toString('hex');
const r = await fetch('https://api.dogecloud.com' + path_, {
method: 'POST',
headers: { Authorization: `TOKEN ${cred.accessKey}:${sig}`, 'Content-Type': 'application/json', Accept: 'application/json' },
body: bodyStr || undefined,
});
return JSON.parse(await r.text());
}
const cl = await dapi('/cdn/cert/list.json', {});
const certs = cl?.data?.certs || [];
const dl = await dapi('/cdn/domain/list.json', {});
const inUse = new Map((dl?.data?.domains || []).map((x) => [x.name, x.cert_id]));
console.log(`域名 ${NAME};将操作的加速域名:${domains.join(', ')}\n`);
console.log('当前绑定:');
for (const n of domains) console.log(` ${n.padEnd(20)} cert_id=${inUse.get(n)}`);
console.log('\n候选证书(域名集含上述域名的):');
const want = new Set(domains.map((s) => s.toLowerCase()));
for (const c of certs) {
const have = new Set((c.domains || []).map((x) => String(x.name).toLowerCase()));
let covers = true;
for (const w of want) if (!have.has(w)) { covers = false; break; }
if (!covers) continue;
console.log(` #${String(c.id).padEnd(8)} note=${JSON.stringify(c.note)} issue=${c.issueDate} expire=${c.expireText} SAN=${JSON.stringify(c.info?.SAN || [])} algo=${c.info?.encryptAlgorithm || '?'}`);
}
if (!WANT_ID) {
console.log('\n(未指定证书 id,只列举,不做改动)');
console.log('回退用法:node src/rollback-dogecloud.mjs ' + NAME + ' <上面的某个 id>');
process.exit(0);
}
const id = Number(WANT_ID);
if (!certs.some((c) => Number(c.id) === id)) { console.error(`\n★ 证书 #${id} 不在列表里,拒绝执行`); process.exit(1); }
for (const n of domains) {
const r = await dapi('/cdn/cert/bind.json', { id, domain: n });
console.log(` bind ${n} → #${id} code=${r.code} msg=${r.msg || ''}`);
}
console.log('\n回退完成。');