diff --git a/README.md b/README.md index 570d879..a8550bd 100755 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ └────────────┘ 日志链路: waf / ca / gateway / static / dnfs / ai_review / captcha / smtp / aibox / confs / redirect80 ──(log_client, pymsg)──► db:8005 (Logs 表) 认证链路(v0.9.0 起): waf:443 ──HTTP──► ca:8443 (认证+前置转发) ──HTTP──► gateway:8445 (纯转发, 仅 loopback) ──► 内部服务 - 配置链路: ca / gateway / db2 / static / redirect80 / smtp / captcha ──(pymsg get)──► confs:8010(配置统一存放于 confs/configs/,mtime 变化自动重载) + 配置链路: gateway / db2 / static / redirect80 / smtp / captcha ──(pymsg get)──► confs:8010(配置统一存放于 confs/configs/,mtime 变化自动重载;ca/dnfs2 例外,配置纯 CLI 传递) ``` > `db2` / `dnfs2` 分别是 `db` / `dnfs` 的 C++20 重实现(接口完全兼容,可平替),架构位置与 Python 版一致。`BotOps` 为 aibox 的终端前端(WebSocket 客户端),`lci` 为嵌入式帧缓冲图形界面,均不在上图中。 @@ -90,9 +90,10 @@ - 极客操作:`kill -HUP ` 热加载规则;`waf -list` 列规则;`waf -test ` 校验语法;`waf -check ""` 测试单条输入。 - 配置见 [waf/config.yaml](waf/config.yaml),详见 [waf/README.md](waf/README.md)。 -### ca(中央认证服务 · v1.0.0) +### ca(中央认证服务 · v1.1.0) - 内部 HTTP 服务,默认监听 127.0.0.1:8443(接管原 gateway 端口,WAF upstream 不变),不再承载 TLS(TLS 已由 WAF 终止)。 +- **配置体系(v1.1.0)**:彻底移除 confs 依赖,配置全部经 CLI 传递(CLI > 环境变量 > 默认值三层),原 `ca.ini` 配置值迁入 [core/services.yaml](core/services.yaml) 的 ca 条目 args;兼容旧环境变量名(`LISTEN_ADDR`/`UPSTREAM_GATEWAY`/`DB_*` 等)。 - **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),常数时间比较防 timing attack。 - **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取;登录成功即更新 `Users.last_login`(`"$now"` 哨兵)。 - **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效;jti 黑名单 + 用户版本号双轨吊销。 @@ -206,7 +207,6 @@ | 配置文件 | 消费模块 | 依赖强度 | |----------|----------|----------| -| `ca.ini` | ca | confs 拉取 | | `gateway.ini` | gateway | confs 拉取(0.7.0 起配置完全迁入) | | `db.ini` | db / db2 | 强依赖(confs 未就绪自动重试) | | `static.ini` | static(fsys.py)/ fsys | 强依赖(拉取失败直接退出) | @@ -215,7 +215,7 @@ | `captcha.ini` | captcha / captcha2 | confs 拉取 | | `example.ini` | —(旧 gateway 示例模板) | 无消费方 | -- **部署安全约定**:`confs/configs/*.ini` 含敏感字段(`ca.ini` 管理员白名单是 `is_admin` 判定依据、`smtp.ini` 密码字段),权限必须收紧为 `root:root 0640`,仅 root 可写;被同机普通用户篡改可直接提权为管理员或劫持邮件验证码(详见 `docs/security/core_SECURITY_AUDIT_20260801.md` 2026-08-10 复审)。 +- **部署安全约定**:`confs/configs/*.ini` 含敏感字段(如 `smtp.ini` 密码字段),权限必须收紧为 `root:root 0640`,仅 root 可写;被同机普通用户篡改可直接劫持邮件验证码(详见 `docs/security/core_SECURITY_AUDIT_20260801.md` 2026-08-10 复审)。原 `ca.ini` 管理员白名单已随 ca v1.1.0 迁入 `core/services.yaml` 的 CLI args,其防护对象相应转为 `services.yaml` 的文件权限。 ### aibox(独立 AI 智能体系统) diff --git a/ca/Makefile b/ca/Makefile index fc0bbe4..90e08f2 100644 --- a/ca/Makefile +++ b/ca/Makefile @@ -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/ diff --git a/ca/README.md b/ca/README.md index e0af986..88b622b 100644 --- a/ca/README.md +++ b/ca/README.md @@ -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 纯转发服务。 diff --git a/ca/_run.sh b/ca/_run.sh index 67da3b7..255f6d6 100755 --- a/ca/_run.sh +++ b/ca/_run.sh @@ -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 字段统一管理 diff --git a/ca/inc/Conf.hpp b/ca/inc/Conf.hpp deleted file mode 100644 index a0b7d82..0000000 --- a/ca/inc/Conf.hpp +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -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 - T get(const std::string &path, const T &_defaut) { - return pt.get(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; - }; -} diff --git a/ca/src/main.cpp b/ca/src/main.cpp index aa163c6..584a916 100644 --- a/ca/src/main.cpp +++ b/ca/src/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -23,7 +24,6 @@ #include #include "ExpBase.hpp" #include "LocalTime.hpp" -#include "Conf.hpp" #include "DbC.hpp" #include @@ -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 @@ -376,28 +376,41 @@ std::string extract_token_from_request(const http::request& 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 _CORS_ORIGINS; // CORS 允许的 Origin 白名单,可由 [CORS] allowed_origins 配置 -static std::vector _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 _CORS_ORIGINS; // CORS 允许的 Origin 白名单(分号分隔) +static std::vector _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(), "绑定地址(默认 127.0.0.1),覆盖 [Server] address") - ("port", po::value(), "监听端口(默认 8443),覆盖 [Server] port") - ("config", po::value(), "confs 拉取的配置名(默认 ca.ini)") + ("host", po::value(), "绑定地址(默认 127.0.0.1,env CA_HOST/LISTEN_ADDR)") + ("port", po::value(), "监听端口(默认 8443,env CA_PORT/LISTEN_PORT)") + ("tls", "启用 HTTPS(证书 full_chain.crt/server.key;缺省纯 HTTP,env CA_TLS=on)") + ("upstream-gateway", po::value(), "上游 gateway 地址(默认 http://127.0.0.1:8445,env CA_UPSTREAM_GATEWAY/UPSTREAM_GATEWAY)") + ("token-keep-time", po::value(), "Token 有效期秒数(默认 3600,env CA_TOKEN_KEEP_TIME)") + ("token-refresh-grace-multiplier", po::value(), "Token 刷新宽限期倍数(默认 2,env CA_TOKEN_REFRESH_GRACE_MULTIPLIER)") + ("token-max-refresh", po::value(), "Token 最大刷新次数(默认 10,env CA_TOKEN_MAX_REFRESH)") + ("cors-origins", po::value(), "CORS Origin 白名单,分号分隔(env CA_CORS_ORIGINS)") + ("admin-users", po::value(), "管理员账号白名单,分号分隔(env CA_ADMIN_USERS)") + ("ws-proxy-routes", po::value(), "WS 代理路由 /path=ws://host:port,分号分隔(env CA_WS_PROXY_ROUTES)") + ("db-host", po::value(), "db 服务地址(默认 127.0.0.1,env CA_DB_HOST/DB_HOST)") + ("db-port", po::value(), "db 服务端口(默认 8005,env CA_DB_PORT/DB_PORT)") + ("captcha-host", po::value(), "captcha 服务地址(默认 127.0.0.1,env CA_CAPTCHA_HOST/CAPTCHA_HOST)") + ("captcha-port", po::value(), "captcha 服务端口(默认 8009,env CA_CAPTCHA_PORT/CAPTCHA_PORT)") + ("smtp-host", po::value(), "smtp 服务地址(默认 127.0.0.1,env CA_SMTP_HOST/SMTP_HOST)") + ("smtp-port", po::value(), "smtp 服务端口(默认 8008,env CA_SMTP_PORT/SMTP_PORT)") ("log-level", po::value(), "日志级别 debug|info|warn|error(默认 info)") ("instance", po::value(), "实例名(注册中心用,默认 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(); - } - - 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("Server.address", ""); - _PORT = cf.get("Server.port", -1); - _TLS = cf.get("Server.tls", false); - _UPSTREAM_GATEWAY = cf.get("Upstream.gateway", "http://127.0.0.1:8445"); - _TOKEN_KEEP_TIME = cf.get("Token.keep_time", 3600); - _TOKEN_REFRESH_GRACE_MULTIPLIER = cf.get("Token.refresh_grace_multiplier", 2); - _TOKEN_MAX_REFRESH = cf.get("Token.max_refresh", 10); - _CORS_ORIGINS = split(cf.get("CORS.allowed_origins", ""), ';'); - _ADMIN_USERS = split(cf.get("Admin.users", ""), ';'); - - /* 下游服务地址 */ - _DB_HOST = cf.get("Downstream.db_host", "127.0.0.1"); - _DB_PORT = cf.get("Downstream.db_port", 8005); - _CAPTCHA_HOST = cf.get("Downstream.captcha_host", "127.0.0.1"); - _CAPTCHA_PORT = cf.get("Downstream.captcha_port", 8009); - _SMTP_HOST = cf.get("Downstream.smtp_host", "127.0.0.1"); - _SMTP_PORT = cf.get("Downstream.smtp_port", 8008); - - /* [WsProxy] WebSocket 代理路由:格式 /path=ws://host:port/target;... */ + /* 1. 环境变量覆盖(默认值之上) */ { - std::string ws_cfg = cf.get("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(); - if (vm.count("port")) _PORT = vm["port"].as(); - if (vm.count("instance")) _INSTANCE = vm["instance"].as(); + /* 2. CLI 参数覆盖(最高优先级) */ + if (vm.count("host")) _ADDR = vm["host"].as(); + if (vm.count("port")) _PORT = vm["port"].as(); + if (vm.count("tls")) _TLS = true; + if (vm.count("upstream-gateway")) _UPSTREAM_GATEWAY = vm["upstream-gateway"].as(); + if (vm.count("token-keep-time")) _TOKEN_KEEP_TIME = vm["token-keep-time"].as(); + if (vm.count("token-refresh-grace-multiplier")) _TOKEN_REFRESH_GRACE_MULTIPLIER = vm["token-refresh-grace-multiplier"].as(); + if (vm.count("token-max-refresh")) _TOKEN_MAX_REFRESH = vm["token-max-refresh"].as(); + if (vm.count("cors-origins")) _CORS_ORIGINS = split(vm["cors-origins"].as(), ';'); + if (vm.count("admin-users")) _ADMIN_USERS = split(vm["admin-users"].as(), ';'); + if (vm.count("ws-proxy-routes")) ws_cfg = vm["ws-proxy-routes"].as(); + if (vm.count("db-host")) _DB_HOST = vm["db-host"].as(); + if (vm.count("db-port")) _DB_PORT = vm["db-port"].as(); + if (vm.count("captcha-host")) _CAPTCHA_HOST = vm["captcha-host"].as(); + if (vm.count("captcha-port")) _CAPTCHA_PORT = vm["captcha-port"].as(); + if (vm.count("smtp-host")) _SMTP_HOST = vm["smtp-host"].as(); + if (vm.count("smtp-port")) _SMTP_PORT = vm["smtp-port"].as(); + if (vm.count("instance")) _INSTANCE = vm["instance"].as(); if (vm.count("log-level")) { _LOG_LEVEL = vm["log-level"].as(); // 归一化 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; } } diff --git a/confs/configs/ca.ini b/confs/configs/ca.ini deleted file mode 100644 index 3c8c190..0000000 --- a/confs/configs/ca.ini +++ /dev/null @@ -1,51 +0,0 @@ -[Server] -# CA(Central Auth)前置认证服务:接管原 gateway 的 8443 端口, -# WAF upstream 不变(waf → ca → gateway 前置拓扑)。 -# - tls = off 启用 HTTP 模式(配合 waf 模块使用) -# - tls = on 启用 HTTPS 模式(独立部署,证书为 full_chain.crt / server.key) -address = 127.0.0.1 -port = 8443 -tls = off - -[Upstream] -# 前置拓扑的唯一上游:认证通过后转发到 gateway(仅 loopback) -# 环境变量 UPSTREAM_GATEWAY 可覆盖 -gateway = http://127.0.0.1:8445 - -[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 等)凭该字段豁免所有权校验,无需各自维护名单。 -# 修改后需重启 ca 生效;旧 Token 无此字段,按非管理员处理。 -users = 1776177662@qq.com;277665039@qq.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 - -[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 diff --git a/core/services.yaml b/core/services.yaml index b3299b6..38cb423 100644 --- a/core/services.yaml +++ b/core/services.yaml @@ -76,7 +76,43 @@ services: - name: ca cmd: bash /opt/micro_servers/ca/_run.sh - args: ["--instance", "ca", "--log-level", "info"] + args: + - "--instance" + - "ca" + - "--log-level" + - "info" + # --- v1.1.0:配置全部经 CLI 传递(原 confs/configs/ca.ini 迁入) --- + - "--host" + - "127.0.0.1" + - "--port" + - "8443" + - "--upstream-gateway" + - "http://127.0.0.1:8445" + - "--token-keep-time" + - "3600" + - "--token-refresh-grace-multiplier" + - "2" + - "--token-max-refresh" + - "10" + # 管理员白名单(分号分隔):登录命中时 Token 写入 is_admin=true + - "--admin-users" + - "1776177662@qq.com;277665039@qq.com" + - "--cors-origins" + - "https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443" + - "--ws-proxy-routes" + - "/ws/aibox=ws://127.0.0.1:8765" + - "--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" depends_on: [db2] sleep_before: 2