e9c3dc1120
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码 切换前端 active 入口并解除旧创作页面和路由编译链 移除旧后端路由、worker 与纯业务 crate 依赖 收敛 SpacetimeDB 模块为历史数据壳 同步 Nginx、Pingora、验证门禁与架构文档
525 lines
16 KiB
JavaScript
525 lines
16 KiB
JavaScript
#!/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 不允许 rewrite;Nginx 和 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');
|