Files
micro_servers/gateway
qing 2e3f07bf4f docs(gateway): 审计文档行号纠偏并补充 CORS 白名单死代码根因
- 修正 gateway 审计文档中因代码演进漂移的行号引用(handleProxy/handleLogout/wrapperHttp/限流/TLS/路径防护等)

- 补充 P2-3 CORS 根因:_CORS_ORIGINS 白名单已读取但 OPTIONS 预检由 libhttp 处理,白名单未生效

- 修正 README 中 OPTIONS 预检描述(反射请求 Origin 而非通配 *)及 CORS 配置失效提示
2026-08-03 23:37:47 +08:00
..
2026-07-20 11:12:29 +08:00
2026-07-20 11:12:29 +08:00
2026-07-20 11:12:29 +08:00
^-^
2026-08-01 06:04:27 +08:00

gateway

UniServe Framework 的应用层网关,C++20 实现。

在引入 waf 模块后,gateway 退化为内部 HTTP 服务:TLS 由 WAF 终止,gateway 专注做账号鉴权与反向代理。通过 Server.tls 配置可一键切换独立部署(HTTPS)或与 WAF 协作(HTTP)两种模式。

职责

  • 账号鉴权:/register 注册(需邮箱验证码,经 pymsg 调 smtp:8008 校验)、/login 登录,签发 RSA 签名的 Token。
  • HTTP 反向代理:携带 Token 的请求校验后转发到 Proxy.opened 中放开的内部服务。
  • WebSocket 反向代理(仅管理员):[WsProxy] 段配置的路径在 HTTP Upgrade 握手时校验 Token(is_admin=true),通过后透明桥接到后端 ws:// 或 wss:// 服务,支持文本/二进制帧双向转发。
  • CAPTCHA 接口:/captcha 生成验证码 token + 文本(内存存储,5 分钟过期,一次性使用)。
  • 邮箱验证码:/register/send_code 发送 6 位验证码邮件(10 分钟有效);注册时校验,一次性消费。
  • 速率限制:登录 5 次/5 分钟、注册 3 次/10 分钟、发验证码 5 次/10 分钟(IP 级)+ 3 次/小时/10 次/天(邮箱级),按 X-Forwarded-For 客户端 IP 计数(由 WAF 注入),超限一律返回 429。
  • 邮件轰炸防护:/register/send_code 强制 CAPTCHA 校验 + 邮箱级双窗口限流(3 次/小时 + 10 次/天),smtp 层兜底同样限流 + 全局熔断(默认 100 封/小时,可配)。
  • OPTIONS 预检:由 libhttp 处理,Access-Control-Allow-Origin 反射请求的 Origin 头(任意来源均被放行);CORS.allowed_origins 白名单已读取但尚未用于预检响应(见审计 P2-3)。

安全特性

特性 实现
密码哈希 PBKDF2-HMAC-SHA256,100000 轮,16 字节随机盐,32 字节哈希
密码强度校验 至少 8 字符;大写/小写/数字/特殊字符中至少 3 类
邮箱格式校验 正则匹配 ^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
旧哈希自动迁移 登录时若发现 SHA256 旧哈希,验证通过后自动迁移为 PBKDF2
Token 机制 Base64(payload) + "_" + RSA-Sign(payload),验签 + 过期校验(默认 1 小时)
常数时间比较 PBKDF2 验证用 `diff
CAPTCHA 一次性、5 分钟过期,登录与注册均可选启用
速率限制 内存计数器,自动清理超过 1 万条的老条目
TLS 可选 Server.tls = off 时纯 HTTP(配合 WAF),on 时用 full_chain.crt / server.key
下游服务容错 pymsg 客户端 retry_on_refused 机制:db 等强依赖服务未启动时自动阻塞重试,避免启动顺序问题;日志客户端 fail-safe 静默丢弃

pymsg 内部通信协议

gateway 通过项目内的 pymsg(长度前缀帧 TCP 协议)与内部微服务通信,C++ 实现在 inc/Msg.hpp,与 Python 端字节级兼容。

线协议:[4字节大端uint32长度][UTF-8载荷],最大 64 MiB。

JSON RPC 约定:请求形如 {"_route": "<路由名>", ...},响应形如 {"success": true, "data": ...} 或 {"success": false, "error": "..."}。

下游服务调用方式:

下游 默认端口 _route 调用位置 重试策略
db 8005 execute DbC::call_msg()(所有数据库 CRUD) retry_on_refused=true,db 未就绪时阻塞等待
db 8005 execute LogC::_send()(日志异步批量推送) retry_on_refused=false,fail-safe 静默丢弃
captcha 8009 verify / image verify_captcha_remote()、/captcha/image 代理 默认不重试,连接失败 fail-closed 返回 502/false
smtp 8008 verify_email / send_email verify_email_code_remote()、/register/send_code 默认不重试,连接失败 fail-closed 返回 502/false
confs 8010 get 启动时拉取 gateway.ini 配置 retry_on_refused=true,confs 未就绪时阻塞等待

retry_on_refused 机制(MsgClient::call(),对应 Python 端 MsgClient.call(retry_on_refused=True)):

  • 仅对 ECONNREFUSED(目标端口无服务监听)重试,超时、连接重置、业务错误立即抛出
  • 固定 1 秒间隔重试;首次失败立即打印 stderr 提示,之后每 5 秒打印一次等待日志(节制日志,避免刷屏)
  • 用于解决微服务启动顺序不确定问题:gateway 启动时 db 未就绪,首次 db 调用自动阻塞等待直到 db 上线

接口

POST /captcha

生成验证码。返回:

{
  "token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "text": "Ab3xy"
}

POST /login

请求体:

{
  "username": "user@example.com",
  "password": "P@ssw0rd",
  "captcha_token": "a1b2c3d4...",   // 可选
  "captcha_answer": "Ab3xy"          // 可选
}

成功响应:

{
  "success": true,
  "data": { "token": "<base64_payload>_<rsa_sign>" }
}

失败响应:

{ "success": false, "message": "Invalid Username or Password" }

错误码:400(参数/CAPTCHA 错误)、401(账号/密码错或 Token 过期)、429(速率超限)。

POST /register

请求体在 /login 基础上增加 email_code(必填,6 位数字邮箱验证码):

{ "username": "user@example.com", "password": "...", "email_code": "123456" }

校验顺序:密码强度 → 邮箱格式 → CAPTCHA(可选)→ 邮箱验证码(经 pymsg 调 smtp:8008 verify_email,一次性消费,连续失败 5 次自动作废,fail-closed),全部通过后写入 Users 表。

错误码:400(密码/邮箱/CAPTCHA/邮箱验证码不合规)、429(速率超限)、500(数据库异常)。

POST /register/send_code

向目标邮箱发送注册验证码(经 pymsg 调 smtp:8008 send_email,10 分钟有效)。

请求体(CAPTCHA 必填,防邮件轰炸):

{
  "email": "user@example.com",
  "captcha_token": "a1b2c3d4...",
  "captcha_answer": "Ab3xy"
}

防护层次:

  1. IP 限流:5 次/10 分钟(X-Forwarded-For)
  2. CAPTCHA 强制校验:经 captcha:8009 pymsg 校验,Token 一次性消费
  3. 邮箱级双窗口限流:3 次/小时 + 10 次/天
  4. smtp 同邮箱 60 秒发送冷却(冷却期拒绝时返回 retry_after 剩余秒数与提示文案,gateway 按 429 透传;已发出且在有效期内的验证码不受影响,发送失败不计入冷却)
  5. smtp 邮箱级长期限流兜底 + 全局熔断(默认 100 封/小时,SMTP_HOURLY_LIMIT 可配)

错误码:400(邮箱格式错误/CAPTCHA 缺失或错误)、429(IP 速率超限/邮箱级限流超限/同邮箱发送冷却)、502(smtp 服务不可用)、500(发送失败)。

GET/POST *(反向代理)

携带 header:

Authorization: Bearer <base64_payload>_<rsa_sign>
Target-Url: http://127.0.0.1:9200   // 可选,缺省取 Proxy.default
  • Target-Url 缺省时走 Proxy.default(不需 Token)。
  • 自定义 Target-Url 必须在 Proxy.opened 白名单中,否则返回 403。
  • 验签通过且未过期(默认 1 小时)才转发,否则返回 401。
  • Proxy.closed(如 db:8005、ai_review:8007)不在 opened 中,外部无法直访。

OPTIONS *

返回 CORS 预检响应:Access-Control-Allow-Origin 反射请求 Origin 头(等价于允许任意来源,见审计 P2-3)、Access-Control-Allow-Methods: GET, POST, OPTIONS、Max-Age: 86400。

WebSocket 代理([WsProxy] 配置的路径)

握手阶段鉴权规则:

  1. 从 Authorization: Bearer <token> 头读取 Token;若缺失则回退到 Cookie: q4_token=<token>。
  2. RSA 验签 + 过期校验(与 HTTP 代理同样的 Token 机制)。
  3. Token payload 中必须 is_admin=true(即登录账号在 Admin.users 白名单内)。
  4. 鉴权失败 → 返回 HTTP 403 Forbidden,连接被拒绝。
  5. 鉴权通过 → 升级为 WebSocket 并透明桥接到配置的后端 ws:// 或 wss:// 地址,双向文本/二进制帧原样转发。

客户端使用方式与普通 WebSocket 一致,只需携带有效管理员 Token:

// 浏览器(同源会自动带 q4_token Cookie)
const ws = new WebSocket("wss://qsont.me/ws/aibox");

// 或显式带 Authorization(构造子协议或连接后首条消息均可,这里由网关在 Upgrade 头校验)

注意:WS 代理路由必须精确匹配(与 ws() 一致,非通配),且优先级高于 HTTP 通配 * 路由。

数据库

用户数据存于 db 服务(pymsg/TCP :8005)的 SQLite 库,表 Users(由 db/create_sqlite3.sql 建立,gateway 不再自动建表):

字段 类型 说明
id INTEGER PK AUTOINCREMENT
username TEXT UNIQUE NOT NULL 登录账号(邮箱格式)
password_hash TEXT NOT NULL pbkdf2:100000:<salt_hex>:<hash_hex>
create_at DATETIME DEFAULT CURRENT_TIMESTAMP
last_login DATETIME 每次登录更新
is_active BOOLEAN DEFAULT 1

gateway 不直连数据库(早期直连本地 sqlite 的代码已注释保留),所有读写经 DbC::call_msg 调 db 的 execute 路由。无默认管理员账号——首位注册者即为管理员。

配置

gateway 配置采用两层加载模式,与其他 Python 服务(dnfs/db/static)保持一致:

  1. confs 统一配置服务:启动时从 confs(127.0.0.1:8010,可通过 CONFS_HOST/CONFS_PORT 环境变量覆盖)通过 pymsg get 路由拉取 gateway.ini,retry_on_refused=true 阻塞等待 confs 就绪。
  2. 环境变量覆盖(最高优先级,Docker 兼容):LISTEN_ADDR/LISTEN_PORT、DB_HOST/DB_PORT、CAPTCHA_HOST/CAPTCHA_PORT、SMTP_HOST/SMTP_PORT 覆盖 ini 中对应值。
  3. 完整性校验:必需配置项缺失则启动失败。

配置文件统一存放于 confs/configs/gateway.ini,gateway 目录下不再保留本地配置文件。

配置项说明:

  • Server.address/port/tls:监听地址、端口、TLS 开关;address/port 可被 LISTEN_ADDR/LISTEN_PORT 环境变量覆盖。
  • Proxy.default:未带 Target-Url header 时的默认转发地址(不需 Token)。
  • Proxy.opened:允许外部带 Token 访问的内部服务白名单(分号分隔)。
  • Proxy.closed:仅文档记录用途,实际靠"不在 opened 中即拒绝"实现隔离。
  • Token.*:Token 有效期(keep_time)、刷新宽限期倍数(refresh_grace_multiplier)、最大刷新次数(max_refresh)。
  • Admin.users:管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 is_admin=true。
  • CORS.allowed_origins:允许跨域的 Origin 白名单(分号分隔)。⚠️ 当前已读取但尚未用于预检响应——OPTIONS 预检由 libhttp 反射请求 Origin,白名单暂不生效(见审计 P2-3)。
  • WsProxy.routes:WebSocket 代理路由,格式 /path=ws://host:port/target,分号分隔。
  • Downstream.*:下游 pymsg 服务地址(db/captcha/smtp),可被 DB_HOST/DB_PORT、CAPTCHA_HOST/CAPTCHA_PORT、SMTP_HOST/SMTP_PORT 环境变量覆盖。

部署模式

模式 A:与 WAF 协作(推荐)

客户端 ──HTTPS──▶ waf:443 (终止 TLS + coraza 规则) ──HTTP──▶ gateway:8443
address = 127.0.0.1
port = 8443
tls = off

证书放在 waf/certs/。RateLimiter 从 WAF 注入的 X-Forwarded-For 取真实客户端 IP。

模式 B:独立部署

客户端 ──HTTPS──▶ gateway:443 (终止 TLS + 鉴权 + 反代)
address = 0.0.0.0
port = 443
tls = on

证书 full_chain.crt / server.key 放在 gateway 目录。RateLimiter 取不到 X-Forwarded-For,所有请求记为 "direct"。

编译与运行

依赖系统库(由根目录 _setup.deb.sh 或 _setup.rhel.sh 安装):

  • libboost_json、libboost_program_options
  • libcurl
  • libssl / libcrypto(OpenSSL,PBKDF2 + RSA + RAND_bytes)
  • libsqlite3
  • 项目内库:libus_http、libus_crypt(由 libhttp、libcrypt 编译安装)
bash _set.sh   # make clean && make
bash _run.sh   # 从 confs 拉取配置启动

无需传参,配置统一由 confs 管理。命令行参数:-h/--help、-v/--version。

目录结构

gateway/
├── src/
│   └── main.cpp          # 全部实现:配置加载(confs → 环境变量覆盖)、PBKDF2、RateLimiter、路由/handler
├── inc/
│   ├── Msg.hpp           # pymsg C++ 实现:长度前缀帧协议、MsgClient(含 retry_on_refused)、MsgException 族
│   ├── DbC.hpp           # db 服务客户端:通过 pymsg 调 dbv2(地址可配置,retry_on_refused=true)
│   ├── LogC.hpp          # 统一日志客户端:异步批量队列 + pymsg INSERT(fail-safe)
│   ├── Conf.hpp          # ConfIni:boost::property_tree ini 解析,支持 fromString/env/env_int
│   ├── ExpBase.hpp       # 异常基类(带 source_location)
│   └── LocalTime.hpp     # 本地时间工具(getCurrent / parse)
├── Makefile
├── _set.sh / _run.sh
└── README.md

配置文件统一在 confs/configs/gateway.ini,gateway 目录无本地配置文件。

版本

0.7.0(见 src/main.cpp:31)。启动时打印:

Gateway 0.7.0 started

近期变更:

  • 0.7.0:配置体系完全迁移到 confs 统一配置服务——配置文件统一存放于 confs/configs/gateway.ini(原 example.ini 内容 + [Downstream] 段),删除本地 example.ini,移除 -i/--input 命令行参数(不再支持本地 ini 文件回退,与 static 模块一致);启动时从 confs(CONFS_HOST/CONFS_PORT,默认 127.0.0.1:8010)通过 pymsg get 路由拉取配置(retry_on_refused=true);ConfIni 增加 fromString()/env()/env_int();环境变量覆盖支持 LISTEN_ADDR/LISTEN_PORT/DB_HOST/DB_PORT/CAPTCHA_HOST/CAPTCHA_PORT/SMTP_HOST/SMTP_PORT;配置完整性校验;DbC 增加 init(host, port),main.cpp 所有硬编码 captcha/smtp/db 地址改为配置变量,logs().set_endpoint() 启动时初始化;--version/-v 选项补全实现;版本升至 0.7.0。
  • 0.6.1:pymsg 客户端增加 retry_on_refused 重试机制——新增 MsgConnectionRefusedException;MsgClient::call() 新增 retry_on_refused/retry_interval 参数;DbC::call_msg() 启用重试等待 db 就绪;LogC::_send() 显式不重试(fail-safe)。
  • 0.6.0:WebSocket 反向代理(仅管理员)——新增 [WsProxy] 配置段,支持 routes = /path=ws://host:port/target;... 精确路径映射;握手阶段校验 Token 中 is_admin=true(基于 Admin.users 白名单),通过后双向透明桥接文本/二进制帧到后端,支持 ws:// 和 wss://;后端连接错误/鉴权失败均优雅关闭。依赖 libhttp v3.1+ 的 ws_proxy() 接口。顺带修复 LocalTime.hpp 在 Apple clang 下 C++20 tzdb 不可用的编译问题(改用 strftime/get_time/mktime)。
  • 0.5.4:修复邮件轰炸漏洞(审计报告 P0-1)——/register/send_code 强制 CAPTCHA 校验(经 captcha:8009,Token 一次性消费),新增邮箱级双窗口限流(3 次/小时 + 10 次/天);smtp 层同步增加邮箱级长期限流兜底 + 全局熔断(SMTP_HOURLY_LIMIT 环境变量,默认 100 封/小时);前端 register.html/register.js 同步接入图形验证码(复用 /captcha/image 代理路由,方案 A:仅在「获取验证码」时要求 CAPTCHA,注册提交不加)。
  • 0.5.3:配合 smtp 同邮箱 60 秒发送冷却——/register/send_code 在 smtp 返回 retry_after 时按 429 透传其提示文案(其余发送失败仍用通用 500 文案,SMTP 内部错误细节不向外暴露)。
  • 0.5.2:限流错误码统一为 429(登录原 400、注册/发码原 403,并修正 "attampts" 拼写);登录时用户不存在不再兜底 500——空结果统一走 401,并执行 dummy PBKDF2 校验拉平时序防用户名枚举;db 查询失败消息由误名的 "Login (Update Step) Failed" 改为 "Login (Query Step) Failed";pymsg 下游调用超时常量化(DbC::DEFAULT_TIMEOUT、_TIMEOUT_CAPTCHA/_SMTP_VERIFY/_SMTP_SEND)。
  • 0.5.1:登录成功后通过 db 更新 Users.last_login("$now" 哨兵,db 日期适配层转为服务端当前 UTC 时间;更新失败经 THROW_EX_CUSTOM 抛 500,与其他步骤一致)。

依赖

  • C++20(g++)
  • Boost:boost::json、boost::program_options、boost::property_tree
  • OpenSSL:PBKDF2-HMAC-SHA256、RSA 签名/验签、RAND_bytes
  • libcurl(反向代理出站请求)
  • libsqlite3(用户表存储)
  • 项目库:libhttp(HttpServer/HttpClient)、libcrypt(MyRSA/Base64/SHA256)、libsqlite3