- 新增 smtp3: pymsg/TCP :8008 邮件验证码服务 (smtp_v2.py 重写, 零新增依赖) - 审计实证并修复 2 项 P0: SMTP 响应解析颠倒 (发送 100% 失败)、base64 符号扩展 - 修复 P1-2 SIGPIPE、P3-1 日志注入脱敏、P3-3 IPv6、P3-4 模偏差、P3-5 LogC 长连接复用 - 剩余 P1-1 (TLS 证书校验) 与端到端发信验证待 smtp.ini 凭据就绪 - 新增审计报告 docs/security/smtp3_SECURITY_AUDIT_20260809.md 并更新 INDEX
32 KiB
smtp 模块 C++20 重写 · 开发方案
文档类型:模块重写开发计划 模块:smtp(邮箱验证码服务) 日期:2026-08-09 参照:captcha2 / db2 / dnfs2 的 Python → C++20 重写路径
1. 概述
1.1 背景与动机
smtp 模块是 micro_servers 中仅存的少量 Python 内部基础服务之一(smtp + captcha + ai_review),负责注册/重置密码流程的邮箱验证码发送与校验。当前 Python 版(smtp_v2.py,317 行)依赖仅 pymsg + Python 标准库 smtplib/email,属于中等规模、外部 I/O 密集的重写目标。
| 维度 | Python 版现状 | 重写收益 |
|---|---|---|
| 依赖 | Python venv + pymsg 本地包 | 消除 Python 运行时,编译为单二进制 smtp2.bin |
| 启动 | _run.sh 激活 venv 后 exec python3 |
与 db2/dnfs2/captcha2 一致的直接启动 |
| 安全遗留 | P1-F1 now 遮蔽 → 回滚失效、P2-F2 验证码明文入日志、P2-F3 random 弱随机等 7 项审计发现未修复 | 重写时一并根治 |
| 一致性 | 项目已有 4 个成功的 C++ 重写案例(db2/dnfs2/captcha2/fsys) | 与 gateway/db2/dnfs2/captcha2 统一技术栈 |
| SMTP 会话 | smtplib.SMTP_SSL 封装良好但异常粒度粗 |
直接控制 OpenSSL 会话,更精细的超时/错误处理 |
1.2 设计目标
- 协议完全兼容:pymsg 三条路由(
send_email/verify_email/health)请求/响应格式与 Python 版字节级一致,gateway 零改动 - 审计清零:修复安全审计报告(
smtp_SECURITY_AUDIT_20260801.md)中全部未修复项 - 零新增重型依赖:仅引入项目已安装的 OpenSSL(libssl-dev,各 C++ 模块均依赖)
- 基础设施复用:libmsg(RpcServer/AsyncQueue)、boost::json、Conf.hpp,所有已在项目内的组件
- 安全增强:
std::random_device替代random、常数时间比较、日志脱敏
2. 现状分析
2.1 源码结构
smtp/
├── smtp_v2.py # 317 行,服务主程序
├── log_client.py # 245 行,异步日志 SDK(项目通用)
├── _set.sh # venv 创建 + pip install
├── _run.sh # venv 激活 + exec python3 smtp_v2.py
├── requirements.txt # ../pymsg + nuitka
└── .gitignore
2.2 功能清单
| 功能 | 代码位置 (smtp_v2.py) | 行数 | C++ 等效方案 |
|---|---|---|---|
| .env 手动加载(4 种格式兼容) | L30–55 | 26 | Conf.hpp 的 ini 解析(与 gateway 一致) |
| 配置常量化 | L66–100 | 35 | struct SmtpConfig 编译时常量 + env 覆盖 |
SmtpServer.__init__ |
L110–120 | 11 | RpcServer 构造 + add_route 注册 |
_handle_send_email |
L145–251 | 107 | SMTP_SSL 客户端 + 配额/冷却/熔断逻辑(~200 行 C++) |
_handle_verify_email |
L253–292 | 40 | 线程安全 Store + 常数时间比较(~70 行 C++) |
_handle_health |
L294–295 | 2 | 返回 {"status":"ok","service":"smtp"} |
main / argparse |
L302–316 | 15 | boost::program_options(与 gateway 一致) |
| log_client | 整个文件 | 245 | 基于 libmsg AsyncQueue,参照 captcha2 LogC.hpp(~60 行) |
2.3 依赖替换矩阵
| Python 依赖 | C++ 替代 | 引入方式 |
|---|---|---|
smtplib.SMTP_SSL |
自写 SMTP_SSL 客户端(POSIX socket + OpenSSL BIO) | 系统已安装 libssl-dev |
email.mime.text.MIMEText |
手写 MIME 拼装(~20 行字符串模板) | 无额外依赖 |
pymsg |
libmsg (RpcServer / AsyncQueue) |
系统已安装 (libus_msg.so) |
log_client.py |
独立 LogC.hpp(基于 AsyncQueue) |
参照 captcha2 LogC.hpp |
random / secrets |
<random> (random_device + mt19937) |
C++11 标准库 |
threading.Lock |
std::mutex + std::lock_guard |
C++11 标准库 |
json |
boost::json |
系统已安装 |
argparse |
boost::program_options |
系统已安装 |
| 手动 .env 解析 | Conf.hpp(ini 格式) / 环境变量 | 与 gateway 一致 |
2.4 SMTP_SSL 会话流程分析
Python smtplib.SMTP_SSL 在端口 465 上的标准流程:
1. socket.create_connection((host, port), timeout=10)
→ TCP 连接
2. ssl.SSLContext.wrap_socket(sock) 或直接 SMTP_SSL 构造
→ TLS 握手(隐式 TLS,端口 465 专用)
3. server.ehlo() / helo()
→ SMTP 握手,获取服务器能力
4. server.login(user, password)
→ AUTH LOGIN 认证
5. server.sendmail(from_addr, [to_addr], msg.as_string())
→ MAIL FROM → RCPT TO → DATA → 发送 MIME 正文
6. server.quit()
→ QUIT 优雅关闭
C++ 实现使用原始 POSIX socket + OpenSSL BIO(参照 libhttp HttpClient 的 TLS 栈),约 100 行。
3. 整体设计
3.1 模块结构
smtp2/
├── src/
│ └── main.cpp # ~550 行,全部实现
│ ├── SmtpConfig # 配置结构体 + 加载(~40 行)
│ ├── CodeStore # 线程安全验证码存储(~60 行)
│ ├── SendLimiter # 双窗口限流 + 全局熔断(~50 行)
│ ├── SmtpClient # SMTP_SSL 客户端(~100 行)
│ ├── generate_code() # 安全随机验证码(~10 行)
│ ├── build_mime() # MIME 邮件拼装(~20 行)
│ ├── route handlers # 三个路由处理(~150 行)
│ └── main() # 参数解析 + 配置 + 启动(~80 行)
├── inc/
│ └── LogC.hpp # 简化版异步日志客户端(~60 行,参照 captcha2)
├── Makefile # 构建(~30 行)
├── _set.sh # make + install(~10 行)
├── _run.sh # 启动脚本(~5 行)
└── README.md
3.2 与现有 C++ 基础库的关系
smtp2.bin
│
├── libmsg (RpcServer) ← pymsg 协议 C++ 实现,路由分发 + 响应包装
│ └── libus_msg.so (系统已安装)
│
├── libmsg (AsyncQueue) ← 异步批量日志推送
│ └── libus_msg.so
│
├── boost::json ← JSON 解析/序列化
│ └── libboost_json (系统已安装)
│
├── boost::program_options ← 命令行参数解析
│ └── libboost_program_options(系统已安装)
│
├── OpenSSL (libssl) ← SMTP_SSL TLS 会话
│ └── libssl.so (系统已安装,各 C++ 模块均依赖)
│
└── POSIX socket ← TCP 连接(标准系统调用)
3.3 配置体系
与 gateway/db2/dnfs2/captcha2 一致的两层加载:confs 拉取 .ini(若存在)→ 环境变量覆盖。
smtp.ini(新增,托管在 confs/configs/):
[server]
host = 127.0.0.1
port = 8008
workers = 4
[smtp]
server = smtp.163.com
port = 465
user = your@163.com
password = your_smtp_password # 也支持环境变量 SMTP_PASSWORD 覆盖
mail_from = # 可选,默认同 user
code_len = 6
code_ttl = 600
send_interval = 60
max_verify_attempts = 5
email_hourly_limit = 3
email_daily_limit = 10
smtp_hourly_limit = 100
default_subject = DNFS 邮箱验证
环境变量覆盖(最高优先级,Docker 兼容):
| 变量 | 对应配置项 |
|---|---|
SMTP_SERVER |
smtp.server |
SMTP_PORT |
smtp.port |
SMTP_USER |
smtp.user |
SMTP_PASSWORD |
smtp.password |
MAIL_FROM |
smtp.mail_from |
EMAIL_HOURLY_LIMIT |
smtp.email_hourly_limit |
EMAIL_DAILY_LIMIT |
smtp.email_daily_limit |
SMTP_HOURLY_LIMIT |
smtp.smtp_hourly_limit |
向后兼容 __PASSWORD 环境变量(Python 版历史遗留,作为 SMTP_PASSWORD 的 fallback)。
4. 详细设计
4.1 CodeStore — 线程安全验证码存储
class CodeStore {
public:
struct Entry {
std::string code; // 6 位数字验证码
double expires_at; // unix timestamp(std::chrono → double)
int attempts = 0; // 连续校验失败次数
};
// 写入验证码(覆盖已有);同时惰性清理过期条目
void put(const std::string& email, const std::string& code,
double ttl_seconds);
// 校验验证码:正确 → 删除并返回 true;错误 → attempts++,
// 达 max_attempts 时删除,invalidated 置 true 并返回 false
bool verify(const std::string& email, const std::string& code,
double ttl_seconds, int max_attempts,
bool& invalidated);
// 删除指定邮箱的验证码(发送失败回滚用)
void remove(const std::string& email);
// 惰性清理所有过期条目
void expire();
private:
mutable std::mutex mtx_;
std::unordered_map<std::string, Entry> store_;
};
与 Python 版的对应:code_store dict + STORE_LOCK。C++ 版封装为独立类,校验通过后 store_.erase(it) 保证一次性消费;verify() 使用 hmac_compare_digest 风格常数时间比较(审计 P3-F4 修复)。
4.2 SendLimiter — 双窗口限流 + 全局熔断
class SendLimiter {
public:
// 检查是否允许发送;允许时预占时间戳(返回 true + 记录 ts_id)。
// ts_id 用于失败回滚(精确移除预占的时间戳)。
bool try_consume(const std::string& email,
int hourly_limit, int daily_limit,
int global_hourly_limit,
uint64_t& ts_id);
// 回滚预占:传入 try_consume 返回的 ts_id,精确移除对应的三条记录
void rollback(const std::string& email, uint64_t ts_id);
private:
// send_log[email] = {hour_deque, day_deque}
// global_send_log = deque<pair<ts_id, timestamp>>
// 惰性清理:每次 try_consume 时剔除滑出窗口的条目
mutable std::mutex mtx_;
struct EmailLog {
std::deque<double> hour;
std::deque<double> day;
};
std::unordered_map<std::string, EmailLog> send_log_;
std::deque<std::pair<uint64_t, double>> global_log_;
uint64_t next_ts_id_ = 1;
};
修复 P1-F1 "now 变量遮蔽":C++ 版使用单调递增 ts_id(uint64_t)标识每次预占,失败回滚时按 ts_id 精确移除,彻底消除 Python 版浮点时间戳 == 比较的不确定性——这是审计 P1-F1 的根本性修复。
修复 P3-F6(subject/content 无长度上限):_handle_send_email 入口处校验 subject ≤ 256 字节、content ≤ 4096 字节,超限拒绝。
4.3 SmtpClient — SMTP_SSL 客户端
核心难点。Python smtplib.SMTP_SSL 封装了 TCP + TLS + SMTP 协议。C++ 手写实现约 100 行。
class SmtpClient {
public:
struct Config {
std::string server;
int port = 465;
std::string user;
std::string password;
std::string mail_from; // 空则用 user
double timeout = 20.0; // 完整 SMTP 会话超时
};
explicit SmtpClient(const Config& cfg);
// 发送一封邮件;失败抛出 std::runtime_error(含可读错误消息)。
// 调用方可选择脱敏后返回或记录日志。
void send(const std::string& to,
const std::string& subject,
const std::string& body_mime);
private:
// 内部:
// 1. socket() → connect() 创建 TCP 连接
// 2. SSL_CTX_new(TLS_client_method()) → SSL_new → SSL_set_fd → SSL_connect
// 3. SMTP 协议交互:
// EHLO → AUTH LOGIN → MAIL FROM → RCPT TO → DATA → QUIT
// 4. 每步读取响应码(2xx/3xx 为成功,4xx/5xx 抛异常)
Config cfg_;
};
SMTP 协议交互伪代码:
void SmtpClient::send(const std::string& to,
const std::string& subject,
const std::string& body_mime) {
// 1. TCP connect
int sock = socket(AF_INET, SOCK_STREAM, 0);
// ... resolve host, connect, set SO_RCVTIMEO/SO_SNDTIMEO ...
// 2. TLS handshake
SSL_CTX* ctx = SSL_CTX_new(TLS_client_method());
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock);
SSL_connect(ssl);
// 3. SMTP dialogue
read_response(ssl, 220); // greeting
send_cmd(ssl, "EHLO localhost"); read_response(ssl, 250);
send_cmd(ssl, "AUTH LOGIN");
read_response(ssl, 334); // Username:
send_cmd(ssl, base64_encode(cfg_.user));
read_response(ssl, 334); // Password:
send_cmd(ssl, base64_encode(cfg_.password));
read_response(ssl, 235); // Authentication successful
send_cmd(ssl, "MAIL FROM:<" + from + ">"); read_response(ssl, 250);
send_cmd(ssl, "RCPT TO:<" + to + ">"); read_response(ssl, 250);
send_cmd(ssl, "DATA"); read_response(ssl, 354);
send_data(ssl, body_mime + "\r\n.\r\n"); read_response(ssl, 250);
send_cmd(ssl, "QUIT"); read_response(ssl, 221);
// 4. cleanup
SSL_shutdown(ssl); SSL_free(ssl); SSL_CTX_free(ctx);
close(sock);
}
超时策略:SO_RCVTIMEO + SO_SNDTIMEO 设为 20s(与 gateway _TIMEOUT_SMTP_SEND 一致)。OpenSSL 的 SSL_connect / SSL_read / SSL_write 遇到 socket 超时会返回错误,被外围 try-catch 捕获并转换为 std::runtime_error。
Base64 编码:复用 libcrypt Base64::encode()(libus_crypt.so),免去手写。
MIME 拼装(build_mime(),~20 行):
std::string build_mime(const std::string& from,
const std::string& to,
const std::string& subject,
const std::string& body_plain) {
std::ostringstream os;
os << "From: " << from << "\r\n"
<< "To: " << to << "\r\n"
<< "Subject: =?UTF-8?B?" << base64_encode(subject) << "?=\r\n"
<< "MIME-Version: 1.0\r\n"
<< "Content-Type: text/plain; charset=UTF-8\r\n"
<< "Content-Transfer-Encoding: base64\r\n"
<< "\r\n"
<< wrap_base64_lines(body_plain); // 每 76 字符换行(RFC 2045)
return os.str();
}
4.4 路由处理
// send_email 路由
json::value handle_send_email(const json::object& req) {
// 1. 取出 email → 空或缺 @ → 返回 invalid email
// 2. SMTP 配置完整性校验(server/user/password 均非空)
// 3. 发送冷却检查(60s)→ 冷却中返回 retry_after
// 4. 双窗口邮箱限流 + 全局熔断预占 → 超限返回对应错误
// 5. 生成验证码 → 写入 CodeStore
// 6. 调用 SmtpClient.send()
// - 成功:刷新冷却时间戳,惰性清理过期冷却/验证码条目
// - 失败:回滚 CodeStore、冷却预占、限流预占;返回失败错误
// 7. 返回 {sent: true, ttl: CODE_TTL}
// ★ 审计修复清单(在重写中一并解决):
// P1-F1:ts_id 精确回滚(§4.2 SendLimiter)
// P2-F2:日志不打印验证码明文,仅记邮箱 + "验证码发送成功"
// P2-F3:generate_code() 使用 std::random_device
// P3-F5:邮箱格式用与 gateway 一致的严格正则
// P3-F6:subject ≤ 256B, content ≤ 4096B
// P3-F7:失败响应返回通用文案,SMTP 细节仅入日志
}
// verify_email 路由
json::value handle_verify_email(const json::object& req) {
// 1. 取出 email + code(兼容旧字段 input_captcha)→ 空值返回 error
// 2. 调 CodeStore.verify() 常数时间比较 → 返回 valid/invalidated
// 3. 日志仅记邮箱 + "验证通过" / "验证失败"
// ★ 审计修复:P3-F4 常数时间比较(hmac_compare_digest 风格)
}
// health 路由
json::value handle_health(const json::object&) {
return {{"status", "ok"}, {"service", "smtp"}};
}
4.5 generate_code() — 安全随机验证码
std::string generate_code(int len = 6) {
// thread_local 保证 RpcServer workers>0 时多线程安全
thread_local std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(0, 9);
std::string code;
code.reserve(len);
for (int i = 0; i < len; ++i)
code.push_back('0' + dist(rng));
return code;
}
审计修复(P2-F3):Python 版使用 random.randint(MT19937 但种子来自系统时间等确定性源),C++ 版 std::random_device 作为种子源,提供真正的非确定性熵(Linux 上映射到 /dev/urandom)。
4.6 LogC.hpp — 异步日志客户端
与 captcha2 的 LogC.hpp 完全一致(参照 captcha2/inc/LogC.hpp),基于 libmsg AsyncQueue<LogEntry>:
- 异步批量推送(攒满 50 条或 2 秒 flush)
- fail-safe(db 不可用静默丢弃)
push()/info()/warning()/error()API- 环境变量
DB_HOST/DB_PORT覆盖
5. 实现计划
5.1 分阶段交付
| 阶段 | 里程碑 | 关键产出 | 预估工时 |
|---|---|---|---|
| P0 — 基础设施 | 目录骨架 + Makefile + LogC.hpp | smtp2/ 结构搭建,能编译出空 main() 链接 libmsg + OpenSSL |
0.5h |
| P1 — SmtpClient | SMTP_SSL 客户端全链路 | POSIX socket → OpenSSL TLS → SMTP EHLO/AUTH/MAIL/DATA/QUIT 完整会话,独立测试程序可发一封真实邮件 | 4h |
| P2 — CodeStore + SendLimiter | 线程安全存储 + 限流熔断 | CodeStore 生成/校验/过期/作废 + SendLimiter 双窗口/全局熔断/ts_id 精确回滚 |
2h |
| P3 — 路由集成 | RpcServer 三路由 + 命令行参数 |
能与 gateway 联调,pymsg 协议互通 | 1.5h |
| P4 — 配置加载 | confs smtp.ini 拉取 + env 覆盖 + 向后兼容 | 与 Python 版 .env 格式兼容,__PASSWORD 别名支持 |
1h |
| P5 — 审计修复验证 | 逐条对照审计报告确认修复 | P1-F1 / P2-F2 / P2-F3 / P3-F4~F7 全部标绿 | 1h |
| P6 — 测试 | 黑盒测试 + SMTP mock 测试 | tests/test_smtp2.py |
2h |
| P7 — 文档 + 部署 | README + _set.sh/_run.sh + init.txt 更新 | 完整文档 + 一键安装 + core 自启动 | 0.5h |
总计预估:~12.5 小时
5.2 P1 SmtpClient 是高风险路径
P1 阶段涉及 OpenSSL TLS 握手与 SMTP 协议状态机,是重写中唯一的外部网络 I/O 路径。策略:
- 独立测试程序:先写一个
smtp2/test_send.cpp,硬编码 SMTP 凭据,独立验证"连接 → TLS → 认证 → 发送 → 断开"全流程 - 逐响应码验证:SMTP 服务器返回 220/250/334/235/354/221 的协议序列化,每步 assert
- 异常路径测试:错误密码 → 认证失败、错误邮箱 → RCPT TO 拒绝、网络断开 → socket 超时
- 多服务商兼容:至少验证 163.com(465 端口隐式 TLS)一种主流配置
5.3 构建系统
# smtp2/Makefile
CXX = g++
CXXFLAGS = -std=c++20 -Wall -O2
INCS = -I./inc
LIBS = -lus_msg -lboost_json -lboost_program_options -lus_crypt -lssl -lcrypto -lpthread
TARGET = smtp2.bin
SRC = src/main.cpp
$(TARGET): $(SRC)
$(CXX) $(CXXFLAGS) $(INCS) $^ $(LIBS) -o $@
install: $(TARGET)
cp $(TARGET) /usr/local/bin/
clean:
rm -f $(TARGET)
说明:
-lssl -lcrypto提供 OpenSSL 的 TLS 和 base64;-lus_crypt备选 base64(libcrypt 已封装),实际按需二选一。无新增系统包。
6. 测试计划
6.1 黑盒测试(tests/test_smtp2.py)
复用现有测试框架(tests/blackbox/helpers.py 的 PymsgClient),覆盖:
| 测试用例 | 说明 |
|---|---|
test_send_email_valid |
正常发送 → {"sent": true, "ttl": 600} |
test_send_invalid_email |
不含 @ → {"sent": false, "error": "invalid email"} |
test_send_missing_config |
SMTP 未配 → {"sent": false, "error": "SMTP 服务未配置"} |
test_send_cooldown |
60s 内连发两次 → 第二次返回 retry_after 且不覆盖验证码 |
test_send_email_hourly_limit |
超过 3 次/小时 → 返回配额耗尽错误 |
test_send_email_daily_limit |
超过 10 次/天 → 返回每日上限错误 |
test_send_global_fuse |
全局 100 次/小时触发 → 返回全局熔断 |
test_send_failure_rollback |
Mock SMTP 失败 → 验证码/冷却/配额全部回滚(P1-F1 回归测试) |
test_verify_correct |
正确验证码 → {"valid": true}(一次性消费) |
test_verify_wrong |
错误验证码 → {"valid": false, "error": "invalid or expired code"} |
test_verify_replay |
校验通过后再次校验 → 第二次也应返回 invalid or expired code |
test_verify_invalidated |
连续 5 次错误 → {"valid": false, "invalidated": true} |
test_verify_expired |
超时后校验 → {"valid": false, "error": "invalid or expired code"} |
test_verify_missing_fields |
缺少 email 或 code → {"valid": false, "error": "email and code required"} |
test_verify_compat_input_captcha |
使用旧字段 input_captcha 传递验证码 → 正常校验 |
test_health |
→ {"status": "ok", "service": "smtp"} |
6.2 SMTP Mock 测试
P1-F1 回归测试需要 SMTP 失败的场景,不能依赖真实 SMTP 服务。方案:
- 独立 mock 程序(
smtp2/test_mock_smtp.py):在本地随机端口启动一个极简 SMTP 服务器(仿 220/250/334/235/354/221 协议但RCPT TO后返回 550 拒绝),smtp2.bin 连接该 mock 端口 - 验证链路:发送请求 → smtp2 连接 mock → mock 返回 550 → smtp2 触发失败回滚 → 验证 store/limiter 中无残留
6.3 gateway 联调
- 启动 smtp2.bin 替代 Python smtp(监听
127.0.0.1:8008) POST /register/send_code→ 正常返回{"success": true}- 邮箱收到验证码,
POST /register校验通过 - 重置密码流程(
/reset/send_code→/reset/password)完整走通
7. 迁移计划
7.1 渐进切换
阶段 1: smtp2.bin 开发完成,通过全部测试(包括 SMTP mock 测试)
↓
阶段 2: 在测试环境与 Python smtp 并行运行(不同端口),全量测试
↓
阶段 3: 修改 core/init.txt,将 smtp/_run.sh 替换为 smtp2/_run.sh
↓
阶段 4: 生产切换,观察 1 周无异常后删除 Python smtp 目录
7.2 gateway 零改动保证
- pymsg 协议不变(JSON RPC 约定不变)
- 监听地址不变(
127.0.0.1:8008) - 三条路由
send_email/verify_email/health请求/响应格式字节级兼容 - gateway 的
handleSendEmailCode()/verify_email_code_remote()/handleResetSendCode()通过 pymsgMsgClient调用,与 Python 版完全兼容
7.3 回滚方案
init.txt中恢复bash /opt/micro_servers/smtp/_run.sh- Python venv 保持不变(不删除),可立即回滚
7.4 配置文件迁移
现有: smtp/.env (gitignore 保护)
新增: confs/configs/smtp.ini (纳入 confs 统一管理)
兼容: 环境变量 SMTP_* 优先级最高,与 .env 行为一致
平滑: smtp2 启动时先读 confs smtp.ini → 环境变量覆盖 → 完整性校验
8. 安全考量
8.1 审计遗留修复清单
以下 7 项审计发现在 C++ 重写中一并根治:
| 编号 | 等级 | 问题 | C++ 版修复 |
|---|---|---|---|
| P1-F1 | P1 | now 变量遮蔽 → SMTP 失败时限流/熔断回滚失效 |
SendLimiter 使用单调 ts_id 精确回滚(§4.2) |
| P2-F2 | P2 | 验证码明文写入集中日志 Logs 表 | 日志仅记邮箱 + 事件名("验证码发送成功"),不做任何编码记录 |
| P2-F3 | P2 | random.randint 非密码学安全随机 |
std::random_device 种子 + std::mt19937(§4.5) |
| P3-F4 | P3 | 验证码比较非常数时间 | std::string_view 逐字符异或比较(短路无分支)或 CRYPTO_memcmp(§4.1) |
| P3-F5 | P3 | 邮箱格式仅检查 @ |
与 gateway 一致的正则:^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$ |
| P3-F6 | P3 | subject/content 无长度上限 |
上限 256 / 4096 字节,超限拒绝 |
| P3-F7 | P3 | 失败响应直传 SMTP 错误原文 | smtp2 自身返回通用文案,SMTP 细节仅入本地日志 |
8.2 已有安全机制保持
| 机制 | Python 版 | C++ 版 |
|---|---|---|
| 发送冷却 60s | send_history + STORE_LOCK |
std::unordered_map<email, double> + std::mutex |
| 冷却失败回滚 | send_history.pop(email, None) |
cooling_store_.erase(email) —— 直接 erase,无 P1-F1 风险 |
| 验证码一次性消费 | del code_store[email] |
store_.erase(it) |
| 连续失败 5 次作废 | entry['attempts'] >= 5 → del |
entry.attempts >= max → erase + invalidated=true |
| 10 分钟过期 | 惰性清理(verify_email + send_email 时) |
同 |
| 邮箱级双窗口限流 | send_log + global_send_log + 惰性清理 |
SendLimiter(ts_id 精确回滚) |
| 全局熔断 | SMTP_HOURLY_LIMIT(默认 100) |
同 |
| 仅监听 127.0.0.1 | RpcServer("127.0.0.1", ...) |
同 |
| 配置缺失不报 value | missing = [k for k, v ... if not v] |
同 |
| fail-closed | 异常时 server 不存在 → 校验失败 | catch (std::exception&) → 返回 false |
8.3 RpcServer 安全特性(继承自 libmsg)
- JSON 解析嵌套深度限制 32 层
- 错误信息脱敏(不泄露内部路径/类型名)
recv_timeout防 Slowloris(默认 10s)- Handler 异常不导致服务崩溃
8.4 新增安全考量:SMTP 凭据管理
Python 版 SMTP 凭据从 .env 文件加载(gitignore 保护)。C++ 版支持两种凭据来源:
- confs smtp.ini(推荐):纳入 confs 统一管理,凭据与其他配置一起分发
- 环境变量(兼容):
SMTP_PASSWORD优先级最高,兼容 Docker secret
password 字段仅出现在 [smtp] 段,不出现在日志或错误响应中。配置加载失败时报告缺失的 key 名,不打印值。
9. 新增/修改文件清单
新增
smtp2/
├── src/
│ └── main.cpp # ~550 行,全部实现
├── inc/
│ └── LogC.hpp # ~60 行(与 captcha2 共用同一份)
├── Makefile # ~30 行
├── _set.sh # make + install(~10 行)
├── _run.sh # 启动脚本(~5 行)
├── test_send.cpp # SmtpClient 独立测试(~50 行)
└── README.md # 模块文档
需修改
| 文件 | 修改内容 |
|---|---|
confs/configs/smtp.ini |
新增,smtp 统一配置文件 |
core/init.txt |
smtp/_run.sh → smtp2/_run.sh |
docs/INDEX.md |
添加本文件的索引条目 |
零改动
gateway / waf / db / 其他服务均零改动。接口字节级兼容。
10. 依赖清单
| 依赖 | 用途 | 是否新增 | 安装方式 |
|---|---|---|---|
libus_msg.so (libmsg) |
RPC 服务端 + 异步日志 | 否 | 已通过 _inst.sh 安装 |
libboost_json |
JSON 解析 | 否 | 已通过 _setup.deb.sh 安装 |
libboost_program_options |
命令行参数 | 否 | 已通过 _setup.deb.sh 安装 |
libus_crypt.so (libcrypt) |
Base64 编码 | 否 | 已通过 _inst.sh 安装 |
libssl.so (OpenSSL) |
SMTP_SSL TLS 会话 | 否 | 已通过 _setup.deb.sh 安装(libssl-dev) |
libcrypto.so (OpenSSL) |
常数时间比较 CRYPTO_memcmp |
否 | 随 libssl 安装 |
零新增系统依赖。 OpenSSL 已是项目所有 C++ 模块的共有依赖(libhttp / libcrypt 均链接 -lssl -lcrypto),无须额外 apt install。
11. 与 Python 版的差异汇总
| 项目 | Python 版 | C++ 版 | 影响 |
|---|---|---|---|
| 运行时 | Python 3 + venv | 单二进制 smtp2.bin |
消除 Python 依赖 |
| 随机数 | random.randint(0,9) |
mt19937(random_device()) |
安全增强(P2-F3) |
| 日志内容 | email: 验证码发送成功(123456) |
email: 验证码发送成功 |
审计 P2-F2 修复 |
| 限流回滚 | now 变量遮蔽 → 预占回滚失效 |
ts_id 精确匹配回滚 |
P1-F1 根治 |
| 验证码比较 | 普通 == |
CRYPTO_memcmp 常数时间 |
P3-F4 修复 |
| 邮箱校验 | 仅 "@" in email |
与 gateway 一致的严格正则 | P3-F5 修复 |
| 输入长度 | subject/content 无上限 | 256 / 4096 字节硬上限 | P3-F6 修复 |
| 错误响应 | 含 SMTP 服务器原始错误 | 通用文案脱敏 | P3-F7 修复 |
| 进程模型 | Python GIL(仅 workers=4 时 I/O 并发受限) |
原生 C++ 多线程 | 真实并发能力 |
| 配置格式 | 手动解析 .env(4 种格式) |
confs ini + 环境变量覆盖 | 标准化,与 gateway 一致 |
| 凭据管理 | .env 文件 |
confs smtp.ini 或环境变量 | 纳入 confs 统一管理 |
12. 风险与应对
| 风险 | 概率 | 影响 | 应对 |
|---|---|---|---|
| OpenSSL SMTP_SSL 实现有协议细节疏漏(如 AUTH LOGIN 编码、响应码多行处理) | 中 | 高 | P1 独立测试程序 + 至少 163.com 实战验证;参考 smtplib 源码的协议状态机 |
SMTP 响应码 250 含多行文本(如 250-SIZE 104857600\r\n250 AUTH LOGIN PLAIN),单行解析丢失信息 |
低 | 中 | 按 RFC 5321 §4.2 实现多行响应解析(xxx- 开始,xxx 结束) |
| 不同 SMTP 服务商(163/QQ)TLS 行为差异 | 低 | 中 | P1 阶段多服务商验证;端口 465(隐式 TLS)仅需标准 TLS 握手 |
| gateway 联调时 pymsg 协议兼容性问题 | 极低 | 高 | libmsg 已在 db2/dnfs2/captcha2/fsys 中经过充分验证 |
ts_id 溢出(uint64_t,每秒 100 万次可用 58 万年) |
无 | 无 | 理论上不可能 |
12.1 SMTP 多行响应处理
Python smtplib 内部处理了 RFC 5321 的多行响应格式(250-SIZE\r\n250-AUTH\r\n250 OK)。C++ 实现需同等处理:
// SMTP 响应 ⇒ 提取最终响应码
// RFC 5321: "250-SIZE\r\n250-AUTH\r\n250 OK" ⇒ 码=250
std::pair<int, std::string> parse_smtp_response(const std::string& raw) {
// 最后一行格式: "250 OK"(空格分隔,非 '-')
// 中间行格式: "250-SIZE"('-' 表示后续还有续行)
...
}
在 P1 阶段独立测试程序中加入多行响应的单元测试。
A. 附录:与 captcha2 重写的对比
| 维度 | captcha2 | smtp2 |
|---|---|---|
| Python 源码行数 | 299 行 | 317 行 |
| 核心难点 | FreeType 字体渲染 + 像素旋转(CPU 密集,~180 行) | SMTP_SSL 客户端 + 协议状态机(网络 I/O,~100 行) |
| 新增外部依赖 | FreeType(libfreetype-dev) | 零(全部复用现有依赖) |
| 审计修复项 | P0-1 大小写 / P2-2 tolower UB | 7 项全部未修复(P1-F1/P2-F2/P2-F3/P3-F4~F7) |
| 预估工时 | 16h | 12.5h |
| 安全随机 | mt19937(random_device) |
同 |
| 日志客户端 | LogC.hpp(AsyncQueue) | 同(共用 captcha2 版本) |
| 配置体系 | confs ini + env | confs ini + env(新增向后兼容 __PASSWORD) |
smtp2 相比 captcha2 的显著优势:零新增系统依赖(OpenSSL 已就位),且解决了 Python 版积压的全部 7 项审计发现,属于"安全债务集中清偿"型重写。
B. 附录:Python smtplib.SMTP_SSL 源码参考路径
- Python 标准库:
Lib/smtplib.py(SMTP_SSL类,约 L1010-1060) - 关键方法:
__init__(socket + SSL wrap)、login(AUTH LOGIN 编码)、sendmail(MAIL/RCPT/DATA 序列)、quit、docmd(发送命令并读响应) - 响应解析:
getreply()处理多行123-续行格式
C++ 实现不需要完全复刻 smtplib 的全部功能(如 STARTTLS、8BITMIME),仅需覆盖 smtp 模块实际使用的子集:隐式 TLS(465 端口)、AUTH LOGIN、MAIL FROM/RCPT TO/DATA/QUIT。
C. 附录:SMTP 响应码速查
| 码 | 含义 | 预期阶段 |
|---|---|---|
| 220 | Service ready | 连接 greeting |
| 250 | OK | EHLO / MAIL FROM / RCPT TO / DATA 结束 |
| 334 | Send auth data | AUTH LOGIN 的用户名/密码提示 |
| 235 | Authentication successful | AUTH 完成 |
| 354 | Start mail input | DATA 后的就绪信号 |
| 221 | Service closing | QUIT |
| 421 | Service unavailable | 临时故障(应重试) |
| 450 | Mailbox unavailable | 邮箱不可用 |
| 535 | Authentication failed | 认证失败 |
| 550 | Mailbox not found | 邮箱不存在 |
所有非 2xx/3xx 响应均为错误,C++ 版逐码抛出含响应行的 std::runtime_error,由上层 handler 捕获后脱敏返回。