Files
Genarrative/scripts/check-pingora-tls-cert-sync.mjs
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

299 lines
8.8 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const SCRIPT = 'scripts/deploy/pingora-tls-cert-sync.mjs';
const failures = [];
const tmpRoot = mkdtempSync(
path.join(tmpdir(), 'genarrative-pingora-tls-cert-sync-'),
);
try {
main();
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
if (failures.length > 0) {
console.error('[check:pingora-tls-cert-sync] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[check:pingora-tls-cert-sync] OK');
function main() {
assertDryRunAcceptsCertbotLiveSymlinks();
assertApplyCopiesToPrivateTarget();
assertRejectsRelativeAndRootPaths();
assertRejectsControlCharactersBeforeApply();
assertRejectsSymlinkTargetDirectory();
assertRejectsSymlinkTargetFiles();
assertRejectsWorldReadableModes();
}
function assertDryRunAcceptsCertbotLiveSymlinks() {
const fixture = prepareFixture('dry-run-live-symlink');
const result = runSync(fixture, []);
assertStatus(result, 0, 'dry-run 应接受 Certbot live symlink 源。');
if (existsSync(fixture.targetCert) || existsSync(fixture.targetKey)) {
failures.push('dry-run 不应写入目标证书文件。');
}
const payload = parseJson(result.stdout, 'dry-run JSON 输出');
assertEqual(payload.mode, 'dry-run', 'dry-run JSON mode 应为 dry-run。');
assertEqual(
payload.files?.[0]?.source?.sourceIsSymlink,
true,
'dry-run JSON 应标记源证书来自 symlink。',
);
}
function assertApplyCopiesToPrivateTarget() {
const fixture = prepareFixture('apply-copy', { symlinkSources: false });
const result = runSync(fixture, ['--apply'], {
serviceUser: `#${process.getuid?.() ?? 0}`,
serviceGroup: `#${process.getgid?.() ?? 0}`,
targetUser: `#${process.getuid?.() ?? 0}`,
});
assertStatus(result, 0, 'apply 应复制证书到目标目录。');
assertFileContent(fixture.targetCert, 'fixture fullchain\n');
assertFileContent(fixture.targetKey, 'fixture private key\n');
assertMode(fixture.targetDir, 0o750, '目标目录权限应为 0750。');
assertMode(fixture.targetCert, 0o640, '目标证书权限应为 0640。');
assertMode(fixture.targetKey, 0o640, '目标私钥权限应为 0640。');
}
function assertRejectsRelativeAndRootPaths() {
const fixture = prepareFixture('reject-paths');
const relative = runRaw([
'--source-cert-file',
'relative/fullchain.pem',
'--source-key-file',
fixture.sourceKey,
'--target-dir',
fixture.targetDir,
]);
assertNonZero(relative, '相对源证书路径必须失败。');
assertIncludes(
`${relative.stdout}\n${relative.stderr}`,
'--source-cert-file 必须是绝对路径',
'相对路径必须给出明确错误。',
);
const root = runRaw([
'--source-cert-file',
fixture.sourceCert,
'--source-key-file',
fixture.sourceKey,
'--target-dir',
'/',
]);
assertNonZero(root, 'target-dir 指向文件系统根目录必须失败。');
assertIncludes(
`${root.stdout}\n${root.stderr}`,
'--target-dir 不能是文件系统根目录',
'根目录目标必须给出明确错误。',
);
}
function assertRejectsControlCharactersBeforeApply() {
const fixture = prepareFixture('reject-control');
const result = runRaw([
'--source-cert-file',
`${fixture.sourceCert}\n--apply`,
'--source-key-file',
fixture.sourceKey,
'--target-dir',
fixture.targetDir,
]);
assertNonZero(result, '带换行的源证书路径必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--source-cert-file 不能包含换行或 NUL 字符',
'控制字符路径必须给出明确错误。',
);
if (existsSync(fixture.targetCert) || existsSync(fixture.targetKey)) {
failures.push('控制字符参数失败时不应写入目标证书。');
}
}
function assertRejectsSymlinkTargetDirectory() {
const fixture = prepareFixture('reject-target-dir-symlink');
const realTarget = path.join(fixture.root, 'real-target');
mkdirSync(realTarget, { recursive: true });
rmSync(fixture.targetDir, { recursive: true, force: true });
symlinkSync(realTarget, fixture.targetDir);
const result = runSync(fixture, []);
assertNonZero(result, 'target-dir 是 symlink 时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--target-dir 已存在路径不能包含符号链接',
'target-dir symlink 必须给出明确错误。',
);
}
function assertRejectsSymlinkTargetFiles() {
const fixture = prepareFixture('reject-target-file-symlink');
mkdirSync(fixture.targetDir, { recursive: true });
const outside = path.join(fixture.root, 'outside-fullchain.pem');
writeFileSync(outside, 'outside\n', 'utf8');
symlinkSync(outside, fixture.targetCert);
const result = runSync(fixture, []);
assertNonZero(result, '目标证书是 symlink 时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'目标 fullchain.pem 不能是符号链接',
'目标文件 symlink 必须给出明确错误。',
);
}
function assertRejectsWorldReadableModes() {
const fixture = prepareFixture('reject-world-readable-mode');
const result = runSync(fixture, ['--file-mode', '0644']);
assertNonZero(result, 'world-readable file mode 必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--file-mode 不能允许 other 读取证书或私钥',
'world-readable file mode 必须给出明确错误。',
);
}
function prepareFixture(name, options = {}) {
const root = path.join(tmpRoot, name);
const archiveDir = path.join(root, 'archive');
const liveDir = path.join(root, 'live');
const targetDir = path.join(root, 'target');
mkdirSync(archiveDir, { recursive: true });
mkdirSync(liveDir, { recursive: true });
writeFileSync(
path.join(archiveDir, 'fullchain1.pem'),
'fixture fullchain\n',
'utf8',
);
writeFileSync(
path.join(archiveDir, 'privkey1.pem'),
'fixture private key\n',
'utf8',
);
const symlinkSources = options.symlinkSources !== false;
const sourceCert = symlinkSources
? path.join(liveDir, 'fullchain.pem')
: path.join(archiveDir, 'fullchain1.pem');
const sourceKey = symlinkSources
? path.join(liveDir, 'privkey.pem')
: path.join(archiveDir, 'privkey1.pem');
if (symlinkSources) {
symlinkSync('../archive/fullchain1.pem', sourceCert);
symlinkSync('../archive/privkey1.pem', sourceKey);
}
return {
root,
sourceCert,
sourceKey,
targetDir,
targetCert: path.join(targetDir, 'fullchain.pem'),
targetKey: path.join(targetDir, 'privkey.pem'),
};
}
function runSync(fixture, args = [], options = {}) {
return runRaw([
'--source-cert-file',
fixture.sourceCert,
'--source-key-file',
fixture.sourceKey,
'--target-dir',
fixture.targetDir,
'--service-user',
options.serviceUser || `#${process.getuid?.() ?? 0}`,
'--service-group',
options.serviceGroup || `#${process.getgid?.() ?? 0}`,
'--target-user',
options.targetUser || `#${process.getuid?.() ?? 0}`,
'--json',
...args,
]);
}
function runRaw(args) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: process.cwd(),
encoding: 'utf8',
});
}
function parseJson(text, label) {
try {
return JSON.parse(text);
} catch (error) {
failures.push(`${label} 不是合法 JSON: ${error.message}\n${text}`);
return {};
}
}
function assertStatus(result, expected, reason) {
if ((result.status ?? 0) !== expected) {
failures.push(
`${reason} 实际退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
}
function assertNonZero(result, reason) {
if ((result.status ?? 0) === 0) {
failures.push(
`${reason}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
}
function assertIncludes(value, expected, reason) {
if (!String(value).includes(expected)) {
failures.push(`${reason} 缺少: ${expected}`);
}
}
function assertEqual(actual, expected, reason) {
if (actual !== expected) {
failures.push(
`${reason} 实际 ${JSON.stringify(actual)},预期 ${JSON.stringify(expected)}`,
);
}
}
function assertFileContent(filePath, expected) {
if (!existsSync(filePath)) {
failures.push(`缺少文件: ${filePath}`);
return;
}
const actual = readFileSync(filePath, 'utf8');
if (actual !== expected) {
failures.push(`${filePath} 内容不符合预期。`);
}
}
function assertMode(filePath, expected, reason) {
const mode = statSync(filePath).mode & 0o777;
if (mode !== expected) {
failures.push(
`${reason} 实际 0${mode.toString(8)},预期 0${expected.toString(8)}。`,
);
}
}