refactor(ca): v1.1.0 配置体系重构为纯 CLI 传递

- 移除 confs 依赖:删 Conf.hpp 与 confs/configs/ca.ini,配置加载改为 CLI > 环境变量 > 默认值三层(与 dnfs2 同模式)
- 新增 16 个配置选项,env 以 CA_* 为主并兼容旧名(LISTEN_ADDR/UPSTREAM_GATEWAY/DB_* 等),启动时配置完整性校验
- 原 ca.ini 配置值迁入 core/services.yaml ca 条目 args
- 修复伪造 Token 触发 500:verify_token 统一捕获 RSA 验签异常,按验签失败返回 401(符合黑盒 B3/B4 契约)
- 同步更新 ca/README.md 与根 README.md
This commit is contained in:
qing committed 2026-08-10 06:13:43 +00:00
1 parent 8b6eb47c80
commit 9d548c064f
8 files changed
+244 -302

No files matched your search

+2 -2
View File
@@ -1,4 +1,4 @@
VERSION := 1.0.0
VERSION := 1.1.0
APP_NAME := ca
INSTL_DIR := /usr/local/bin
CPP := g++
@@ -13,7 +13,7 @@ $(shell mkdir -p $(BUILD_DIR))
all: $(BUILD_DIR)/$(APP_NAME).out
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
$(CPP) $^ $(LIBS) $(MY_LIBS) -o $@
$(BUILD_DIR)/main.o: src/main.cpp inc/DbC.hpp inc/Conf.hpp inc/ExpBase.hpp inc/LocalTime.hpp
$(BUILD_DIR)/main.o: src/main.cpp inc/DbC.hpp inc/ExpBase.hpp inc/LocalTime.hpp
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
clear:
@rm -rvf build/
+31 -16
View File
@@ -21,7 +21,7 @@ UniServe Framework 的中央认证服务(Central Auth),C++20 实现,v1.0
- 无 `Target-Url` 头 = 公开请求:剥离伪造身份头(`X-Auth-User`/`X-Auth-Admin`)后直转。
- 有 `Target-Url` 头 = 受保护请求:本地验签(RSA + 过期 + jti 黑名单 + 用户版本号),通过后注入 `X-Auth-User`(管理员追加 `X-Auth-Admin: 1`)与 `Authorization: Bearer` 再转发。
- 上游不可达/超时统一返回 `502 Bad Gateway`(区别于 CA 自身故障的 500)。
- **WebSocket 反向代理(仅管理员)**:`[WsProxy]` 段配置的路径在 HTTP Upgrade 握手时校验 Token(`is_admin=true`),通过后透明桥接到后端 ws:// 或 wss:// 服务。
- **WebSocket 反向代理(仅管理员)**:`--ws-proxy-routes` 配置的路径在 HTTP Upgrade 握手时校验 Token(`is_admin=true`),通过后透明桥接到后端 ws:// 或 wss:// 服务。
- **CAPTCHA 代理**:`GET /captcha/image` 代理 captcha2 签发图形验证码(PNG + `X-Captcha-Token` 响应头,10 次/分钟/IP,禁缓存)。
- **邮箱验证码**:`/register/send_code`、`/reset/send_code` 发送 6 位验证码邮件(10 分钟有效);`/reset_password` 重置密码(防枚举恒成功 + 改密后版本号吊销全端 Token)。
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟、发验证码 5 次/10 分钟(IP 级)+ 3 次/小时/10 次/天(邮箱级),按 `X-Forwarded-For` 客户端 IP 计数(由 WAF 注入),超限一律返回 429。
@@ -48,8 +48,8 @@ gateway 侧仅做**存在性不变量**检查(显式 `Target-Url` 请求必须
| 常数时间比较 | PBKDF2 验证用 `diff |= hash[i] ^ expected[i]` 防 timing attack |
| 防用户枚举 | 登录对不存在用户执行 dummy PBKDF2 拉平时序;重置密码恒返回成功 |
| CAPTCHA | 统一走 captcha2 远端校验(Token 一次性消费);`/login` 强制 |
| TLS 可选 | `Server.tls = off` 时纯 HTTP(配合 WAF),`on` 时用 `full_chain.crt` / `server.key` |
| 下游容错 | pymsg 客户端 `retry_on_refused`:db/confs 未就绪时阻塞等待;日志客户端 fail-safe 静默丢弃 |
| TLS 可选 | 缺省(不传 `--tls`)纯 HTTP(配合 WAF),`--tls` 时用 `full_chain.crt` / `server.key` |
| 下游容错 | pymsg 客户端 `retry_on_refused`:db 未就绪时阻塞等待;日志客户端 fail-safe 静默丢弃 |
## 接口
@@ -93,32 +93,46 @@ Target-Url: http://127.0.0.1:8006 // 可选;携带即视为
- 有 `Target-Url`:验签通过后注入身份头转发;`Target-Url` 白名单由 gateway 侧 `Proxy.opened` 裁决(403)。
- 上游故障:`502 {"success": false, "message": "Bad Gateway"}`。
### WebSocket 代理(`[WsProxy]` 配置的路径)
### WebSocket 代理(`--ws-proxy-routes` 配置的路径)
握手阶段从 `Authorization: Bearer` / `Cookie: q4_token` 提取 Token,验签且 `is_admin=true` 才升级连接并桥接后端;否则拒绝。路由精确匹配,优先级高于 HTTP 通配 `*`。
## 配置
两层加载:**confs 统一配置服务**(启动时 pymsg `get` 拉取 `ca.ini`,`retry_on_refused=true`)→ **环境变量覆盖**(最高优先级)。
v1.1.0 起配置**全部经 CLI 传递**,不再依赖 confs 统一配置服务(与 dnfs2 同模式)。三层优先级:**CLI 参数 > 环境变量 > 代码默认值**;部署侧由 [`core/services.yaml`](../core/services.yaml) 的 `args` 声明完整传入。
配置文件:[confs/configs/ca.ini](../confs/configs/ca.ini)。
| CLI 选项 | 环境变量(CA_ 前缀优先,兼容旧名) | 默认值 |
|----------|-----------------------------------|--------|
| `--host` | `CA_HOST` / `LISTEN_ADDR` | `127.0.0.1` |
| `--port` | `CA_PORT` / `LISTEN_PORT` | `8443` |
| `--tls` | `CA_TLS=on` | 关(纯 HTTP) |
| `--upstream-gateway` | `CA_UPSTREAM_GATEWAY` / `UPSTREAM_GATEWAY` | `http://127.0.0.1:8445` |
| `--token-keep-time` | `CA_TOKEN_KEEP_TIME` | `3600` |
| `--token-refresh-grace-multiplier` | `CA_TOKEN_REFRESH_GRACE_MULTIPLIER` | `2` |
| `--token-max-refresh` | `CA_TOKEN_MAX_REFRESH` | `10` |
| `--cors-origins` | `CA_CORS_ORIGINS` | 空 |
| `--admin-users` | `CA_ADMIN_USERS` | 空 |
| `--ws-proxy-routes` | `CA_WS_PROXY_ROUTES` | 空 |
| `--db-host` / `--db-port` | `CA_DB_HOST`/`DB_HOST` / `CA_DB_PORT`/`DB_PORT` | `127.0.0.1:8005` |
| `--captcha-host` / `--captcha-port` | `CA_CAPTCHA_HOST`/`CAPTCHA_HOST` / `CA_CAPTCHA_PORT`/`CAPTCHA_PORT` | `127.0.0.1:8009` |
| `--smtp-host` / `--smtp-port` | `CA_SMTP_HOST`/`SMTP_HOST` / `CA_SMTP_PORT`/`SMTP_PORT` | `127.0.0.1:8008` |
| `--instance` | — | `ca` |
| `--log-level` | — | `info` |
- `Server.address`/`port`/`tls`:监听地址/端口/TLS(默认 `127.0.0.1:8443`,env `LISTEN_ADDR`/`LISTEN_PORT`)。
- `Upstream.gateway`:唯一上游 gateway 地址(默认 `http://127.0.0.1:8445`,env `UPSTREAM_GATEWAY`)。
- `Token.*`:`keep_time` / `refresh_grace_multiplier` / `max_refresh`。
- `Admin.users`:管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 `is_admin=true`。
- `CORS.allowed_origins`:已读取但尚未用于预检响应(见 [`ca_SECURITY_AUDIT.md`](../docs/security/ca_SECURITY_AUDIT.md) P2-3)。
- `WsProxy.routes`:`/path=ws://host:port/target`,分号分隔。
- `Downstream.*`:db/captcha/smtp 地址(env `DB_*`/`CAPTCHA_*`/`SMTP_*`/`CONFS_*`)。
- `--admin-users`:管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 `is_admin=true`。
- `--cors-origins`:已读取但尚未用于预检响应(见 [`ca_SECURITY_AUDIT.md`](../docs/security/ca_SECURITY_AUDIT.md) P2-3)。
- `--ws-proxy-routes`:`/path=ws://host:port/target`,分号分隔多条。
- 旧环境变量名(`LISTEN_ADDR` 等)保留兼容,供 Docker 部署用服务名覆盖 `127.0.0.1`。
- 启动时做配置完整性校验:监听地址/端口、上游 gateway、db 地址非法则启动失败。
## 编译与运行
```bash
bash _set.sh # make clean && make
bash _run.sh # 从 confs 拉取配置启动(先 sleep 2 等待 confs/db)
bash _run.sh # 纯 CLI 配置启动("$@" 透传,可临时覆盖任意配置项)
```
命令行参数:`-h/--help`、`-v/--version`。日志经 LogC 异步批量推送 db:8005 `Logs` 表(service 名 `ca`)。
命令行参数:完整选项见 `--help`(见上表);`-h/--help`、`-v/--version`。日志经 LogC 异步批量推送 db:8005 `Logs` 表(service 名 `ca`)。
## 目录结构
@@ -129,7 +143,6 @@ ca/
├── inc/
│ ├── DbC.hpp # db 服务客户端(pymsg,retry_on_refused=true;client 名 "ca")
│ ├── LogC.hpp # 统一日志客户端(异步批量,fail-safe;client 名 "ca-log")
│ ├── Conf.hpp # ConfIni:ini 解析 + env 覆盖
│ ├── ExpBase.hpp # 异常基类(带 source_location)
│ └── LocalTime.hpp # 本地时间工具
├── Makefile
@@ -139,4 +152,6 @@ ca/
## 版本
`1.1.0`:配置体系重构——彻底移除 confs 依赖(删 `Conf.hpp` 与 `confs/configs/ca.ini`),配置全部经 CLI 传递(CLI > 环境变量 > 默认值三层),原 ca.ini 配置值迁入 `core/services.yaml` 的 ca 条目 args;修复伪造 Token 触发 500(RSA 验签异常未捕获,现统一返回 401,符合黑盒测试 B3/B4 契约)。
`1.0.0`:认证独立化首发——自 gateway v0.8.0 整体搬迁全部认证资产(逐字保留实现),新增前置转发层(固定上游 + strip-then-set 身份头契约 + 上游 502 语义)。原 gateway 同步瘦身为 v0.9.0 纯转发服务。
+3 -2
View File
@@ -1,7 +1,8 @@
#!/bin/bash
# ca 启动脚本
# 配置统一由 confs 服务管理(confs/configs/ca.ini),
# 启动时从 confs(CONFS_HOST:CONFS_PORT,默认 127.0.0.1:8010)拉取配置。
# 配置完全经 CLI 参数传递(v1.1.0 起不再依赖 confs),
# 部署侧由 core/services.yaml 的 args 声明完整传入;
# "$@" 透传允许命令行临时覆盖任意配置项。
cd "$(dirname "$0")" || exit
# sleep 已移至 services.yaml 的 sleep_before 字段统一管理
-77
View File
@@ -1,77 +0,0 @@
#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;
using ptree = property_tree::ptree;
namespace qing {
class ConfIni {
public:
/// 默认构造(空配置,后续通过赋值覆盖,仅供 confs 加载流程内部使用)
ConfIni() = default;
/// 从文件路径解析 ini(保留用于开发/调试模式)
ConfIni(const std::string &filename) {
if (filename.empty()) return; // 空文件名不报错,后续会通过赋值覆盖
try {
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 << "[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)
: message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
private:
std::string message;
};
private:
ptree pt;
};
}
+167 -149
View File
@@ -10,6 +10,7 @@
#include <typeinfo>
#include <memory>
#include <csignal>
#include <cstdlib>
#include <random>
#include <regex>
#include <thread>
@@ -23,7 +24,6 @@
#include <openssl/kdf.h>
#include "ExpBase.hpp"
#include "LocalTime.hpp"
#include "Conf.hpp"
#include "DbC.hpp"
#include <us/LogC.hpp>
@@ -32,7 +32,7 @@ using namespace qing;
namespace json = boost::json;
namespace po = boost::program_options;
constexpr const char *_VER = "1.0.0";
constexpr const char *_VER = "1.1.0";
//////////////////////////////////
#include <sstream>
@@ -376,28 +376,41 @@ std::string extract_token_from_request(const http::request<http::string_body>& r
// 参数 raw_token:"base64data_signature" 格式(不含 "Bearer " 前缀)
TokenInfo verify_token(const std::string& raw_token);
/* configure items */
static std::string _ADDR;
static int _PORT;
static bool _TLS = false;
/* 上游 gateway 地址(前置拓扑:ca 认证后转发至此),由 [Upstream] gateway 配置 */
static std::string _UPSTREAM_GATEWAY = "http://127.0.0.1:8445";
static int _TOKEN_KEEP_TIME = 3600; // Token 有效期(秒),可由 [Token] keep_time 配置
static int _TOKEN_REFRESH_GRACE_MULTIPLIER = 2; // Token 刷新宽限期倍数,过期后 iat + keep_time*倍数 窗口内仍可刷新
static int _TOKEN_MAX_REFRESH = 10; // Token 生命周期内最大刷新次数,可由 [Token] max_refresh 配置
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
/* 环境变量读取辅助(v1.1.0 配置体系 CLI > env > 默认值) */
static std::string env_or(const char* name, const std::string& def = "") {
const char* v = std::getenv(name);
return (v && *v) ? std::string(v) : def;
}
static int env_int_or(const char* name, int def) {
const char* v = std::getenv(name);
if (!v || !*v) return def;
try { return std::stoi(v); }
catch (...) { return def; }
}
static bool env_flag(const char* name) {
std::string v = env_or(name);
return v == "1" || v == "on" || v == "true" || v == "yes";
}
/* 下游 pymsg 服务地址(从 confs [Downstream] 段加载,可被环境变量覆盖) */
/* configure items(默认值即声明处初值,env 与 CLI 依次覆盖) */
static std::string _ADDR = "127.0.0.1";
static int _PORT = 8443;
static bool _TLS = false;
/* 上游 gateway 地址(前置拓扑:ca 认证后转发至此) */
static std::string _UPSTREAM_GATEWAY = "http://127.0.0.1:8445";
static int _TOKEN_KEEP_TIME = 3600; // Token 有效期(秒)
static int _TOKEN_REFRESH_GRACE_MULTIPLIER = 2; // Token 刷新宽限期倍数,过期后 iat + keep_time*倍数 窗口内仍可刷新
static int _TOKEN_MAX_REFRESH = 10; // Token 生命周期内最大刷新次数
static std::vector<std::string> _CORS_ORIGINS; // CORS 允许的 Origin 白名单(分号分隔)
static std::vector<std::string> _ADMIN_USERS; // 管理员账号白名单(分号分隔),登录时写入 Token payload 的 is_admin
/* 下游 pymsg 服务地址 */
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 */
@@ -620,9 +633,16 @@ TokenInfo verify_token(const std::string& raw_token) {
return info;
}
// RSA 验签
// RSA 验签(Verify 对非法签名格式(如奇数长度 hex)会抛异常,
// 此处统一捕获按验签失败处理,避免伪造 Token 触发 500——审计 B3/B4 契约要求 401)
auto publ = MyRSA::Public_Key::from_pem(_KEYGEN->get_public_key_pem());
if (!publ.Verify(auth, sign)) {
bool sig_ok = false;
try {
sig_ok = publ.Verify(auth, sign);
} catch (const std::exception&) {
sig_ok = false;
}
if (!sig_ok) {
info.http_status = 401;
return info;
}
@@ -767,14 +787,27 @@ void set_signal_action() {
/* -------- 入口 --------*/
int main(int argc, char **argv) {
/* 首先处理命令行参数 */
/* 首先处理命令行参数(v1.1.0 起配置全部经 CLI 传递) */
po::options_description desc("cpp language web ca (central auth)");
desc.add_options()
("help,h", "output help message")
("version,v", "output version message")
("host", po::value<std::string>(), "绑定地址(默认 127.0.0.1),覆盖 [Server] address")
("port", po::value<int>(), "监听端口(默认 8443),覆盖 [Server] port")
("config", po::value<std::string>(), "confs 拉取的配置名(默认 ca.ini)")
("host", po::value<std::string>(), "绑定地址(默认 127.0.0.1,env CA_HOST/LISTEN_ADDR)")
("port", po::value<int>(), "监听端口(默认 8443,env CA_PORT/LISTEN_PORT)")
("tls", "启用 HTTPS(证书 full_chain.crt/server.key;缺省纯 HTTP,env CA_TLS=on)")
("upstream-gateway", po::value<std::string>(), "上游 gateway 地址(默认 http://127.0.0.1:8445,env CA_UPSTREAM_GATEWAY/UPSTREAM_GATEWAY)")
("token-keep-time", po::value<int>(), "Token 有效期秒数(默认 3600,env CA_TOKEN_KEEP_TIME)")
("token-refresh-grace-multiplier", po::value<int>(), "Token 刷新宽限期倍数(默认 2,env CA_TOKEN_REFRESH_GRACE_MULTIPLIER)")
("token-max-refresh", po::value<int>(), "Token 最大刷新次数(默认 10,env CA_TOKEN_MAX_REFRESH)")
("cors-origins", po::value<std::string>(), "CORS Origin 白名单,分号分隔(env CA_CORS_ORIGINS)")
("admin-users", po::value<std::string>(), "管理员账号白名单,分号分隔(env CA_ADMIN_USERS)")
("ws-proxy-routes", po::value<std::string>(), "WS 代理路由 /path=ws://host:port,分号分隔(env CA_WS_PROXY_ROUTES)")
("db-host", po::value<std::string>(), "db 服务地址(默认 127.0.0.1,env CA_DB_HOST/DB_HOST)")
("db-port", po::value<int>(), "db 服务端口(默认 8005,env CA_DB_PORT/DB_PORT)")
("captcha-host", po::value<std::string>(), "captcha 服务地址(默认 127.0.0.1,env CA_CAPTCHA_HOST/CAPTCHA_HOST)")
("captcha-port", po::value<int>(), "captcha 服务端口(默认 8009,env CA_CAPTCHA_PORT/CAPTCHA_PORT)")
("smtp-host", po::value<std::string>(), "smtp 服务地址(默认 127.0.0.1,env CA_SMTP_HOST/SMTP_HOST)")
("smtp-port", po::value<int>(), "smtp 服务端口(默认 8008,env CA_SMTP_PORT/SMTP_PORT)")
("log-level", po::value<std::string>(), "日志级别 debug|info|warn|error(默认 info)")
("instance", po::value<std::string>(), "实例名(注册中心用,默认 ca)")
;
@@ -808,133 +841,98 @@ int main(int argc, char **argv) {
return 0;
}
/* ==================== 配置加载(两层:confs 强依赖 → 环境变量覆盖) ====================
* 与 Python 服务(dnfs/db/static)保持一致的加载模式:
* 1. 引导配置:confs 地址从 CONFS_HOST/CONFS_PORT 环境变量读取,缺省 127.0.0.1:8010
* 2. 从 confs 拉取 ca.ini 全文(retry_on_refused=true 等待 confs 就绪)
* 3. 用 boost::property_tree 解析 ini
* 4. 环境变量覆盖(最高优先级,Docker 环境需要服务名而非 127.0.0.1)
* 5. 配置完整性校验:必需字段缺失则启动失败
* 不再支持本地 ini 文件回退(与 static 模块一致,配置统一由 confs 管理)。
* ==================================================================================== */
/* ==================== 配置加载(v1.1.0:CLI > 环境变量 > 默认值) ====================
* 配置完全经命令行传递(与 dnfs2 同模式),不再依赖 confs 统一配置服务:
* 1. 默认值:各配置项声明处初值(与原 ca.ini 默认值一致)
* 2. 环境变量覆盖:CA_ 前缀为主,兼容旧名(LISTEN_ADDR / LISTEN_PORT / UPSTREAM_GATEWAY / DB_x / CAPTCHA_x / SMTP_x,Docker 环境需要服务名而非 127.0.0.1)
* 3. CLI 参数覆盖:最高优先级
* 4. 配置完整性校验:必需字段非法则启动失败
* 部署侧由 core/services.yaml 的 args 声明完整传入配置。
* ============================================================================== */
{
/* 1. 引导配置:confs 连接地址(不依赖 confs 本身,避免循环依赖) */
_CONFS_HOST = ConfIni::env("CONFS_HOST", "127.0.0.1");
_CONFS_PORT = ConfIni::env_int("CONFS_PORT", 8010);
/* --config 覆盖 confs 拉取的配置名(在 confs 拉取前读取) */
std::string confs_config_name = "ca.ini";
if (vm.count("config")) {
confs_config_name = vm["config"].as<std::string>();
}
ConfIni cf;
try {
/* 从 confs 拉取配置 */
std::cout << "[ca] 从 confs (" << _CONFS_HOST << ":" << _CONFS_PORT
<< ") 拉取 " << confs_config_name << " ..." << std::endl;
pmc::net::MsgClient confs_client(_CONFS_HOST, _CONFS_PORT, "ca");
json::object req;
req["_route"] = "get";
req["name"] = confs_config_name;
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 << "[ca] 致命错误: 从 confs 获取 ca.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 << "[ca] 配置来源: confs (ca.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 << "[ca] 致命错误: 配置加载失败: " << 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);
_UPSTREAM_GATEWAY = cf.get<std::string>("Upstream.gateway", "http://127.0.0.1:8445");
_TOKEN_KEEP_TIME = cf.get<int>("Token.keep_time", 3600);
_TOKEN_REFRESH_GRACE_MULTIPLIER = cf.get<int>("Token.refresh_grace_multiplier", 2);
_TOKEN_MAX_REFRESH = cf.get<int>("Token.max_refresh", 10);
_CORS_ORIGINS = split(cf.get<std::string>("CORS.allowed_origins", ""), ';');
_ADMIN_USERS = split(cf.get<std::string>("Admin.users", ""), ';');
/* 下游服务地址 */
_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;... */
/* 1. 环境变量覆盖(默认值之上) */
{
std::string ws_cfg = cf.get<std::string>("WsProxy.routes", "");
if (!ws_cfg.empty()) {
auto entries = split(ws_cfg, ';');
for (auto& entry : entries) {
auto eq = entry.find('=');
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();
};
trim(path); trim(url);
if (!path.empty() && !url.empty()) {
_WS_PROXY_ROUTES[path] = url;
}
}
}
}
std::string v = env_or("CA_HOST", env_or("LISTEN_ADDR"));
if (!v.empty()) _ADDR = v;
}
{
int v = env_int_or("CA_PORT", env_int_or("LISTEN_PORT", -1));
if (v > 0) _PORT = v;
}
if (env_flag("CA_TLS")) _TLS = true;
{
std::string v = env_or("CA_UPSTREAM_GATEWAY", env_or("UPSTREAM_GATEWAY"));
if (!v.empty()) _UPSTREAM_GATEWAY = v;
}
{
int v = env_int_or("CA_TOKEN_KEEP_TIME", -1);
if (v > 0) _TOKEN_KEEP_TIME = v;
}
{
int v = env_int_or("CA_TOKEN_REFRESH_GRACE_MULTIPLIER", -1);
if (v > 0) _TOKEN_REFRESH_GRACE_MULTIPLIER = v;
}
{
int v = env_int_or("CA_TOKEN_MAX_REFRESH", -1);
if (v > 0) _TOKEN_MAX_REFRESH = v;
}
{
std::string v = env_or("CA_CORS_ORIGINS");
if (!v.empty()) _CORS_ORIGINS = split(v, ';');
}
{
std::string v = env_or("CA_ADMIN_USERS");
if (!v.empty()) _ADMIN_USERS = split(v, ';');
}
/* 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 v = env_or("CA_DB_HOST", env_or("DB_HOST"));
if (!v.empty()) _DB_HOST = v;
}
{
int v = env_int_or("CA_DB_PORT", env_int_or("DB_PORT", -1));
if (v > 0) _DB_PORT = v;
}
{
std::string v = env_or("CA_CAPTCHA_HOST", env_or("CAPTCHA_HOST"));
if (!v.empty()) _CAPTCHA_HOST = v;
}
{
int v = env_int_or("CA_CAPTCHA_PORT", env_int_or("CAPTCHA_PORT", -1));
if (v > 0) _CAPTCHA_PORT = v;
}
{
std::string v = env_or("CA_SMTP_HOST", env_or("SMTP_HOST"));
if (!v.empty()) _SMTP_HOST = v;
}
{
int v = env_int_or("CA_SMTP_PORT", env_int_or("SMTP_PORT", -1));
if (v > 0) _SMTP_PORT = v;
}
std::string env_upstream = ConfIni::env("UPSTREAM_GATEWAY");
if (!env_upstream.empty()) _UPSTREAM_GATEWAY = env_upstream;
/* WS 代理路由 env:/path=ws://host:port/target;...(解析逻辑与 CLI 共用) */
std::string ws_cfg = env_or("CA_WS_PROXY_ROUTES");
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. CLI 参数覆盖(最高优先级) */
if (vm.count("host")) _ADDR = vm["host"].as<std::string>();
if (vm.count("port")) _PORT = vm["port"].as<int>();
if (vm.count("instance")) _INSTANCE = vm["instance"].as<std::string>();
/* 2. CLI 参数覆盖(最高优先级) */
if (vm.count("host")) _ADDR = vm["host"].as<std::string>();
if (vm.count("port")) _PORT = vm["port"].as<int>();
if (vm.count("tls")) _TLS = true;
if (vm.count("upstream-gateway")) _UPSTREAM_GATEWAY = vm["upstream-gateway"].as<std::string>();
if (vm.count("token-keep-time")) _TOKEN_KEEP_TIME = vm["token-keep-time"].as<int>();
if (vm.count("token-refresh-grace-multiplier")) _TOKEN_REFRESH_GRACE_MULTIPLIER = vm["token-refresh-grace-multiplier"].as<int>();
if (vm.count("token-max-refresh")) _TOKEN_MAX_REFRESH = vm["token-max-refresh"].as<int>();
if (vm.count("cors-origins")) _CORS_ORIGINS = split(vm["cors-origins"].as<std::string>(), ';');
if (vm.count("admin-users")) _ADMIN_USERS = split(vm["admin-users"].as<std::string>(), ';');
if (vm.count("ws-proxy-routes")) ws_cfg = vm["ws-proxy-routes"].as<std::string>();
if (vm.count("db-host")) _DB_HOST = vm["db-host"].as<std::string>();
if (vm.count("db-port")) _DB_PORT = vm["db-port"].as<int>();
if (vm.count("captcha-host")) _CAPTCHA_HOST = vm["captcha-host"].as<std::string>();
if (vm.count("captcha-port")) _CAPTCHA_PORT = vm["captcha-port"].as<int>();
if (vm.count("smtp-host")) _SMTP_HOST = vm["smtp-host"].as<std::string>();
if (vm.count("smtp-port")) _SMTP_PORT = vm["smtp-port"].as<int>();
if (vm.count("instance")) _INSTANCE = vm["instance"].as<std::string>();
if (vm.count("log-level")) {
_LOG_LEVEL = vm["log-level"].as<std::string>();
// 归一化 warning → warn
@@ -952,18 +950,38 @@ int main(int argc, char **argv) {
}
}
/* 5. 配置完整性校验 */
if (_ADDR.empty() || _PORT == -1) {
std::cerr << "[ca] 致命错误: 配置项 [Server] address/port 缺失"
"(ini 中未配置且无 LISTEN_ADDR/LISTEN_PORT 环境变量覆盖)" << std::endl;
/* WS 代理路由解析:/path=ws://host:port/target;... */
if (!ws_cfg.empty()) {
auto entries = split(ws_cfg, ';');
for (auto& entry : entries) {
auto eq = entry.find('=');
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();
};
trim(path); trim(url);
if (!path.empty() && !url.empty()) {
_WS_PROXY_ROUTES[path] = url;
}
}
}
}
/* 3. 配置完整性校验 */
if (_ADDR.empty() || _PORT <= 0) {
std::cerr << "[ca] 致命错误: 监听地址/端口非法"
"(--host/--port 或 CA_HOST/CA_PORT 环境变量)" << std::endl;
return 1;
}
if (_UPSTREAM_GATEWAY.empty()) {
std::cerr << "[ca] 致命错误: 配置项 [Upstream] gateway 缺失" << std::endl;
std::cerr << "[ca] 致命错误: 上游 gateway 地址缺失(--upstream-gateway)" << std::endl;
return 1;
}
if (_DB_HOST.empty() || _DB_PORT <= 0) {
std::cerr << "[ca] 致命错误: 配置项 [Downstream] db_host/db_port 缺失" << std::endl;
std::cerr << "[ca] 致命错误: db 服务地址缺失(--db-host/--db-port)" << std::endl;
return 1;
}
}