feat(gateway): migrate configuration to confs service (v0.7.0)
Migrate gateway from local -i ini file to the unified confs configuration
service, following the same two-layer pattern as Python services (dnfs/db/static):
- Add confs/configs/gateway.ini with all existing settings plus new
[Downstream] section for db/captcha/smtp host:port (previously hardcoded).
- Conf.hpp: add fromString() to parse ini from text (confs response),
env()/env_int() static helpers for environment variables, public default
constructor, and empty-filename guard in file constructor.
- DbC.hpp: add init(host, port) static method and static _host/_port members,
replacing hardcoded "127.0.0.1":8005. Defaults preserved for backward compat.
- main.cpp:
* Rewrite config loading: bootstrap CONFS_HOST/CONFS_PORT from env
(default 127.0.0.1:8010), fetch gateway.ini via pymsg from confs
(retry_on_refused=true), parse with ConfIni::fromString().
* -i flag is now optional; when provided, reads local ini (dev/debug mode).
* Add downstream host/port globals (_DB_HOST/_DB_PORT, _CAPTCHA_HOST/_CAPTCHA_PORT,
_SMTP_HOST/_SMTP_PORT, _CONFS_HOST/_CONFS_PORT), populated from [Downstream].
* Environment variable overrides (highest priority): LISTEN_ADDR/LISTEN_PORT,
DB_HOST/DB_PORT, CAPTCHA_HOST/CAPTCHA_PORT, SMTP_HOST/SMTP_PORT.
* Config integrity validation: required fields (Server.address/port,
Proxy.default, Downstream.db_host/db_port) cause startup failure if missing.
* Initialize DbC::init() and logs().set_endpoint() after config load.
* Replace all hardcoded "127.0.0.1":8005/8008/8009 in captcha/smtp/db calls
with configurable globals.
* Add downstream service info to startup log output.
* Fix: -v/--version flag now actually prints version and exits.
* Bump version to 0.7.0.
- _run.sh: remove -i example.ini default; now starts without args (confs mode),
passes "$@" through for optional -i override.
- Update README with new config loading docs, Downstream config section,
updated directory structure, and v0.7.0 changelog.
This commit is contained in:
1 parent
de58c0487a
commit
9ccb9cbdca
6 files changed
+289
-108
No files matched your search
@@ -0,0 +1,53 @@
|
||||
[Server]
|
||||
# 当 WAF 在前面终止 TLS 时,gateway 退化为内部 HTTP 服务:
|
||||
# - tls = off 启用 HTTP 模式(配合 waf 模块使用)
|
||||
# - tls = on 启用 HTTPS 模式(独立部署,证书为 full_chain.crt / server.key)
|
||||
address = 127.0.0.1
|
||||
port = 8443
|
||||
tls = off
|
||||
|
||||
[Proxy]
|
||||
default = http://127.0.0.1:9200
|
||||
closed = http://127.0.0.1:8005;http://127.0.0.1:8007;
|
||||
opened = http://127.0.0.1:9200;http://127.0.0.1:8006;
|
||||
|
||||
[Token]
|
||||
# Token 有效期(秒),默认 3600 = 1 小时
|
||||
keep_time = 3600
|
||||
# Token 刷新宽限期倍数:过期后 iat + keep_time * 倍数 窗口内仍可刷新,默认 2
|
||||
refresh_grace_multiplier = 2
|
||||
# Token 生命周期内最大刷新次数,超过后需重新登录,默认 10
|
||||
max_refresh = 10
|
||||
|
||||
[Admin]
|
||||
# 管理员账号白名单(分号分隔)。登录时命中即在 Token payload 写入 is_admin=true,
|
||||
# 下游服务(dnfs 等)凭该字段豁免所有权校验,无需各自维护名单。
|
||||
# 修改后需重启 gateway 生效;旧 Token 无此字段,按非管理员处理。
|
||||
users = qing@qsont.me;shufeng@qsont.me
|
||||
|
||||
[CORS]
|
||||
# 允许跨域的 Origin 白名单(分号分隔)
|
||||
# 仅命中的 Origin 才会回填 Access-Control-Allow-Origin;缺省为空(不回填)
|
||||
allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443
|
||||
|
||||
[Database]
|
||||
path = sqlite3.db
|
||||
|
||||
[WsProxy]
|
||||
# WebSocket 反向代理路由(仅管理员可访问)
|
||||
# 格式:routes = /path=ws://host:port/target;/path2=ws://host2:port2/target2
|
||||
# 握手阶段会校验 HTTP Upgrade 请求中的 Token(Authorization: Bearer xxx 或 Cookie: q4_token=xxx),
|
||||
# 仅 Token 中 is_admin=true 的用户才能升级连接;非管理员或无 Token 直接拒绝。
|
||||
# 支持 ws:// 和 wss:// 后端
|
||||
# 示例:routes = /ws/aibox=ws://127.0.0.1:8765;/ws/other=wss://example.com/ws
|
||||
routes = /ws/aibox=ws://127.0.0.1:8765;
|
||||
|
||||
[Downstream]
|
||||
# 下游 pymsg 服务地址(其他微服务的 host:port)
|
||||
# 环境变量可覆盖:DB_HOST/DB_PORT, CAPTCHA_HOST/CAPTCHA_PORT, SMTP_HOST/SMTP_PORT, CONFS_HOST/CONFS_PORT
|
||||
db_host = 127.0.0.1
|
||||
db_port = 8005
|
||||
captcha_host = 127.0.0.1
|
||||
captcha_port = 8009
|
||||
smtp_host = 127.0.0.1
|
||||
smtp_port = 8008
|
||||
+31
-57
@@ -40,12 +40,13 @@ gateway 通过项目内的 [pymsg](../pymsg/)(长度前缀帧 TCP 协议)与
|
||||
|
||||
**下游服务调用方式**:
|
||||
|
||||
| 下游 | 端口 | `_route` | 调用位置 | 重试策略 |
|
||||
|------|------|----------|---------|---------|
|
||||
| 下游 | 默认端口 | `_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`(目标端口无服务监听)重试,超时、连接重置、业务错误立即抛出
|
||||
@@ -186,53 +187,25 @@ gateway 不直连数据库(早期直连本地 sqlite 的代码已注释保留
|
||||
|
||||
## 配置
|
||||
|
||||
[example.ini](example.ini):
|
||||
gateway 配置采用**两层加载模式**,与其他 Python 服务(dnfs/db/static)保持一致:
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
# tls = off HTTP 模式(配合 waf 模块)
|
||||
# tls = on HTTPS 模式(独立部署,证书为 full_chain.crt / server.key)
|
||||
address = 127.0.0.1
|
||||
port = 8443
|
||||
tls = off
|
||||
1. **confs 统一配置服务**(默认模式):启动时从 confs(`127.0.0.1:8010`,可通过 `CONFS_HOST`/`CONFS_PORT` 环境变量覆盖)通过 pymsg `get` 路由拉取 `gateway.ini`,`retry_on_refused=true` 阻塞等待 confs 就绪。
|
||||
2. **本地文件覆盖**(开发/调试模式):启动时传 `-i <path>` 则跳过 confs,直接读取本地 ini 文件。
|
||||
3. **环境变量覆盖**(最高优先级,Docker 兼容):`LISTEN_ADDR`/`LISTEN_PORT`、`DB_HOST`/`DB_PORT`、`CAPTCHA_HOST`/`CAPTCHA_PORT`、`SMTP_HOST`/`SMTP_PORT` 覆盖 ini 中对应值。
|
||||
4. **完整性校验**:必需配置项缺失则启动失败。
|
||||
|
||||
[Proxy]
|
||||
default = http://127.0.0.1:9200
|
||||
closed = http://127.0.0.1:8005;http://127.0.0.1:8007;
|
||||
opened = http://127.0.0.1:9200;http://127.0.0.1:8006;
|
||||
配置文件存放于 [confs/configs/gateway.ini](../confs/configs/gateway.ini)([example.ini](example.ini) 为本地开发参考副本,内容一致,新增 `[Downstream]` 段)。
|
||||
|
||||
[Token]
|
||||
# Token 有效期(秒),默认 3600 = 1 小时
|
||||
keep_time = 3600
|
||||
|
||||
[Admin]
|
||||
# 管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 is_admin=true
|
||||
# WebSocket 代理仅允许管理员访问,其他需管理员权限的下游同样凭此字段判断
|
||||
users = admin@example.com
|
||||
|
||||
[CORS]
|
||||
# 允许跨域的 Origin 白名单(分号分隔)
|
||||
# 仅命中的 Origin 才会回填 Access-Control-Allow-Origin;缺省为空(不回填)
|
||||
allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443
|
||||
|
||||
[Database]
|
||||
path = sqlite3.db
|
||||
|
||||
[WsProxy]
|
||||
# WebSocket 反向代理路由(仅管理员)
|
||||
# 格式:/path=ws://host:port/target;/path2=wss://host2/target2
|
||||
# 支持 ws:// 和 wss:// 后端,握手阶段校验管理员 Token
|
||||
# routes = /ws/aibox=ws://127.0.0.1:8765;/ws/log=wss://example.com/log
|
||||
routes =
|
||||
```
|
||||
|
||||
- `Proxy.default`:未带 `Target-Url` header 时的默认转发地址(不需 Token,主要服务前端静态资源)。
|
||||
配置项说明:
|
||||
- `Server.address`/`port`/`tls`:监听地址、端口、TLS 开关;`address`/`port` 可被 `LISTEN_ADDR`/`LISTEN_PORT` 环境变量覆盖。
|
||||
- `Proxy.default`:未带 `Target-Url` header 时的默认转发地址(不需 Token)。
|
||||
- `Proxy.opened`:允许外部带 Token 访问的内部服务白名单(分号分隔)。
|
||||
- `Proxy.closed`:仅记录用途,实际靠"不在 opened 中即拒绝"实现隔离。
|
||||
- `Token.keep_time`:登录签发 Token 的有效期(秒),缺省 3600。
|
||||
- `Admin.users`:管理员白名单(分号分隔邮箱),登录时命中即在 Token 中写入 `is_admin=true`。
|
||||
- `CORS.allowed_origins`:OPTIONS 预检允许跨域的 Origin 白名单(分号分隔),缺省为空(不回填 ACAO)。
|
||||
- `WsProxy.routes`:WebSocket 代理路由映射,格式 `/path=ws://host:port/target`,多个用分号分隔;留空则不开启 WS 代理。
|
||||
- `Proxy.closed`:仅文档记录用途,实际靠"不在 opened 中即拒绝"实现隔离。
|
||||
- `Token.*`:Token 有效期(`keep_time`)、刷新宽限期倍数(`refresh_grace_multiplier`)、最大刷新次数(`max_refresh`)。
|
||||
- `Admin.users`:管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 `is_admin=true`。
|
||||
- `CORS.allowed_origins`:允许跨域的 Origin 白名单(分号分隔)。
|
||||
- `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` 环境变量覆盖。
|
||||
|
||||
## 部署模式
|
||||
|
||||
@@ -275,42 +248,43 @@ tls = on
|
||||
- 项目内库:`libus_http`、`libus_crypt`(由 [libhttp](../libhttp/)、[libcrypt](../libcrypt/) 编译安装)
|
||||
|
||||
```bash
|
||||
bash _set.sh # make clear && make
|
||||
bash _run.sh # ./build/gateway.out -i example.ini
|
||||
bash _set.sh # make clean && make
|
||||
bash _run.sh # 默认从 confs 拉取配置;开发模式可用 _run.sh -i example.ini
|
||||
```
|
||||
|
||||
`gateway.out` 需以 `-i <ini>` 传配置文件路径。其他参数:`-h/--help`、`-v/--version`。
|
||||
默认无需传参(从 confs 拉取配置);`-i <ini>` 为本地文件模式(开发/调试用)。其他参数:`-h/--help`、`-v/--version`。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
gateway/
|
||||
├── src/
|
||||
│ └── main.cpp # 全部实现:PBKDF2 / PasswordValidator / RateLimiter / 路由 / handler
|
||||
│ └── main.cpp # 全部实现:配置加载(confs/本地文件/环境变量)、PBKDF2、RateLimiter、路由/handler
|
||||
├── inc/
|
||||
│ ├── Msg.hpp # pymsg C++ 实现:长度前缀帧协议、MsgClient(含 retry_on_refused 重试)、MsgException 族
|
||||
│ ├── DbC.hpp # db 服务客户端:通过 pymsg 调 dbv2 execute 路由(retry_on_refused=true)
|
||||
│ ├── LogC.hpp # 统一日志客户端:异步批量队列 + pymsg INSERT(fail-safe,不重试)
|
||||
│ ├── Conf.hpp # ConfIni:基于 boost::property_tree 的 ini 解析
|
||||
│ ├── 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)
|
||||
├── example.ini # 本地开发配置参考副本(与 confs/configs/gateway.ini 一致)
|
||||
├── Makefile
|
||||
├── example.ini
|
||||
├── _set.sh / _run.sh
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 版本
|
||||
|
||||
`0.6.1`(见 [src/main.cpp:31](src/main.cpp#L31))。启动时打印:
|
||||
`0.7.0`(见 [src/main.cpp:31](src/main.cpp#L31))。启动时打印:
|
||||
|
||||
```
|
||||
Gateway 0.6.1 started
|
||||
Gateway 0.7.0 started
|
||||
```
|
||||
|
||||
近期变更:
|
||||
|
||||
- `0.6.1`:pymsg 客户端增加 `retry_on_refused` 重试机制,与 Python 端 `pymsg.MsgClient.call(retry_on_refused=True)` 行为对齐——新增 `MsgConnectionRefusedException` 异常类(区分 ECONNREFUSED 与其他连接错误);`MsgClient::call()` 新增 `retry_on_refused`/`retry_interval` 参数,仅对连接拒绝重试,固定间隔 1s,节制日志(首次立即打印 + 每 5s 一次),其他异常(超时/连接重置/业务错误)立即抛出;`DbC::call_msg()` 启用 `retry_on_refused=true`(db 是硬依赖,启动顺序不确定时自动阻塞等待);`LogC::_send()` 显式传 `retry_on_refused=false`(日志是 fail-safe,db 不可用时静默丢弃,绝不阻塞后台线程);captcha/smtp 辅助服务保持默认不重试(fail-fast,请求线程中永久阻塞会耗尽线程池)。
|
||||
- `0.7.0`:配置体系迁移到 confs 统一配置服务——新增 `confs/configs/gateway.ini`(原 example.ini 内容 + `[Downstream]` 段配置下游服务地址);启动时默认从 confs(`CONFS_HOST`/`CONFS_PORT`,默认 `127.0.0.1:8010`)通过 pymsg `get` 路由拉取配置(`retry_on_refused=true`),`-i <path>` 保留为本地文件开发模式;`ConfIni` 增加 `fromString()`(从 ini 文本解析)、`env()`/`env_int()`(环境变量读取)、公开默认构造函数;环境变量覆盖支持 `LISTEN_ADDR`/`LISTEN_PORT`/`DB_HOST`/`DB_PORT`/`CAPTCHA_HOST`/`CAPTCHA_PORT`/`SMTP_HOST`/`SMTP_PORT`;配置完整性校验(`Server.address`/`port`、`Proxy.default`、`Downstream.db_host`/`db_port` 缺失则启动失败);`DbC` 增加 `init(host, port)` 支持可配置 db 地址(默认 127.0.0.1:8005),main.cpp 中所有硬编码 captcha/smtp 地址改为配置变量,`logs().set_endpoint()` 在启动时初始化;`_run.sh` 默认不传 `-i`,`--version/-v` 选项补全实现。
|
||||
- `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 内部错误细节不向外暴露)。
|
||||
|
||||
+7
-1
@@ -1,4 +1,10 @@
|
||||
#!/bin/bash
|
||||
# gateway 启动脚本
|
||||
# 默认从 confs 统一配置服务拉取配置(confs 需已启动)。
|
||||
# 若需使用本地 ini 文件(开发/调试),传入 -i <path>,例如:./_run.sh -i example.ini
|
||||
cd "$(dirname "$0")" || exit
|
||||
|
||||
# 等待依赖服务启动(confs/db)
|
||||
sleep 2
|
||||
exec ./build/gateway.out -i example.ini #1>/dev/null
|
||||
|
||||
exec ./build/gateway.out "$@"
|
||||
+37
-2
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
namespace property_tree = boost::property_tree;
|
||||
@@ -9,22 +12,54 @@ namespace qing {
|
||||
|
||||
class ConfIni {
|
||||
public:
|
||||
/// 默认构造(空配置,后续通过赋值覆盖,仅供 confs 加载流程内部使用)
|
||||
ConfIni() = default;
|
||||
|
||||
/// 从文件路径解析 ini(保留用于开发/调试模式)
|
||||
ConfIni(const std::string &filename) {
|
||||
if (filename.empty()) return; // 空文件名不报错,后续会通过赋值覆盖
|
||||
try {
|
||||
std::cout << "open and parse ini file: " << filename << std::endl;
|
||||
std::cout << "[conf] 从文件加载配置: " << filename << std::endl;
|
||||
property_tree::ini_parser::read_ini(filename, pt);
|
||||
}
|
||||
catch (const boost::property_tree::ini_parser_error& e) {
|
||||
std::cerr << "INI parsing error: " << e.what() << std::endl;
|
||||
std::cerr << "[conf] INI 解析错误: " << e.what() << std::endl;
|
||||
throw IniConfigureFileParseException("INI parsing error!");
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 ini 文本字符串解析(用于从 confs 拉取后解析)
|
||||
static ConfIni fromString(const std::string &ini_text) {
|
||||
ConfIni c;
|
||||
try {
|
||||
std::istringstream iss(ini_text);
|
||||
property_tree::ini_parser::read_ini(iss, c.pt);
|
||||
}
|
||||
catch (const boost::property_tree::ini_parser_error& e) {
|
||||
std::cerr << "[conf] INI 文本解析错误: " << e.what() << std::endl;
|
||||
throw IniConfigureFileParseException("INI parsing error!");
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T get(const std::string &path, const T &_defaut) {
|
||||
return pt.get<T>(path, _defaut);
|
||||
}
|
||||
|
||||
/// 读取环境变量,不存在则返回默认值
|
||||
static std::string env(const char *name, const std::string &default_val = "") {
|
||||
const char *v = std::getenv(name);
|
||||
return v ? std::string(v) : default_val;
|
||||
}
|
||||
|
||||
static int env_int(const char *name, int default_val) {
|
||||
const char *v = std::getenv(name);
|
||||
if (!v || !*v) return default_val;
|
||||
try { return std::stoi(v); }
|
||||
catch (...) { return default_val; }
|
||||
}
|
||||
|
||||
class IniConfigureFileParseException: public std::exception {
|
||||
public:
|
||||
explicit IniConfigureFileParseException(const std::string &msg)
|
||||
|
||||
+27
-16
@@ -10,6 +10,12 @@ namespace qing {
|
||||
* smtp/captcha 等其他下游的超时常量见 main.cpp。 */
|
||||
static constexpr double DEFAULT_TIMEOUT = 5.0;
|
||||
|
||||
/// 初始化 db 服务地址(启动时从配置设置,默认 127.0.0.1:8005)
|
||||
static void init(const std::string& host, int port) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
}
|
||||
|
||||
// 【保留】旧版 HTTP 接口:通过 HttpClient POST 到 dbv2 HTTP /execute 端点。
|
||||
// 保留用于兼容性参考,新代码优先使用 call_msg()(pymsg TCP 协议,更可靠)。
|
||||
static json::value call_http(json::value& params) {
|
||||
@@ -18,29 +24,34 @@ namespace qing {
|
||||
client.set_header("Content-Type: application/json");
|
||||
std::string payload = json::serialize(params);
|
||||
std::string res = "";
|
||||
client.post_stream_sync(_DBURL, payload, [&res](const auto& ret) {
|
||||
std::string url = "http://" + _host + ":" + std::to_string(_port) + "/execute";
|
||||
client.post_stream_sync(url, payload, [&res](const auto& ret) {
|
||||
res += ret;
|
||||
});
|
||||
return json::parse(res);
|
||||
}
|
||||
|
||||
// 新版 pymsg 接口:通过 MsgClient(TCP 长度前缀帧协议)调用 dbv2。
|
||||
// 自动添加 _route = "execute",返回 dbv2 pymsg 完整 JSON 响应
|
||||
// {"success":..., "data":[...]}。
|
||||
// 响应格式与 call_http 兼容,调用方可直接替换。
|
||||
// db 是强依赖,启用 retry_on_refused:db 未启动时阻塞等待,避免启动顺序问题。
|
||||
static json::value call_msg(const json::value& params,
|
||||
double timeout = DEFAULT_TIMEOUT) {
|
||||
json::value jv_route = params;
|
||||
jv_route.as_object()["_route"] = "execute";
|
||||
pmc::net::MsgClient client("127.0.0.1", 8005, "gateway");
|
||||
std::string resp = client.call(
|
||||
json::serialize(jv_route), timeout,
|
||||
/*retry_on_refused=*/true);
|
||||
return json::parse(resp);
|
||||
}
|
||||
// 自动添加 _route = "execute",返回 dbv2 pymsg 完整 JSON 响应
|
||||
// {"success":..., "data":[...]}。
|
||||
// 响应格式与 call_http 兼容,调用方可直接替换。
|
||||
// db 是强依赖,启用 retry_on_refused:db 未启动时阻塞等待,避免启动顺序问题。
|
||||
static json::value call_msg(const json::value& params,
|
||||
double timeout = DEFAULT_TIMEOUT) {
|
||||
json::value jv_route = params;
|
||||
jv_route.as_object()["_route"] = "execute";
|
||||
pmc::net::MsgClient client(_host, _port, "gateway");
|
||||
std::string resp = client.call(
|
||||
json::serialize(jv_route), timeout,
|
||||
/*retry_on_refused=*/true);
|
||||
return json::parse(resp);
|
||||
}
|
||||
|
||||
static const std::string& host() { return _host; }
|
||||
static int port() { return _port; }
|
||||
|
||||
private:
|
||||
static constexpr const char* _DBURL = "http://127.0.0.1:8005/execute";
|
||||
static inline std::string _host = "127.0.0.1";
|
||||
static inline int _port = 8005;
|
||||
};
|
||||
}
|
||||
+134
-32
@@ -28,7 +28,7 @@ using namespace qing;
|
||||
namespace json = boost::json;
|
||||
namespace po = boost::program_options;
|
||||
|
||||
constexpr const char *_VER = "0.6.1";
|
||||
constexpr const char *_VER = "0.7.0";
|
||||
|
||||
//////////////////////////////////
|
||||
#include <sstream>
|
||||
@@ -339,6 +339,17 @@ static int _TOKEN_MAX_REFRESH = 10; // Token 生命周期内最大刷新次数
|
||||
static std::vector<std::string> _CORS_ORIGINS; // CORS 允许的 Origin 白名单,可由 [CORS] allowed_origins 配置
|
||||
static std::vector<std::string> _ADMIN_USERS; // 管理员账号白名单,可由 [Admin] users 配置(分号分隔),登录时写入 Token payload 的 is_admin
|
||||
|
||||
/* 下游 pymsg 服务地址(从 confs [Downstream] 段加载,可被环境变量覆盖) */
|
||||
static std::string _DB_HOST = "127.0.0.1";
|
||||
static int _DB_PORT = 8005;
|
||||
static std::string _CAPTCHA_HOST = "127.0.0.1";
|
||||
static int _CAPTCHA_PORT = 8009;
|
||||
static std::string _SMTP_HOST = "127.0.0.1";
|
||||
static int _SMTP_PORT = 8008;
|
||||
/* confs 引导配置(不依赖 confs 本身,环境变量 CONFS_HOST/CONFS_PORT 覆盖,默认 127.0.0.1:8010) */
|
||||
static std::string _CONFS_HOST = "127.0.0.1";
|
||||
static int _CONFS_PORT = 8010;
|
||||
|
||||
/* WebSocket 代理路由:path -> ws://backend-host:port/target
|
||||
* 仅管理员用户可访问,握手阶段由 ws_proxy 回调验签 Token 中的 is_admin */
|
||||
static std::unordered_map<std::string, std::string> _WS_PROXY_ROUTES;
|
||||
@@ -414,7 +425,7 @@ bool verify_captcha(const std::string& token, const std::string& answer) {
|
||||
* 与内置 verify_captcha 并存:登录/注册可任选其一接入。 */
|
||||
bool verify_captcha_remote(const std::string& token, const std::string& answer) {
|
||||
try {
|
||||
pmc::net::MsgClient client("127.0.0.1", 8009, "gateway");
|
||||
pmc::net::MsgClient client(_CAPTCHA_HOST, _CAPTCHA_PORT, "gateway");
|
||||
json::value jv_req = {
|
||||
{"_route", "verify"},
|
||||
{"token", token},
|
||||
@@ -430,11 +441,11 @@ bool verify_captcha_remote(const std::string& token, const std::string& answer)
|
||||
}
|
||||
}
|
||||
|
||||
/* 调用 smtp 服务(pymsg/TCP :8008)校验邮箱验证码。
|
||||
/* 调用 smtp 服务(pymsg/TCP)校验邮箱验证码。
|
||||
* 验证码一次性消费;服务不可用或响应异常一律视为校验失败(fail-closed)。 */
|
||||
bool verify_email_code_remote(const std::string& email, const std::string& code) {
|
||||
try {
|
||||
pmc::net::MsgClient client("127.0.0.1", 8008, "gateway");
|
||||
pmc::net::MsgClient client(_SMTP_HOST, _SMTP_PORT, "gateway");
|
||||
json::value jv_req = {
|
||||
{"_route", "verify_email"},
|
||||
{"email", email},
|
||||
@@ -475,7 +486,7 @@ int main(int argc, char **argv) {
|
||||
desc.add_options()
|
||||
("help,h", "output help message")
|
||||
("version,v", "output version message")
|
||||
("input,i", po::value<std::string>(), "input a configure text file")
|
||||
("input,i", po::value<std::string>(), "(可选) 指定本地 ini 配置文件路径,跳过 confs 拉取(开发/调试用)")
|
||||
;
|
||||
|
||||
|
||||
@@ -501,21 +512,75 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 异常:没输入配置文件 */
|
||||
if (!vm.count("input")) {
|
||||
std::cerr << "Please input configure file via -i" << std::endl;
|
||||
return 1;
|
||||
/* 分支:输出版本号 */
|
||||
if (vm.count("version")) {
|
||||
std::cout << "Gateway " << _VER << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
{
|
||||
try {
|
||||
/* ==================== 配置加载(两层:confs 强依赖 → 环境变量覆盖) ====================
|
||||
* 与 Python 服务(dnfs/db/static)保持一致的加载模式:
|
||||
* 1. 引导配置:confs 地址从 CONFS_HOST/CONFS_PORT 环境变量读取,缺省 127.0.0.1:8010
|
||||
* 2. 从 confs 拉取 gateway.ini 全文(retry_on_refused=true 等待 confs 就绪)
|
||||
* - 若指定了 -i 参数,则跳过 confs,直接读取本地 ini 文件(开发/调试模式)
|
||||
* 3. 用 boost::property_tree 解析 ini
|
||||
* 4. 环境变量覆盖(最高优先级,Docker 环境需要服务名而非 127.0.0.1)
|
||||
* 5. 配置完整性校验:必需字段缺失则启动失败
|
||||
* ==================================================================================== */
|
||||
|
||||
ConfIni cf(vm["input"].as<std::string>());
|
||||
{
|
||||
ConfIni cf; // 默认空构造,后续根据模式赋值
|
||||
|
||||
/* 1. 引导配置:confs 连接地址(不依赖 confs 本身,避免循环依赖) */
|
||||
_CONFS_HOST = ConfIni::env("CONFS_HOST", "127.0.0.1");
|
||||
_CONFS_PORT = ConfIni::env_int("CONFS_PORT", 8010);
|
||||
|
||||
try {
|
||||
if (vm.count("input")) {
|
||||
/* 开发模式:-i 指定本地文件,跳过 confs */
|
||||
std::string ini_path = vm["input"].as<std::string>();
|
||||
cf = ConfIni(ini_path);
|
||||
std::cout << "[gateway] 配置来源: 本地文件 " << ini_path << std::endl;
|
||||
} else {
|
||||
/* 生产模式:从 confs 拉取 gateway.ini */
|
||||
std::cout << "[gateway] 从 confs (" << _CONFS_HOST << ":" << _CONFS_PORT
|
||||
<< ") 拉取 gateway.ini ..." << std::endl;
|
||||
pmc::net::MsgClient confs_client(_CONFS_HOST, _CONFS_PORT, "gateway");
|
||||
json::object req;
|
||||
req["_route"] = "get";
|
||||
req["name"] = "gateway.ini";
|
||||
std::string resp_str = confs_client.call(
|
||||
json::serialize(req), /*timeout=*/5.0,
|
||||
/*retry_on_refused=*/true);
|
||||
auto resp = json::parse(resp_str).as_object();
|
||||
if (!resp.at("success").as_bool()) {
|
||||
std::string err = "unknown";
|
||||
if (auto* ev = resp.if_contains("error")) {
|
||||
if (ev->is_string()) err = ev->as_string().c_str();
|
||||
}
|
||||
std::cerr << "[gateway] 致命错误: 从 confs 获取 gateway.ini 失败: " << err << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::string ini_content = resp.at("data").as_object().at("content").as_string().c_str();
|
||||
cf = ConfIni::fromString(ini_content);
|
||||
std::cout << "[gateway] 配置来源: confs (gateway.ini @ "
|
||||
<< _CONFS_HOST << ":" << _CONFS_PORT << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
catch (ConfIni::IniConfigureFileParseException &e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << "ini parse error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
catch (const std::exception &e) {
|
||||
std::cerr << "[gateway] 致命错误: 配置加载失败: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 2. 从 ini 读取配置 */
|
||||
_ADDR = cf.get<std::string>("Server.address", "");
|
||||
_PORT = cf.get<int> ("Server.port", -1);
|
||||
_TLS = cf.get<bool> ("Server.tls", false);
|
||||
_PORT = cf.get<int>("Server.port", -1);
|
||||
_TLS = cf.get<bool>("Server.tls", false);
|
||||
_DEFT_URL = cf.get<std::string>("Proxy.default", "");
|
||||
_OPEN_URL = split(cf.get<std::string>("Proxy.opened", ""), ';');
|
||||
_DB_PATH = cf.get<std::string>("Database.path", "");
|
||||
@@ -525,8 +590,15 @@ int main(int argc, char **argv) {
|
||||
_CORS_ORIGINS = split(cf.get<std::string>("CORS.allowed_origins", ""), ';');
|
||||
_ADMIN_USERS = split(cf.get<std::string>("Admin.users", ""), ';');
|
||||
|
||||
/* [WsProxy] WebSocket 代理路由:格式 /path=ws://host:port/target;...
|
||||
* 仅管理员(Token 中 is_admin=true)可访问,用于 aibox 等内部 WS 服务的网关暴露 */
|
||||
/* 下游服务地址 */
|
||||
_DB_HOST = cf.get<std::string>("Downstream.db_host", "127.0.0.1");
|
||||
_DB_PORT = cf.get<int>("Downstream.db_port", 8005);
|
||||
_CAPTCHA_HOST = cf.get<std::string>("Downstream.captcha_host", "127.0.0.1");
|
||||
_CAPTCHA_PORT = cf.get<int>("Downstream.captcha_port", 8009);
|
||||
_SMTP_HOST = cf.get<std::string>("Downstream.smtp_host", "127.0.0.1");
|
||||
_SMTP_PORT = cf.get<int>("Downstream.smtp_port", 8008);
|
||||
|
||||
/* [WsProxy] WebSocket 代理路由:格式 /path=ws://host:port/target;... */
|
||||
{
|
||||
std::string ws_cfg = cf.get<std::string>("WsProxy.routes", "");
|
||||
if (!ws_cfg.empty()) {
|
||||
@@ -536,7 +608,6 @@ int main(int argc, char **argv) {
|
||||
if (eq != std::string::npos) {
|
||||
std::string path = entry.substr(0, eq);
|
||||
std::string url = entry.substr(eq + 1);
|
||||
// 去除首尾空白
|
||||
auto trim = [](std::string& s) {
|
||||
while (!s.empty() && std::isspace((unsigned char)s.front())) s.erase(s.begin());
|
||||
while (!s.empty() && std::isspace((unsigned char)s.back())) s.pop_back();
|
||||
@@ -549,22 +620,53 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. 环境变量覆盖(优先级最高,Docker 环境使用服务名而非 127.0.0.1) */
|
||||
std::string env_listen_addr = ConfIni::env("LISTEN_ADDR");
|
||||
if (!env_listen_addr.empty()) _ADDR = env_listen_addr;
|
||||
int env_listen_port = ConfIni::env_int("LISTEN_PORT", -1);
|
||||
if (env_listen_port > 0) _PORT = env_listen_port;
|
||||
|
||||
std::string env_db_host = ConfIni::env("DB_HOST");
|
||||
if (!env_db_host.empty()) _DB_HOST = env_db_host;
|
||||
int env_db_port = ConfIni::env_int("DB_PORT", -1);
|
||||
if (env_db_port > 0) _DB_PORT = env_db_port;
|
||||
|
||||
std::string env_captcha_host = ConfIni::env("CAPTCHA_HOST");
|
||||
if (!env_captcha_host.empty()) _CAPTCHA_HOST = env_captcha_host;
|
||||
int env_captcha_port = ConfIni::env_int("CAPTCHA_PORT", -1);
|
||||
if (env_captcha_port > 0) _CAPTCHA_PORT = env_captcha_port;
|
||||
|
||||
std::string env_smtp_host = ConfIni::env("SMTP_HOST");
|
||||
if (!env_smtp_host.empty()) _SMTP_HOST = env_smtp_host;
|
||||
int env_smtp_port = ConfIni::env_int("SMTP_PORT", -1);
|
||||
if (env_smtp_port > 0) _SMTP_PORT = env_smtp_port;
|
||||
|
||||
/* 4. 配置完整性校验 */
|
||||
if (_ADDR.empty() || _PORT == -1) {
|
||||
std::cerr << "[gateway] 致命错误: 配置项 [Server] address/port 缺失"
|
||||
"(ini 中未配置且无 LISTEN_ADDR/LISTEN_PORT 环境变量覆盖)" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/* 常规输出 */
|
||||
catch (ConfIni::IniConfigureFileParseException &e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << "ini format input file error" << std::endl;
|
||||
if (_DEFT_URL.empty()) {
|
||||
std::cerr << "[gateway] 致命错误: 配置项 [Proxy] default 缺失" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (_DB_HOST.empty() || _DB_PORT <= 0) {
|
||||
std::cerr << "[gateway] 致命错误: 配置项 [Downstream] db_host/db_port 缺失" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (_ADDR == "" || _PORT == -1) {
|
||||
std::cerr << "Please setup addree and port in Serve section in configure file";
|
||||
std::cerr << std::endl;
|
||||
return 0;
|
||||
}
|
||||
/* 初始化下游客户端地址 */
|
||||
DbC::init(_DB_HOST, _DB_PORT);
|
||||
logs().set_endpoint(_DB_HOST, _DB_PORT);
|
||||
|
||||
std::cout << "[gateway] 监听: " << _ADDR << ":" << _PORT
|
||||
<< " (TLS: " << (_TLS ? "on" : "off") << ")" << std::endl;
|
||||
std::cout << "[gateway] 下游服务: db=" << _DB_HOST << ":" << _DB_PORT
|
||||
<< ", captcha=" << _CAPTCHA_HOST << ":" << _CAPTCHA_PORT
|
||||
<< ", smtp=" << _SMTP_HOST << ":" << _SMTP_PORT << std::endl;
|
||||
|
||||
// 根据 Server.tls 选择 HTTPS(带证书)或 HTTP(无证书)构造函数
|
||||
// HTTP 模式用于 WAF 终止 TLS 后的内部转发链路
|
||||
@@ -1373,7 +1475,7 @@ http::response<http::string_body> handleCaptchaImage(
|
||||
res.version(req.version());
|
||||
|
||||
/* 调 captcha 服务(pymsg/TCP),连接失败/超时视为 502 */
|
||||
pmc::net::MsgClient client("127.0.0.1", 8009, "gateway");
|
||||
pmc::net::MsgClient client(_CAPTCHA_HOST, _CAPTCHA_PORT, "gateway");
|
||||
std::string resp;
|
||||
try {
|
||||
resp = client.call(R"({"_route":"image"})", _TIMEOUT_CAPTCHA);
|
||||
@@ -1571,7 +1673,7 @@ http::response<http::string_body> handleSendEmailCode(
|
||||
int retry_after = 0; // smtp 同邮箱发送冷却的剩余秒数,0 表示非冷却拒绝
|
||||
std::string smtp_err; // 冷却拒绝时 smtp 返回的提示文案(仅冷却场景透传客户端)
|
||||
try {
|
||||
pmc::net::MsgClient client("127.0.0.1", 8008, "gateway");
|
||||
pmc::net::MsgClient client(_SMTP_HOST, _SMTP_PORT, "gateway");
|
||||
json::value jv_req = {
|
||||
{"_route", "send_email"},
|
||||
{"email", email}
|
||||
|
||||
Reference in new issue
Block a user