"""对 /api/v1 请求体解密、响应体加密(需配置 API_AES_KEY)。 使用纯 ASGI 中间件,避免 BaseHTTPMiddleware 读取 body 后缓存导致路由拿不到明文。 """ from __future__ import annotations from starlette.types import ASGIApp, Message, Receive, Scope, Send from app.config import get_settings from app.core.aes_crypto import ( ENCRYPTED_HEADER, is_encrypted_envelope, parse_aes_key, unwrap_encrypted, wrap_encrypted, ) _API_PREFIX = "/api/v1" _api_aes_key: bytes | None = None _api_aes_key_initialized = False def get_api_aes_key() -> bytes | None: print("get_api_aes_key",get_settings().api_aes_key) global _api_aes_key, _api_aes_key_initialized if not _api_aes_key_initialized: _api_aes_key = parse_aes_key(get_settings().api_aes_key) _api_aes_key_initialized = True return _api_aes_key async def _read_body(receive: Receive) -> bytes: body = b"" more = True while more: message = await receive() if message["type"] != "http.request": continue body += message.get("body", b"") more = message.get("more_body", False) return body def _make_receive(body: bytes) -> Receive: sent = False async def receive() -> Message: nonlocal sent if sent: return {"type": "http.disconnect"} sent = True return {"type": "http.request", "body": body, "more_body": False} return receive def _scope_with_content_length(scope: Scope, length: int) -> Scope: new_scope = dict(scope) headers: list[tuple[bytes, bytes]] = [] for name, value in scope.get("headers", []): if name.lower() != b"content-length": headers.append((name, value)) headers.append((b"content-length", str(length).encode())) new_scope["headers"] = headers return new_scope def _header_value(scope: Scope, name: str) -> str | None: target = name.lower().encode() for key, value in scope.get("headers", []): if key.lower() == target: return value.decode("latin-1") return None def _is_json_response(headers: list[tuple[bytes, bytes]]) -> bool: for key, value in headers: if key.lower() == b"content-type": return b"json" in value.lower() return False def _filter_response_headers(headers: list[tuple[bytes, bytes]]) -> list[tuple[bytes, bytes]]: skip = {b"content-length", b"content-encoding"} return [(k, v) for k, v in headers if k.lower() not in skip] async def _send_plain_json(send: Send, scope: Scope, status: int, payload: bytes) -> None: await send( { "type": "http.response.start", "status": status, "headers": [(b"content-type", b"application/json; charset=utf-8")], } ) await send({"type": "http.response.body", "body": payload, "more_body": False}) def _encrypting_send(send: Send, key: bytes) -> Send: status = 200 headers: list[tuple[bytes, bytes]] = [] chunks: list[bytes] = [] started = False async def wrapper(message: Message) -> None: nonlocal status, headers, started if message["type"] == "http.response.start": started = True status = message["status"] headers = list(message.get("headers", [])) return if message["type"] != "http.response.body": await send(message) return chunks.append(message.get("body", b"")) if message.get("more_body", False): return body = b"".join(chunks) out_headers = _filter_response_headers(headers) if _is_json_response(headers): try: body = wrap_encrypted(body, key) except Exception as exc: err = ( '{"ok":false,"message":"响应加密失败: ' + str(exc).replace('"', '\\"') + '"}' ).encode("utf-8") await _send_plain_json(send, {}, 500, err) return out_headers.append((ENCRYPTED_HEADER.encode(), b"1")) out_headers.append((b"content-length", str(len(body)).encode())) await send({"type": "http.response.start", "status": status, "headers": out_headers}) await send({"type": "http.response.body", "body": body, "more_body": False}) return wrapper class ApiCryptoMiddleware: """纯 ASGI 中间件(勿使用 BaseHTTPMiddleware)。""" def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http": await self.app(scope, receive, send) return key = get_api_aes_key() path = scope.get("path", "") if key is None or not path.startswith(_API_PREFIX): await self.app(scope, receive, send) return method = scope.get("method", "GET") if method in ("POST", "PUT", "PATCH"): body = await _read_body(receive) if ( body and _header_value(scope, ENCRYPTED_HEADER) == "1" and is_encrypted_envelope(body) ): try: body = unwrap_encrypted(body, key) except Exception as exc: err = f'{{"ok":false,"message":"请求解密失败: {exc}"}}'.encode("utf-8") await _send_plain_json(send, scope, 400, err) return receive = _make_receive(body) scope = _scope_with_content_length(scope, len(body)) await self.app(scope, receive, _encrypting_send(send, key))