/** * Cloudflare Worker 边缘探针验证(离线,不需要 wrangler) * * 做法:把 worker.js 与 uptimerobot.js 以 data: URL 动态载入, * 注入假的 ASSETS / MONITOR_KV / UptimeRobot 服务器,实测: * [A] UptimeRobot 映射(暂停/离线/可用率/端口目标) * [B] 探测范围(probeScope)与展示口径解耦 * [C] UR 同步失败时保留上次数据,绝不误判为离线 * [D] 管理端路由(/admin 与 /api/admin/* 一律拒绝,Worker 无可写面) * * 用法:node scripts/verify-worker.cjs */ const fs = require('node:fs'); const http = require('node:http'); const path = require('node:path'); const ROOT = path.join(__dirname, '..'); const EDGE = path.join(ROOT, 'edge', 'cloudflare'); const UR_PORT = 8896; const HTTP_PORT = 8895; const results = []; const check = (name, pass, detail) => { results.push({ name, pass }); console.log(` ${pass ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`); }; const group = (t) => console.log(`\n${t}`); const UR_PAYLOAD = { stat: 'ok', monitors: [ { id: 700001, friendly_name: 'UR-主站', url: 'https://a.example.com', type: 1, status: 2, interval: 300, average_response_time: '120', response_times: [{ datetime: Math.floor(Date.now() / 1000) - 60, value: '118' }, { datetime: Math.floor(Date.now() / 1000) - 360, value: '130' }], custom_uptime_ratio: '100-100-99.980-99.990', custom_down_durations: '0-0-120-260', logs: [{ type: 2, datetime: Math.floor(Date.now() / 1000) - 86400 }], }, { id: 700002, friendly_name: 'UR-端口', url: 'b.example.com', port: 8443, type: 4, status: 9, interval: 300, average_response_time: '0', response_times: [{ datetime: Math.floor(Date.now() / 1000) - 120, value: '0' }], custom_uptime_ratio: '98-99-99.5-99.7', custom_down_durations: '300-600-1200-2400', logs: [{ type: 1, datetime: Math.floor(Date.now() / 1000) - 120, reason: { code: '503', detail: 'Service Unavailable' } }], }, { id: 700003, friendly_name: 'UR-暂停', url: 'https://c.example.com', type: 1, status: 0, interval: 300, response_times: [], custom_uptime_ratio: '100-100-100-100', custom_down_durations: '0-0-0-0', logs: [], }, ], }; // 假 UptimeRobot:正常返回 payload;切到 error 模式返回 stat=fail let urMode = 'ok'; function startUrServer() { return http.createServer((req, res) => { let body = ''; req.on('data', (c) => { body += c; }); req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); if (urMode === 'fail') return res.end(JSON.stringify({ stat: 'fail', error: { type: 'invalid_parameter', message: 'bad key' } })); res.end(JSON.stringify(UR_PAYLOAD)); }); }); } // 载入 Worker:替换 cloudflare:sockets 与 UR 模块的 API 地址 async function loadWorker() { let ur = fs.readFileSync(path.join(EDGE, 'uptimerobot.js'), 'utf8'); ur = ur.replace("const API_URL = 'https://api.uptimerobot.com/v2/getMonitors';", `const API_URL = 'http://localhost:${UR_PORT}/v2/getMonitors';`); const urUrl = `data:text/javascript;base64,${Buffer.from(ur).toString('base64')}`; let w = fs.readFileSync(path.join(EDGE, 'worker.js'), 'utf8'); // Workers 专有模块在 Node 下无法加载,替换为抛错桩(测试只覆盖 http 与 UR 路径) w = w.replace("import { connect } from 'cloudflare:sockets';", "const connect = () => { throw new Error('TCP unavailable in offline test'); };"); w = w.replace("import { fetchUptimeRobotMonitors } from './uptimerobot.js';", `import { fetchUptimeRobotMonitors } from '${urUrl}';`); return import(`data:text/javascript;base64,${Buffer.from(w).toString('base64')}`); } // 内存版 KV + 静态资源夹具 function makeEnv(settings, monitors) { const kv = new Map(); const config = JSON.stringify({ settings, monitors }); return { MONITOR_KV: { // 与真实 KV 一致:type='json' 时自动反序列化,否则返回原始字符串 async get(key, type) { const v = kv.get(key); if (v == null) return null; return type === 'json' ? JSON.parse(v) : v; }, async put(k, v) { kv.set(k, v); }, }, ASSETS: { // 与真实 Workers 一致:这里收到的是 URL 对象而非 Request async fetch(input) { const raw = String(input && typeof input.href === 'string' ? input.href : input); const pathname = new URL(raw, 'http://placeholder').pathname; if (pathname === '/monitors.json') { return new Response(config, { headers: { 'content-type': 'application/json' } }); } return new Response('not found', { status: 404 }); }, }, _kv: kv, }; } const LOCAL_TARGETS = [ { id: 'local-1', name: '本地A', type: 'http', url: `http://127.0.0.1:${HTTP_PORT}/ok`, interval: 60 }, { id: 'local-2', name: '本地B', type: 'http', url: `http://127.0.0.1:${HTTP_PORT}/ok`, interval: 60 }, ]; // 记录哪些 URL 被真正探测过(用于验证探测范围) let probed = []; const origFetch = globalThis.fetch; async function main() { const urSrv = startUrServer(); await new Promise((r) => urSrv.listen(UR_PORT, r)); const httpSrv = http.createServer((req, res) => { res.writeHead(200); res.end('ok'); }); await new Promise((r) => httpSrv.listen(HTTP_PORT, r)); const worker = await loadWorker(); const { fetchUptimeRobotMonitors } = await import( `data:text/javascript;base64,${Buffer.from( fs.readFileSync(path.join(EDGE, 'uptimerobot.js'), 'utf8') .replace("const API_URL = 'https://api.uptimerobot.com/v2/getMonitors';", `const API_URL = 'http://localhost:${UR_PORT}/v2/getMonitors';`) ).toString('base64')}` ); /* ---------- [A] UptimeRobot 映射 ---------- */ group('[A] UptimeRobot 映射'); const list = await fetchUptimeRobotMonitors({ apiKey: 'ur-test-key' }); check('映射出 3 个目标', list.length === 3, list.map((m) => m.name).join('、')); check('在线目标延迟取自最新响应样本', list[0].latency === 118, `${list[0].latency}ms`); check('离线目标取故障日志 code 作状态码', list[1].code === 503, `code=${list[1].code}`); check('离线目标详情带原因', /503 Service Unavailable/.test(list[1].detail), list[1].detail); check('暂停目标归为 pending 而非离线', list[2].status === 'pending', list[2].status); check('暂停目标提示已暂停', /暂停/.test(list[2].detail), list[2].detail); check('90 天可用率按 ratios 顺序解析', list[0].uptimeRatios[90] === 99.99, `${list[0].uptimeRatios[1]}/${list[0].uptimeRatios[7]}/${list[0].uptimeRatios[30]}/${list[0].uptimeRatios[90]}`); check('累计离线时长解析正确', list[1].downSeconds[90] === 2400, `${list[1].downSeconds[90]}s`); check('端口目标地址带端口号', list[1].url === 'b.example.com:8443', list[1].url); check('历史心率按时间升序(供心率图直接绘制)', list[0].history[0].t < list[0].history[1].t, ''); check('类型标注为来源协议', list[1].sourceLabel === 'PORT', list[1].sourceLabel); check('目标 id 加 ur- 前缀(避免与自建 id 冲突)', list.every((m) => m.id.startsWith('ur-')), list.map((m) => m.id).join(',')); urMode = 'fail'; let threw = null; try { await fetchUptimeRobotMonitors({ apiKey: 'bad' }); } catch (e) { threw = e; } check('UR 返回 stat=fail 时抛出可读错误', Boolean(threw) && /UptimeRobot/.test(threw.message), threw?.message); urMode = 'ok'; /* ---------- [B] 探测范围与展示口径解耦 ---------- */ group('[B] 探测范围(probeScope)与展示口径(dataFilter)解耦'); const urSettings = (extra = {}) => ({ dataFilter: 'uptimerobot', uptimeRobot: { enabled: true, apiKey: 'ur-k' }, ...extra }); // 记录探测行为 globalThis.fetch = async (input, init) => { const u = String(input); if (u.startsWith(`http://localhost:${UR_PORT}`)) return origFetch(input, init); probed.push(u); return origFetch(input, init); }; probed = []; let env = makeEnv(urSettings(), LOCAL_TARGETS); let snap = await worker.__test.runChecks(env); const urIds = (s) => Object.keys(s.monitors).filter((k) => k.startsWith('ur-')); check('未设 probeScope 时跟随 dataFilter=uptimerobot(不探测本地目标)', probed.length === 0, `探测了 ${probed.length} 个本地目标`); check('UR 目标进入快照', urIds(snap).length === 3, `${urIds(snap).length} 个`); check('快照带 sources.uptimerobot 同步状态', snap.sources.uptimerobot.ok === true && snap.sources.uptimerobot.count === 3, `count=${snap.sources.uptimerobot.count}`); check('快照带 probeScope(面板据此解释口径)', snap.probeScope === 'uptimerobot', snap.probeScope); check('统计总数 = 实际参与的目标数(scope=uptimerobot 时只有 3 个 UR)', snap.stats.total === 3, `total=${snap.stats.total}`); check('UR 侧口径:在线 1 / 离线 1 / 待检测 1', snap.stats.up === 1 && snap.stats.down === 1 && snap.stats.pending === 1, `up=${snap.stats.up} down=${snap.stats.down} pending=${snap.stats.pending}`); const urState = snap.monitors['ur-700001']; check('UR 目标带 meta(面板靠它区分自建/外部)', urState.meta?.source === 'uptimerobot' && urState.meta.name === 'UR-主站', JSON.stringify(urState.meta).slice(0, 70)); check('UR 目标带 uptimeRatios(面板算官方可用率用)', urState.uptimeRatios?.[90] === 99.99, `${urState.uptimeRatios?.[90]}`); check('UR 目标有 daily 分桶(本探针自行累积)', urState.daily && Object.keys(urState.daily).length > 0, Object.keys(urState.daily)[0]); probed = []; snap = await worker.__test.runChecks(makeEnv(urSettings({ probeScope: 'all' }), LOCAL_TARGETS)); check('显式 probeScope=all 时探测本地目标', probed.length === 2, `探测了 ${probed.length} 个`); check('probeScope=all 时 UR 目标同样在快照中', urIds(snap).length === 3, ''); probed = []; snap = await worker.__test.runChecks(makeEnv(urSettings({ probeScope: 'config' }), LOCAL_TARGETS)); check('probeScope=config 时不拉 UR 也不展示 UR 目标', urIds(snap).length === 0 && probed.length === 2, `UR=${urIds(snap).length} 本地探测=${probed.length}`); probed = []; snap = await worker.__test.runChecks(makeEnv(urSettings({ uptimeRobot: { enabled: false, apiKey: '' } }), LOCAL_TARGETS)); check('UR 未启用时不拉取', urIds(snap).length === 0 && probed.length === 0, `UR=${urIds(snap).length} 本地探测=${probed.length}`); /* ---------- [C] UR 同步失败的降级行为 ---------- */ group('[C] UR 同步失败时保留上次数据(绝不误判为离线)'); const envC = makeEnv(urSettings(), LOCAL_TARGETS); await worker.__test.runChecks(envC); // 先成功一轮,把 UR 数据写进 KV urMode = 'fail'; snap = await worker.__test.runChecks(envC); check('同步失败时标记 ok=false 并给出原因', snap.sources.uptimerobot.ok === false && /UptimeRobot/.test(snap.sources.uptimerobot.error), snap.sources.uptimerobot.error); check('同步失败时 UR 目标仍在快照里(用上次数据)', urIds(snap).length === 3, `${urIds(snap).length} 个`); check('同步失败时 UR 目标状态未被改成 offline', snap.monitors['ur-700002'].status === 'down', `仍是上次的 ${snap.monitors['ur-700002'].status}(真实状态)`); urMode = 'ok'; /* ---------- [D] 管理端路由 ---------- */ group('[D] 管理端:Worker 侧无可写面'); // 真实运行时会用 ctx.waitUntil 托管后台任务,测试里同步执行以便断言生效 const pending = []; const call = (pathname) => worker.default.fetch(new Request(`https://x${pathname}`), makeEnv(urSettings(), LOCAL_TARGETS), { waitUntil(p) { pending.push(p); } }); await Promise.all(pending.map((p) => p.catch(() => {}))); const adminPage = await call('/admin'); const adminBody = await adminPage.json(); check('/admin 返回 403(而非 404,提示去哪找管理端)', adminPage.status === 403, `HTTP ${adminPage.status}`); check('/admin 的提示指向本地 Node 探针', /docs\/DEPLOYMENT\.md|本地 Node/.test(adminBody.hint), adminBody.hint?.slice(0, 50)); const adminApi = await call('/api/admin/overview'); check('/api/admin/* 一律 403', adminApi.status === 403 && (await adminApi.json()).error === 'admin-not-here', `HTTP ${adminApi.status}`); const adminStatus = await call('/api/admin/status'); check('未绑定的 2FA 状态接口在 Worker 上也不开放', adminStatus.status === 403, `HTTP ${adminStatus.status}`); const panel = await call('/'); check('面板本身不受影响', panel.status === 404 || panel.status === 200, `静态资源响应 HTTP ${panel.status}(夹具无 /index.html,属预期)`); const st = await call('/api/status'); const stBody = await st.json(); check('/api/status 正常提供(首次无数据时返回空 state 并后台补跑)', st.status === 200 && typeof stBody === 'object' && stBody.monitors !== undefined, `HTTP ${st.status},monitors=${stBody.monitors ? Object.keys(stBody.monitors).length : '无'}`); check('首次访问触发后台探测(waitUntil 被调用)', pending.length > 0, `${pending.length} 个后台任务`); globalThis.fetch = origFetch; urSrv.close(); httpSrv.close(); const failed = results.filter((x) => !x.pass); console.log(`\n结果:${results.length - failed.length}/${results.length} 通过`); if (failed.length) { console.log('失败项:'); for (const f of failed) console.log(` ✗ ${f.name}`); process.exit(1); } } main().catch((err) => { console.error('FAILED:', err.stack || err.message); process.exit(1); });