Files
Genarrative/scripts/check-nginx-pingora-canary.mjs
T
kdletters e9c3dc1120 退役旧创作模板业务
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码
切换前端 active 入口并解除旧创作页面和路由编译链
移除旧后端路由、worker 与纯业务 crate 依赖
收敛 SpacetimeDB 模块为历史数据壳
同步 Nginx、Pingora、验证门禁与架构文档
2026-07-17 22:07:52 +08:00

525 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const PREFIX_SNIPPET_PATH =
'deploy/nginx/snippets/genarrative-pingora-canary.conf';
const REALPATH_SNIPPET_PATH =
'deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf';
const PROBE_TOKEN_PLACEHOLDER = '__GENARRATIVE_PINGORA_PROBE_TOKEN__';
const CANARY_PREFIX = '/__genarrative_pingora_canary';
const REALPATH_HEALTHZ_PATH = '/__genarrative_pingora_realpath_canary/healthz';
const PINGORA_UPSTREAM = 'http://127.0.0.1:18081';
const REALPATH_HANDOFF =
'add_header X-Genarrative-Nginx-Handoff pingora-realpath-canary always;';
const requireNginx = process.argv.includes('--require-nginx');
const explicitNginxBinary =
process.env.GENARRATIVE_CHECK_NGINX_BINARY?.trim() ?? '';
const nginxBinary = explicitNginxBinary || 'nginx';
const failures = [];
let activeSnippetPath = PREFIX_SNIPPET_PATH;
function fail(message) {
failures.push(message);
}
function countOccurrences(content, needle) {
return content.split(needle).length - 1;
}
function requireIncludes(content, needle, reason) {
if (!content.includes(needle)) {
fail(`${activeSnippetPath}: 缺少 ${needle}${reason}`);
}
}
function requireNotMatches(content, pattern, reason) {
if (pattern.test(content)) {
fail(`${activeSnippetPath}: ${reason}`);
}
}
function extractLocationBlocks(content) {
const blocks = new Map();
const locationPattern = /^\s*location\s+([^{]+)\{/gmu;
for (const match of content.matchAll(locationPattern)) {
const openBraceIndex = content.indexOf('{', match.index);
let depth = 0;
for (let index = openBraceIndex; index < content.length; index += 1) {
const char = content[index];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
const header = `location ${match[1].trim()}`;
const block = content.slice(match.index, index + 1);
const body = content.slice(openBraceIndex + 1, index);
blocks.set(header, { body, block });
break;
}
}
}
}
return blocks;
}
function requireLocation(blocks, header) {
const block = blocks.get(header);
if (!block) {
fail(`${activeSnippetPath}: 缺少 ${header} location。`);
}
return block;
}
function requireLocationIncludes(header, block, needle, reason) {
if (!block?.body.includes(needle)) {
fail(`${activeSnippetPath}: ${header} 缺少 ${needle}${reason}`);
}
}
function requireLocalOnlyAccess(header, block) {
for (const directive of ['allow 127.0.0.1;', 'allow ::1;', 'deny all;']) {
requireLocationIncludes(
header,
block,
directive,
'canary 入口默认必须只放行本机,避免误 include 后公开接流。',
);
}
}
function runStaticChecks(content) {
requireIncludes(
content,
PROBE_TOKEN_PLACEHOLDER,
'仓库模板必须保留 probe token 占位符,部署时再替换为非 Git 密钥。',
);
requireIncludes(
content,
`proxy_pass ${PINGORA_UPSTREAM}`,
'canary 必须固定转发到本机 Pingora shadow 端口。',
);
requireIncludes(
content,
'X-Genarrative-Nginx-Handoff',
'canary 响应必须带 handoff 标识,方便验证请求确实经 Nginx 交给 Pingora。',
);
requireNotMatches(
content,
/^\s*(http|server|listen)\b/mu,
'canary snippet 只能包含 location 片段,不能自行声明 http/server/listen。',
);
const locationBlocks = extractLocationBlocks(content);
if (locationBlocks.size !== 3) {
fail(
`${activeSnippetPath}: 预期只有 3 个 location,实际 ${locationBlocks.size} 个。`,
);
}
const healthzHeader = `location = ${CANARY_PREFIX}/healthz`;
const websocketHeader = `location ~ ^${CANARY_PREFIX}/v1/database/[^/]+/subscribe$`;
const genericHeader = `location ${CANARY_PREFIX}/`;
const healthz = requireLocation(locationBlocks, healthzHeader);
const websocket = requireLocation(locationBlocks, websocketHeader);
const generic = requireLocation(locationBlocks, genericHeader);
for (const [header, block] of [
[healthzHeader, healthz],
[websocketHeader, websocket],
[genericHeader, generic],
]) {
requireLocalOnlyAccess(header, block);
requireLocationIncludes(
header,
block,
'add_header X-Genarrative-Nginx-Handoff pingora-canary always;',
'每条 canary 路由都必须可通过响应头审计 handoff。',
);
requireLocationIncludes(
header,
block,
'proxy_set_header X-Request-Id $request_id;',
'canary 访问需要沿用 Nginx request_id,便于和 Pingora access log 对照。',
);
}
requireLocationIncludes(
healthzHeader,
healthz,
`proxy_set_header X-Genarrative-Pingora-Probe "${PROBE_TOKEN_PLACEHOLDER}";`,
'healthz handoff 必须带 shadow probe token。',
);
requireLocationIncludes(
healthzHeader,
healthz,
`proxy_pass ${PINGORA_UPSTREAM}/__genarrative_pingora/healthz;`,
'healthz canary 必须转到 Pingora 内部探针路径。',
);
for (const [header, block] of [
[websocketHeader, websocket],
[genericHeader, generic],
]) {
requireLocationIncludes(
header,
block,
`rewrite ^${CANARY_PREFIX}(/.*)$ $1 break;`,
'canary 前缀必须在转发给 Pingora 前剥离。',
);
requireLocationIncludes(
header,
block,
`proxy_pass ${PINGORA_UPSTREAM};`,
'剥离前缀后的请求必须交给 Pingora shadow。',
);
}
requireLocationIncludes(
websocketHeader,
websocket,
'proxy_set_header Upgrade $http_upgrade;',
'SpacetimeDB subscribe 必须保留 WebSocket Upgrade。',
);
requireLocationIncludes(
websocketHeader,
websocket,
'proxy_set_header Connection "Upgrade";',
'SpacetimeDB subscribe 必须保留 WebSocket Upgrade 连接语义。',
);
requireLocationIncludes(
websocketHeader,
websocket,
'proxy_read_timeout 3600s;',
'WebSocket canary 不能被短读超时提前断开。',
);
requireLocationIncludes(
genericHeader,
generic,
'proxy_buffering off;',
'API / SSE canary 必须保持低缓冲口径。',
);
requireLocationIncludes(
genericHeader,
generic,
'add_header X-Accel-Buffering no always;',
'API / SSE canary 必须显式关闭加速缓冲。',
);
requireLocationIncludes(
genericHeader,
generic,
'proxy_set_header X-Genarrative-Pingora-Canary "nginx-prefixed";',
'Pingora access log 需要能识别来自 Nginx 前缀 canary 的请求。',
);
if (
countOccurrences(
content,
'add_header X-Genarrative-Nginx-Handoff pingora-canary always;',
) !== 3
) {
fail(`${activeSnippetPath}: handoff 响应头必须在 3 个 location 中各出现一次。`);
}
if (countOccurrences(content, 'deny all;') !== 3) {
fail(`${activeSnippetPath}: deny all 必须在 3 个 location 中各出现一次。`);
}
if (countOccurrences(content, 'X-Genarrative-Pingora-Probe') !== 1) {
fail(`${activeSnippetPath}: probe token 只能出现在 healthz canary 路由。`);
}
}
function runRealpathStaticChecks(content) {
requireIncludes(
content,
PROBE_TOKEN_PLACEHOLDER,
'仓库模板必须保留 probe token 占位符,部署时再替换为非 Git 密钥。',
);
requireIncludes(
content,
'server {',
'真实路径 canary 必须是独立 server 片段,避免 include 到生产 443 server 后覆盖正式 location。',
);
requireIncludes(
content,
'listen 127.0.0.1:18083;',
'真实路径 canary 只能默认监听本机 loopback 端口。',
);
requireIncludes(
content,
'access_log /var/log/nginx/genarrative-pingora-realpath-canary.access.log genarrative_upstream;',
'真实路径 canary 必须写独立 access log,避免和生产真实用户请求混淆。',
);
requireIncludes(
content,
`proxy_pass ${PINGORA_UPSTREAM}`,
'真实路径 canary 必须固定转发到本机 Pingora shadow 端口。',
);
requireNotMatches(
content,
/^\s*http\b/mu,
'真实路径 canary snippet 只能包含 server 片段,不能自行声明 http。',
);
requireNotMatches(
content,
/^\s*rewrite\b/mu,
'真实路径 canary 不允许 rewriteNginx 和 Pingora access log path 必须一一对齐。',
);
requireNotMatches(
content,
/^\s*listen\s+(?:80|443|0\.0\.0\.0|\[::\])/mu,
'真实路径 canary 默认不能监听公网地址或 80/443。',
);
const serverCount = [...content.matchAll(/^\s*server\s*\{/gmu)].length;
if (serverCount !== 1) {
fail(`${activeSnippetPath}: 预期只有 1 个 server,实际 ${serverCount} 个。`);
}
for (const directive of ['allow 127.0.0.1;', 'allow ::1;', 'deny all;']) {
requireIncludes(
content,
directive,
'真实路径 canary 默认必须只放行本机,避免误 include 后公开接流。',
);
}
const locationBlocks = extractLocationBlocks(content);
if (locationBlocks.size !== 7) {
fail(
`${activeSnippetPath}: 真实路径 canary 预期只有 7 个 location,实际 ${locationBlocks.size} 个。`,
);
}
const healthzHeader = `location = ${REALPATH_HEALTHZ_PATH}`;
const apiHeader = 'location = /api/assets/history';
const websocketHeader = 'location ~ ^/v1/database/[^/]+/subscribe$';
const identityHeader = 'location ^~ /v1/identity';
const assetHeader = 'location = /assets/app.js';
const generatedHeader = 'location = /generated-pingora-canary-smoke';
const fallbackHeader = 'location /';
const healthz = requireLocation(locationBlocks, healthzHeader);
const api = requireLocation(locationBlocks, apiHeader);
const websocket = requireLocation(locationBlocks, websocketHeader);
const identity = requireLocation(locationBlocks, identityHeader);
const asset = requireLocation(locationBlocks, assetHeader);
const generated = requireLocation(locationBlocks, generatedHeader);
const fallback = requireLocation(locationBlocks, fallbackHeader);
for (const [header, block] of [
[healthzHeader, healthz],
[apiHeader, api],
[websocketHeader, websocket],
[identityHeader, identity],
[assetHeader, asset],
[generatedHeader, generated],
]) {
requireLocationIncludes(
header,
block,
REALPATH_HANDOFF,
'每条真实路径 canary 路由都必须可通过响应头审计 handoff。',
);
requireLocationIncludes(
header,
block,
'proxy_set_header X-Request-Id $request_id;',
'真实路径 canary 访问需要沿用 Nginx request_id,便于和 Pingora access log 对照。',
);
}
requireLocationIncludes(
healthzHeader,
healthz,
`proxy_set_header X-Genarrative-Pingora-Probe "${PROBE_TOKEN_PLACEHOLDER}";`,
'healthz handoff 必须带 shadow probe token。',
);
requireLocationIncludes(
healthzHeader,
healthz,
`proxy_pass ${PINGORA_UPSTREAM}/__genarrative_pingora/healthz;`,
'真实路径 canary healthz 必须转到 Pingora 内部探针路径。',
);
requireLocationIncludes(
apiHeader,
api,
'proxy_buffering off;',
'代表性 API 真实路径 canary 必须保持低缓冲口径。',
);
requireLocationIncludes(
apiHeader,
api,
'add_header X-Accel-Buffering no always;',
'代表性 API 真实路径 canary 必须显式关闭加速缓冲。',
);
requireLocationIncludes(
apiHeader,
api,
'proxy_set_header X-Genarrative-Pingora-Canary "nginx-realpath";',
'Pingora access log 需要能识别来自 Nginx 真实路径 canary 的请求。',
);
for (const [header, block] of [
[websocketHeader, websocket],
[identityHeader, identity],
]) {
requireLocationIncludes(
header,
block,
'proxy_set_header Upgrade $http_upgrade;',
'SpacetimeDB 真实路径 canary 必须保留 Upgrade 设置。',
);
requireLocationIncludes(
header,
block,
'proxy_set_header Connection "Upgrade";',
'SpacetimeDB 真实路径 canary 必须保留 Upgrade 连接语义。',
);
}
requireLocationIncludes(
websocketHeader,
websocket,
'proxy_read_timeout 3600s;',
'WebSocket 真实路径 canary 不能被短读超时提前断开。',
);
requireLocationIncludes(
fallbackHeader,
fallback,
'return 404;',
'真实路径 canary 只能覆盖代表路径,其余路径必须拒绝。',
);
if (countOccurrences(content, REALPATH_HANDOFF) !== 6) {
fail(
`${activeSnippetPath}: pingora-realpath-canary handoff 响应头必须在 6 个代理 location 中各出现一次。`,
);
}
if (countOccurrences(content, 'X-Genarrative-Pingora-Probe') !== 1) {
fail(`${activeSnippetPath}: probe token 只能出现在真实路径 healthz canary 路由。`);
}
}
function hasNginxBinary() {
try {
execFileSync(nginxBinary, ['-v'], { encoding: 'utf8', stdio: 'pipe' });
return true;
} catch (error) {
if (explicitNginxBinary || requireNginx) {
fail(
`[check:nginx-pingora-canary] 无法执行 ${nginxBinary}: ${error.message}`,
);
}
return false;
}
}
function runNginxSyntaxCheck(prefixContent, realpathContent) {
if (!hasNginxBinary()) {
console.log(
'[check:nginx-pingora-canary] 未找到 nginx,已完成静态检查;需要强制 nginx -t 时使用 --require-nginx。',
);
return;
}
const tempRoot = mkdtempSync(
path.join(tmpdir(), 'genarrative-nginx-canary-'),
);
try {
const renderedPrefixSnippetPath = path.join(
tempRoot,
'genarrative-pingora-canary.conf',
);
const renderedRealpathSnippetPath = path.join(
tempRoot,
'genarrative-pingora-realpath-canary.conf',
);
const nginxConfigPath = path.join(tempRoot, 'nginx.conf');
const renderedPrefixSnippet = prefixContent.replaceAll(
PROBE_TOKEN_PLACEHOLDER,
'local-nginx-canary-check-token',
);
const renderedRealpathSnippet = realpathContent
.replaceAll(PROBE_TOKEN_PLACEHOLDER, 'local-nginx-canary-check-token')
.replace(
'/var/log/nginx/genarrative-pingora-realpath-canary.access.log',
path.join(tempRoot, 'genarrative-pingora-realpath-canary.access.log'),
)
.replace(
'/var/log/nginx/genarrative-pingora-realpath-canary.error.log',
path.join(tempRoot, 'genarrative-pingora-realpath-canary.error.log'),
);
const nginxConfig = `
pid ${tempRoot}/nginx.pid;
error_log stderr notice;
events {
worker_connections 64;
}
http {
log_format genarrative_upstream
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'request_time=$request_time upstream_connect_time=$upstream_connect_time '
'upstream_header_time=$upstream_header_time upstream_response_time=$upstream_response_time '
'upstream_status=$upstream_status request_id=$request_id';
access_log ${tempRoot}/access.log genarrative_upstream;
server {
listen 127.0.0.1:18082;
server_name genarrative-pingora-canary-check.local;
include ${renderedPrefixSnippetPath};
}
include ${renderedRealpathSnippetPath};
}
`;
writeFileSync(renderedPrefixSnippetPath, renderedPrefixSnippet, 'utf8');
writeFileSync(renderedRealpathSnippetPath, renderedRealpathSnippet, 'utf8');
writeFileSync(nginxConfigPath, nginxConfig, 'utf8');
execFileSync(
nginxBinary,
['-t', '-q', '-p', `${tempRoot}/`, '-c', nginxConfigPath],
{ encoding: 'utf8', stdio: 'pipe' },
);
console.log('[check:nginx-pingora-canary] nginx -t OK');
} catch (error) {
const stderr = error.stderr ? `\n${error.stderr}` : '';
const stdout = error.stdout ? `\n${error.stdout}` : '';
fail(`[check:nginx-pingora-canary] nginx -t 失败。${stderr}${stdout}`);
} finally {
rmSync(tempRoot, { force: true, recursive: true });
}
}
const prefixSnippet = readFileSync(PREFIX_SNIPPET_PATH, 'utf8');
activeSnippetPath = PREFIX_SNIPPET_PATH;
runStaticChecks(prefixSnippet);
const realpathSnippet = readFileSync(REALPATH_SNIPPET_PATH, 'utf8');
activeSnippetPath = REALPATH_SNIPPET_PATH;
runRealpathStaticChecks(realpathSnippet);
if (failures.length === 0) {
runNginxSyntaxCheck(prefixSnippet, realpathSnippet);
}
if (failures.length > 0) {
console.error('[check:nginx-pingora-canary] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[check:nginx-pingora-canary] OK');