middleware.js 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. 'use strict';
  2. // 标准中间件:错误边界。
  3. // 路由处理器只需 throw,由这里统一映射为响应:
  4. // · err.status 优先(处理器可对上游错误标注 502 等)
  5. // · 智能问数的参数类错误码 → 400
  6. // · 其余 → 500
  7. // 响应体形状与拆分前一致:{ error: message, code?: errCode }
  8. const { sendJson } = require('./util.js');
  9. const ASK_CLIENT_CODES = new Set(['ASK_NO_MODEL', 'ASK_EMPTY', 'ASK_NO_ENTITY']);
  10. function errorStatus(e) {
  11. if (e && e.status) return e.status;
  12. if (e && ASK_CLIENT_CODES.has(e.code)) return 400;
  13. return 500;
  14. }
  15. function sendError(res, e) {
  16. if (res.headersSent) {
  17. // 响应已开始(流式文件等)时只能终止连接,与拆分前 writeHead 抛错的表现一致但不崩进程
  18. res.destroy();
  19. return;
  20. }
  21. const body = { error: (e && e.message) || String(e) };
  22. if (e && e.code !== undefined) body.code = e.code;
  23. sendJson(res, errorStatus(e), body);
  24. }
  25. // 包裹异步处理器:同步/异步异常都进入错误边界
  26. function errorBoundary(handler) {
  27. return async (c) => {
  28. try {
  29. await handler(c);
  30. } catch (e) {
  31. sendError(c.res, e);
  32. }
  33. };
  34. }
  35. // ---------- 访问口令(局域网模式) ----------
  36. // config.accessToken 非空时启用:/api/* 与 /files 需要口令(cookie / header / query 三选一)。
  37. // 本机回环访问与健康检查放行;静态页面放行(无登录页,页面先加载,前端 401 后引导输入)。
  38. function parseCookie(header) {
  39. const out = {};
  40. String(header || '').split(';').forEach((part) => {
  41. const i = part.indexOf('=');
  42. if (i > 0) {
  43. try { out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim()); }
  44. catch { out[part.slice(0, i).trim()] = part.slice(i + 1).trim(); }
  45. }
  46. });
  47. return out;
  48. }
  49. function isLoopbackRemote(req) {
  50. const addr = String((req.socket && req.socket.remoteAddress) || '');
  51. return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
  52. }
  53. function makeTokenGuard(config) {
  54. return function tokenGuard(req, pathname, searchParams) {
  55. const expected = config.accessToken;
  56. if (!expected) return true;
  57. if (pathname === '/healthz' || pathname === '/readyz') return true;
  58. if (!pathname.startsWith('/api/') && !pathname.startsWith('/files')) return true;
  59. if (isLoopbackRemote(req)) return true;
  60. if (searchParams.get('token') === expected) return true;
  61. if (req.headers['x-access-token'] === expected) return true;
  62. if (parseCookie(req.headers.cookie).accessToken === expected) return true;
  63. return false;
  64. };
  65. }
  66. module.exports = { ASK_CLIENT_CODES, errorStatus, sendError, errorBoundary, makeTokenGuard };