feat: captcha C++20 重写 + 安全审计修复 (P0/P1/P2/P3, 10/11 已修复)
captcha2 — C++20 验证码服务(FreeType + stb_image_write): - 完整实现: 图片生成、CaptchaStore、RateLimiter、pymsg RPC 路由 - 协议完全兼容 Python 原版,gateway 零改动 审计修复 (captcha2_SECURITY_AUDIT_20260809.md): - P0-1: 存储答案小写化 (对齐 Python 版语义) - P1-1: gateway 按 IP 限流 10/min + captcha2 全局限流 600/min 兜底 - P2-1: generate_text mt19937 → std::random_device - P2-2: ::tolower UB → unsigned char lambda - P3-1: parse_int 辅助函数, stoi 异常可诊断 - P3-2: 显式 #include <iomanip> - P3-3: 补齐 CAPTCHA_LEN/TTL/RATE_LIMIT 环境变量 - P3-4: LogC 日志超时 5.0→1.0s - P3-5: 限流/满容抛异常, gateway 映射 429/503 - P3-6: 清理死代码 集成: - core/init.txt: captcha → captcha2 - confs configs/captcha.ini: 新增 max_store/rate_limit_per_min - docs: 开发方案、安全审计报告、交叉引用更新
This commit is contained in:
1 parent
29e4f96061
commit
546394b681
15 files changed
+3959
-4
No files matched your search
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
test_rpc.py — captcha2 pymsg RPC 功能验证脚本
|
||||
|
||||
用法: python3 test_rpc.py [host] [port]
|
||||
默认: python3 test_rpc.py 127.0.0.1 8009
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
_here = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(_here, "..", "pymsg"))
|
||||
from pymsg import RpcClient # noqa: E402
|
||||
|
||||
HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
|
||||
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 8009
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
|
||||
def test(name: str, expect_ok: bool, fn):
|
||||
global passed, failed
|
||||
try:
|
||||
result = fn()
|
||||
# RPC 调用成功 = 返回 dict 且没有 error 字段(或 error 是预期的一部分)
|
||||
rpc_ok = isinstance(result, dict)
|
||||
if expect_ok == rpc_ok:
|
||||
print(f" ✓ {name} → {result}")
|
||||
passed += 1
|
||||
return result
|
||||
else:
|
||||
print(f" ✗ {name} — 预期 {'成功' if expect_ok else '失败'},实际: {result}")
|
||||
failed += 1
|
||||
return result
|
||||
except Exception as e:
|
||||
if not expect_ok:
|
||||
print(f" ✓ {name} (预期异常: {e})")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ✗ {name} — 异常: {e}")
|
||||
failed += 1
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
global passed, failed
|
||||
print(f"captcha2 RPC 功能测试 — tcp://{HOST}:{PORT}")
|
||||
print("=" * 60)
|
||||
|
||||
client = RpcClient(HOST, PORT, "test-captcha2", timeout=5.0)
|
||||
|
||||
# ── 1. health ──
|
||||
print("\n[1] health 检查")
|
||||
r = test("health 返回 ok", True, lambda: client.call("health"))
|
||||
if r:
|
||||
print(f" 服务: {r.get('service')}, 状态: {r.get('status')}")
|
||||
|
||||
# ── 2. image 生成 ──
|
||||
print("\n[2] image 验证码生成")
|
||||
r = test("image 返回 token+image", True, lambda: client.call("image"))
|
||||
token = ""
|
||||
if r:
|
||||
token = r.get("token", "")
|
||||
img_b64 = r.get("image", "")
|
||||
print(f" token: {token[:16]}... (len={len(token)})")
|
||||
print(f" image base64: {len(img_b64)} 字符")
|
||||
|
||||
# 解码保存 PNG
|
||||
try:
|
||||
img_bytes = base64.b64decode(img_b64)
|
||||
fname = f"/tmp/captcha2_test_{token[:8]}.png"
|
||||
with open(fname, "wb") as f:
|
||||
f.write(img_bytes)
|
||||
print(f" PNG 已保存: {fname} ({len(img_bytes)} bytes)")
|
||||
except Exception as e:
|
||||
print(f" ⚠ PNG 保存失败: {e}")
|
||||
|
||||
# ── 3. verify 校验 ──
|
||||
print("\n[3] verify 校验")
|
||||
|
||||
# 3a. 正向校验(仅在终端交互模式下)
|
||||
if sys.stdin.isatty():
|
||||
print(" 生成验证码用于正向校验...")
|
||||
r2 = client.call("image")
|
||||
token2 = r2.get("token", "")
|
||||
if token2 and r2.get("image"):
|
||||
img_bytes = base64.b64decode(r2["image"])
|
||||
fname = f"/tmp/captcha2_manual_{token2[:8]}.png"
|
||||
with open(fname, "wb") as f:
|
||||
f.write(img_bytes)
|
||||
print(f" 请查看图片: {fname}")
|
||||
print(f" 输入图片中的 4 位字符(不区分大小写): ", end="", flush=True)
|
||||
try:
|
||||
user_answer = sys.stdin.readline().strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
user_answer = ""
|
||||
if user_answer:
|
||||
vr = client.call("verify", {"token": token2, "answer": user_answer})
|
||||
is_valid = bool(vr.get("valid", False))
|
||||
if is_valid:
|
||||
print(f" ✓ verify 正向校验 → {{'valid': True}}(答案 '{user_answer}' 正确)")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ✗ verify 正向校验 → {{'valid': False}}(答案 '{user_answer}' 未通过)")
|
||||
failed += 1
|
||||
else:
|
||||
print(f" ⚠ 跳过(无输入)")
|
||||
else:
|
||||
print(" ⚠ 非交互模式,跳过正向校验(需人工读取 PNG)")
|
||||
|
||||
# 3b. 错误答案 → RPC 成功返回 valid=false
|
||||
test("verify 错误答案 → valid=false", True,
|
||||
lambda: client.call("verify", {"token": token, "answer": "ZZZZ"}))
|
||||
|
||||
# 3c. 大小写不敏感(P0 修复验证)
|
||||
r3 = client.call("image")
|
||||
token3 = r3.get("token", "")
|
||||
if token3:
|
||||
# 输入全大写,验证大小写不敏感(P0 修复后应失效——答案存的是小写)
|
||||
# 这里仅验证 RPC 调用不崩溃
|
||||
test("verify 全大写答案 → RPC 成功", True,
|
||||
lambda: client.call("verify", {"token": token3, "answer": "ABCD"}))
|
||||
|
||||
# 3d. 缺参数 → RPC 成功返回 valid=false + error
|
||||
test("verify 缺参数 → valid=false", True,
|
||||
lambda: client.call("verify", {}))
|
||||
|
||||
# 3e. 无效 token → RPC 成功返回 valid=false + error
|
||||
test("verify 无效 token → valid=false", True,
|
||||
lambda: client.call("verify", {"token": "deadbeef" * 8, "answer": "abcd"}))
|
||||
|
||||
# ── 4. 速率限制压力测试(P1-1: 全局限流 600/min,70 次应全部通过;gateway 层按 IP 限流 10/min) ──
|
||||
print("\n[4] captcha2 自身全局限流(阈值 600/min,70 次请求)")
|
||||
ok_count = 0
|
||||
rate_limited = 0
|
||||
for i in range(70):
|
||||
try:
|
||||
r = client.call("image")
|
||||
if r.get("token"):
|
||||
ok_count += 1
|
||||
elif not r.get("success", True):
|
||||
rate_limited += 1
|
||||
except Exception:
|
||||
rate_limited += 1
|
||||
print(f" 成功: {ok_count}, 被限: {rate_limited}")
|
||||
# P1-1 修复后,全局限流阈值调到 600,70 次应全部通过
|
||||
if ok_count == 70:
|
||||
print(f" ✓ 全局限流未误拦(70/70 通过,阈值 600/min 正确)")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ✗ 预期 70 次全通过,实际成功 {ok_count}")
|
||||
failed += 1
|
||||
|
||||
# ── 结果汇总 ──
|
||||
print("\n" + "=" * 60)
|
||||
print(f"结果: {passed} 通过, {failed} 失败")
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ok = main()
|
||||
sys.exit(0 if ok else 1)
|
||||
Reference in new issue
Block a user