'use strict'; // 标准中间件:错误边界。 // 路由处理器只需 throw,由这里统一映射为响应: // · err.status 优先(处理器可对上游错误标注 502 等) // · 智能问数的参数类错误码 → 400 // · 其余 → 500 // 响应体形状与拆分前一致:{ error: message, code?: errCode } const crypto = require('crypto'); const { sendJson } = require('./util.js'); const ASK_CLIENT_CODES = new Set(['ASK_NO_MODEL', 'ASK_EMPTY', 'ASK_NO_ENTITY']); function errorStatus(e) { if (e && e.status) return e.status; if (e && ASK_CLIENT_CODES.has(e.code)) return 400; return 500; } function sendError(res, e) { if (res.headersSent) { // 响应已开始(流式文件等)时只能终止连接,与拆分前 writeHead 抛错的表现一致但不崩进程 res.destroy(); return; } const body = { error: (e && e.message) || String(e) }; if (e && e.code !== undefined) body.code = e.code; sendJson(res, errorStatus(e), body); } // 包裹异步处理器:同步/异步异常都进入错误边界 function errorBoundary(handler) { return async (c) => { try { await handler(c); } catch (e) { sendError(c.res, e); } }; } // ---------- 访问口令(局域网模式) ---------- // config.accessToken 非空时启用:/api/* 与 /files 需要口令(cookie / header / query 三选一)。 // 本机回环访问与健康检查放行;静态页面放行(无登录页,页面先加载,前端 401 后引导输入)。 function parseCookie(header) { const out = {}; String(header || '').split(';').forEach((part) => { const i = part.indexOf('='); if (i > 0) { try { out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim()); } catch { out[part.slice(0, i).trim()] = part.slice(i + 1).trim(); } } }); return out; } function isLoopbackRemote(req) { const addr = String((req.socket && req.socket.remoteAddress) || ''); return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1'; } // 常量时间比较:LAN 场景防时序侧信道逐字节猜口令 function safeEqual(a, b) { const ba = Buffer.from(String(a)); const bb = Buffer.from(String(b)); if (ba.length !== bb.length) { crypto.timingSafeEqual(ba, ba); // 长度不等也耗一遍比较,不泄漏长度信息 return false; } return crypto.timingSafeEqual(ba, bb); } function makeTokenGuard(config) { return function tokenGuard(req, pathname, searchParams) { const expected = config.accessToken; if (!expected) return true; if (pathname === '/healthz' || pathname === '/readyz') return true; if (!pathname.startsWith('/api/') && !pathname.startsWith('/files')) return true; if (isLoopbackRemote(req)) return true; if (safeEqual(searchParams.get('token') || '', expected)) return true; if (safeEqual(req.headers['x-access-token'] || '', expected)) return true; if (safeEqual(parseCookie(req.headers.cookie).accessToken || '', expected)) return true; return false; }; } // ---------- 本地 CSRF 防护 ---------- // 回环请求在口令守卫里全放行,意味着用户浏览器里任意网页都能向 127.0.0.1 打 // no-cors POST(不带自定义头也能被 readBody 解析)。浏览器发起的跨站写请求必带 // Origin;本站同源请求的 Origin 与 Host 一致;非浏览器客户端(curl/脚本)不带 // Origin → 放行,仍受口令守卫约束。 function makeOriginGuard() { const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); return function originGuard(req) { if (SAFE_METHODS.has(req.method)) return true; const origin = req.headers.origin; if (!origin) return true; try { return new URL(origin).host === (req.headers.host || ''); } catch { return false; } }; } module.exports = { ASK_CLIENT_CODES, errorStatus, sendError, errorBoundary, makeTokenGuard, makeOriginGuard };