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: 开发方案、安全审计报告、交叉引用更新
167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
#!/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)
|