""" micro_servers 黑盒测试 — 共享工具模块。 提供: HTTP 请求封装、断言工具、测试计数器、pymsg 协议客户端。 纯标准库实现,零外部依赖。 """ import http.client import json import random import socket import struct import time import urllib.request import urllib.error import urllib.parse import ssl # --------------------------------------------------------------------------- # 测试计数器 # --------------------------------------------------------------------------- class Counter: """每个测试模块一个计数器实例,统计 passed/failed/skipped。""" def __init__(self): self.passed = 0 self.failed = 0 self.skipped = 0 self._failures = [] def add_pass(self): self.passed += 1 def add_fail(self, name: str, reason: str = ""): self.failed += 1 self._failures.append((name, reason)) def add_skip(self): self.skipped += 1 @property def total(self): return self.passed + self.failed + self.skipped def summary(self) -> str: return f"{self.total} tests: {self.passed} passed, {self.failed} failed, {self.skipped} skipped" def failures(self) -> list: return self._failures # --------------------------------------------------------------------------- # 断言工具 # --------------------------------------------------------------------------- def assert_eq(actual, expected, name: str, counter: Counter): """断言 actual == expected。""" if actual == expected: print(f" [PASS] {name}") counter.add_pass() return True else: print(f" [FAIL] {name}: expected {expected!r}, got {actual!r}") counter.add_fail(name, f"expected {expected!r}, got {actual!r}") return False def assert_contains(haystack: str, needle: str, name: str, counter: Counter): """断言 needle in haystack(不区分大小写)。""" if needle.lower() in haystack.lower(): print(f" [PASS] {name}") counter.add_pass() return True else: snippet = haystack[:200] if len(haystack) > 200 else haystack print(f" [FAIL] {name}: '{needle}' not found in response") counter.add_fail(name, f"'{needle}' not found in response") return False def assert_status(status: int, expected: int, name: str, counter: Counter): """断言 HTTP 状态码。""" if status == expected: print(f" [PASS] {name}") counter.add_pass() return True else: print(f" [FAIL] {name}: expected status {expected}, got {status}") counter.add_fail(name, f"expected status {expected}, got {status}") return False def assert_true(condition: bool, name: str, counter: Counter, detail: str = ""): """断言条件为真。""" if condition: print(f" [PASS] {name}") counter.add_pass() return True else: msg = f" [FAIL] {name}" if detail: msg += f": {detail}" print(msg) counter.add_fail(name, detail) return False def assert_json_key(data: dict, key: str, name: str, counter: Counter): """断言 JSON 对象包含指定 key。""" if key in data: print(f" [PASS] {name}") counter.add_pass() return True else: print(f" [FAIL] {name}: key '{key}' not found in {json.dumps(data)[:200]}") counter.add_fail(name, f"key '{key}' not found") return False def skip(name: str, reason: str, counter: Counter): """标记测试为跳过。""" print(f" [SKIP] {name}: {reason}") counter.add_skip() # --------------------------------------------------------------------------- # HTTP 请求封装 # --------------------------------------------------------------------------- def http_request(method: str, url: str, *, body: dict = None, headers: dict = None, timeout: float = 10.0, raw_body: bytes = None, raw_body_content_type: str = None): """发送 HTTP 请求,返回 (status, body_str, resp_headers)。 支持: - JSON body (body 参数) - 原始二进制 body (raw_body + raw_body_content_type) - 任意自定义 headers - 超时控制 """ parsed = urllib.parse.urlparse(url) data = None hdrs = {} if headers: hdrs.update(headers) if raw_body is not None: data = raw_body if raw_body_content_type: hdrs["Content-Type"] = raw_body_content_type elif body is not None: data = json.dumps(body).encode("utf-8") hdrs["Content-Type"] = "application/json" ctx = ssl._create_unverified_context() if parsed.scheme == "https" else None req = urllib.request.Request(url, data=data, headers=hdrs, method=method) try: with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp: resp_body = resp.read().decode("utf-8", errors="replace") resp_headers = dict(resp.headers) return resp.status, resp_body, resp_headers except urllib.error.HTTPError as e: resp_body = e.read().decode("utf-8", errors="replace") if e.fp else "" resp_headers = dict(e.headers) if e.headers else {} return e.code, resp_body, resp_headers except Exception as e: return 0, str(e), {} def http_get(url: str, headers: dict = None, timeout: float = 10.0): """HTTP GET 请求。""" return http_request("GET", url, headers=headers, timeout=timeout) def http_post(url: str, body: dict = None, headers: dict = None, timeout: float = 10.0): """HTTP POST 请求(JSON body)。""" return http_request("POST", url, body=body, headers=headers, timeout=timeout) def http_post_raw(url: str, raw_body: bytes, content_type: str, headers: dict = None, timeout: float = 10.0): """HTTP POST 请求(原始二进制 body)。""" return http_request("POST", url, raw_body=raw_body, raw_body_content_type=content_type, headers=headers, timeout=timeout) def try_parse_json(text: str): """尝试解析 JSON,失败返回 None。""" try: return json.loads(text) except (json.JSONDecodeError, ValueError): return None # --------------------------------------------------------------------------- # pymsg 协议客户端 # --------------------------------------------------------------------------- class PymsgClient: """pymsg TCP 协议客户端(用于直接与 db:8005 通信)。 协议格式: [4字节大端长度][UTF-8 JSON payload] """ def __init__(self, host: str = "127.0.0.1", port: int = 8005, name: str = "test_client"): self.host = host self.port = port self.name = name self._sock = None def _ensure_connected(self): if self._sock is None: self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.settimeout(10) self._sock.connect((self.host, self.port)) def _close(self): if self._sock: try: self._sock.close() except Exception: pass self._sock = None def _send_frame(self, data: str): """发送一帧 pymsg 数据。""" payload = data.encode("utf-8") header = struct.pack(">I", len(payload)) self._sock.sendall(header + payload) def _recv_frame(self, timeout: float = 10.0) -> str: """接收一帧 pymsg 数据。""" self._sock.settimeout(timeout) header = self._recv_exact(4, timeout) if header is None: raise ConnectionError("Failed to receive pymsg header") length = struct.unpack(">I", header)[0] payload = self._recv_exact(length, timeout) if payload is None: raise ConnectionError("Failed to receive pymsg payload") return payload.decode("utf-8") def _recv_exact(self, n: int, timeout: float = 10.0): """精确接收 n 字节。""" self._sock.settimeout(timeout) data = b"" while len(data) < n: try: chunk = self._sock.recv(n - len(data)) except socket.timeout: return None if not chunk: return None data += chunk return data def call(self, payload: dict, timeout: float = 10.0) -> dict: """发送请求并接收响应(短连接模式)。 payload 必须包含 "_route": "execute"。 返回 {"success": true/false, "data": [...], "error": "..."} """ try: self._ensure_connected() msg = json.dumps(payload) self._send_frame(msg) resp_text = self._recv_frame(timeout) return json.loads(resp_text) except Exception as e: return {"success": False, "error": str(e)} finally: self._close() # --------------------------------------------------------------------------- # 服务可用性检查 # --------------------------------------------------------------------------- def check_tcp_service(host: str, port: int, timeout: float = 5.0) -> tuple: """检查纯 TCP 服务(如 pymsg 协议服务 db/captcha/smtp)是否可达。 返回 (reachable: bool, detail: str)。 """ try: with socket.create_connection((host, port), timeout=timeout): return True, "TCP 连接成功" except ConnectionRefusedError: return False, "连接被拒绝 (服务未启动或端口未监听)" except socket.timeout: return False, f"TCP 连接超时 ({timeout}s)" except OSError as e: return False, f"网络错误: {e}" def check_service(url: str, timeout: float = 8.0) -> tuple: """检查服务是否可达(HTTP 请求检测,与测试实际行为一致)。 返回 (reachable: bool, detail: str)。 任何 HTTP 响应(包括 200/302/401/403 等)均视为可达。 """ parsed = urllib.parse.urlparse(url) ctx = ssl._create_unverified_context() if parsed.scheme == "https" else None try: req = urllib.request.Request(url, method="HEAD") with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp: return True, f"HTTP {resp.status}" except urllib.error.HTTPError as e: # 任何 HTTP 错误码都说明服务在运行 return True, f"HTTP {e.code}" except urllib.error.URLError as e: reason = str(e.reason) if hasattr(e, 'reason') else str(e) return False, reason except socket.timeout: return False, f"HTTP 请求超时 ({timeout}s)" except ConnectionRefusedError: return False, "连接被拒绝 (服务未启动或端口未监听)" except socket.gaierror: return False, "DNS 解析失败" except OSError as e: return False, f"网络错误: {e.strerror if hasattr(e, 'strerror') else str(e)}" except Exception as e: return False, str(e) # --------------------------------------------------------------------------- # gateway 测试专用工具 # --------------------------------------------------------------------------- def random_xff() -> str: """生成随机 X-Forwarded-For 值,用于隔离 gateway 的 IP 级限流桶。 背景:gateway 直连模式下信任客户端 XFF 头(审计发现 A4),每个测试场景 使用独立随机 XFF,可避免限流用例之间互相消耗配额(如登录 5 次/5 分钟)。 """ return f"10.{random.randint(1, 254)}.{random.randint(1, 254)}.{random.randint(1, 254)}" # 响应体泄露特征:源码文件名/路径/C++ 类型签名(P0-2/P0-3 脱敏回归基线) _LEAK_PATTERNS = ("main.cpp", ".hpp", ".cpp:", "/opt/", "boost::", "std::", "source_location", "typeid", "__cxa") def assert_no_internal_leak(body: str, name: str, counter: Counter): """断言响应体不含内部实现信息(源码路径/C++ 类型名等,P0-2/P0-3 回归)。""" hits = [p for p in _LEAK_PATTERNS if p in body] if not hits: print(f" [PASS] {name}") counter.add_pass() return True else: print(f" [FAIL] {name}: 响应泄露内部信息 {hits}") counter.add_fail(name, f"leak patterns {hits}") return False def db_execute(action: str, table: str, *, where: dict = None, values: dict = None, host: str = "127.0.0.1", port: int = 8005, timeout: float = 10.0) -> dict: """通过 pymsg 调 db execute 路由(测试数据准备/清理用)。 返回 db 完整响应 {"success": ..., "data": ...};db 不可达时 返回 {"success": False, "error": ...}(PymsgClient 内部兜底)。 """ payload = {"_route": "execute", "action": action, "table": table} if where is not None: payload["where"] = where if values is not None: payload["values"] = values return PymsgClient(host, port, "test_blackbox").call(payload, timeout)