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:
qing committed 2026-08-01 13:55:18 +08:00
1 parent de58c0487a
commit 9ccb9cbdca
6 files changed
+289 -108

No files matched your search

+31 -57
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}