758 lines
29 KiB
Python
758 lines
29 KiB
Python
import json
|
||
from collections.abc import Awaitable, Callable
|
||
from urllib.parse import parse_qs, urlencode
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.datastructures import Headers, QueryParams
|
||
from fastapi.responses import JSONResponse
|
||
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
||
|
||
from common.constant import HttpStatusConstant
|
||
from config.env import AppConfig, TransportCryptoConfig
|
||
from utils.transport_crypto_util import (
|
||
DecryptedTransportEnvelope,
|
||
TransportCryptoMonitorUtil,
|
||
TransportCryptoUtil,
|
||
TransportSecurityUtil,
|
||
)
|
||
|
||
|
||
class TransportCryptoMiddleware:
|
||
"""
|
||
传输层请求解密与响应加密中间件
|
||
"""
|
||
|
||
_ENCRYPT_REQUEST_HEADER = 'x-transport-encrypt'
|
||
_ENCRYPT_RESPONSE_HEADER = 'x-body-encrypted'
|
||
_ENCRYPT_ALG_HEADER = 'x-encrypt-alg'
|
||
_ENCRYPT_KID_HEADER = 'x-key-id'
|
||
_MONITOR_REQUEST_MODE_HEADER = 'x-transport-request-mode'
|
||
_MONITOR_RESPONSE_MODE_HEADER = 'x-transport-response-mode'
|
||
_MONITOR_STATUS_HEADER = 'x-transport-crypto-status'
|
||
_MONITOR_KID_HEADER = 'x-transport-key-id'
|
||
|
||
def __init__(self, app: ASGIApp) -> None:
|
||
"""
|
||
初始化传输层加解密中间件
|
||
|
||
:param app: FastAPI/Starlette应用对象
|
||
:return: None
|
||
"""
|
||
self.app = app
|
||
|
||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||
"""
|
||
拦截HTTP请求,按配置执行请求解密与响应加密
|
||
|
||
:param scope: 当前ASGI请求作用域
|
||
:param receive: ASGI receive函数
|
||
:param send: ASGI send函数
|
||
:return: None
|
||
"""
|
||
if scope['type'] != 'http' or not TransportCryptoConfig.transport_crypto_enabled:
|
||
await self.app(scope, receive, send)
|
||
return
|
||
|
||
current_app = scope.get('app')
|
||
path = self._normalize_path(str(scope.get('path', '')))
|
||
if (
|
||
self._is_excluded_path(path)
|
||
or TransportCryptoConfig.transport_crypto_mode == 'off'
|
||
or not self._is_enabled_path(path)
|
||
):
|
||
await self.app(scope, receive, send)
|
||
return
|
||
|
||
headers = Headers(scope=scope)
|
||
request_encrypted = headers.get(self._ENCRYPT_REQUEST_HEADER) == '1'
|
||
request_required = TransportCryptoConfig.transport_crypto_mode == 'required' or self._is_required_path(path)
|
||
|
||
if request_required and not request_encrypted:
|
||
await TransportCryptoMonitorUtil.record_plain_request(current_app)
|
||
await TransportCryptoMonitorUtil.record_required_rejected(
|
||
current_app, str(scope.get('method', '')).upper(), path
|
||
)
|
||
await TransportCryptoMonitorUtil.record_plain_response(current_app)
|
||
await self._send_error_response(
|
||
scope,
|
||
receive,
|
||
send,
|
||
'当前接口要求使用加密传输',
|
||
headers=self._build_monitor_headers(
|
||
request_mode='plain',
|
||
response_mode='plain',
|
||
crypto_status='required_missing',
|
||
),
|
||
)
|
||
return
|
||
|
||
if not request_encrypted:
|
||
await TransportCryptoMonitorUtil.record_plain_request(current_app)
|
||
response_observer = self._build_passthrough_response_observer(
|
||
app=current_app,
|
||
send=send,
|
||
request_mode='plain',
|
||
crypto_status='pass_through',
|
||
)
|
||
await self.app(scope, receive, response_observer)
|
||
return
|
||
|
||
body = await self._read_body(receive)
|
||
request = Request(scope, receive=self._build_receive(body))
|
||
try:
|
||
decrypted_scope, decrypted_body, crypto_context = await self._decrypt_request(scope, request, headers, body)
|
||
await TransportCryptoMonitorUtil.record_encrypted_request(current_app, str(crypto_context['kid']))
|
||
await TransportCryptoMonitorUtil.record_decrypt_success(current_app, str(crypto_context['kid']))
|
||
except Exception as exc:
|
||
error_crypto_context = self._build_error_crypto_context(scope, headers, body)
|
||
error_kid = (
|
||
str(error_crypto_context['kid'])
|
||
if error_crypto_context
|
||
else self._extract_request_kid(scope, headers, body)
|
||
)
|
||
await TransportCryptoMonitorUtil.record_encrypted_request(current_app, error_kid)
|
||
failure_reason = self._classify_failure_reason(str(exc))
|
||
await TransportCryptoMonitorUtil.record_decrypt_failure(
|
||
current_app,
|
||
method=str(scope.get('method', '')).upper(),
|
||
path=path,
|
||
reason=failure_reason,
|
||
kid=error_kid,
|
||
)
|
||
if error_crypto_context:
|
||
await TransportCryptoMonitorUtil.record_encrypted_response(current_app, error_kid, is_error=True)
|
||
else:
|
||
await TransportCryptoMonitorUtil.record_plain_response(current_app)
|
||
await self._send_error_response(
|
||
scope,
|
||
receive,
|
||
send,
|
||
str(exc) or '加密请求解析失败',
|
||
error_crypto_context,
|
||
headers=self._build_monitor_headers(
|
||
request_mode='encrypted',
|
||
response_mode='encrypted' if error_crypto_context else 'plain',
|
||
crypto_status=failure_reason,
|
||
kid=error_kid,
|
||
),
|
||
)
|
||
return
|
||
|
||
async def send_wrapper(message: Message) -> None:
|
||
await response_encryptor(message)
|
||
|
||
response_encryptor = self._build_response_encryptor(
|
||
app=current_app,
|
||
scope=decrypted_scope,
|
||
send=send,
|
||
crypto_context=crypto_context,
|
||
)
|
||
await self.app(decrypted_scope, self._build_receive(decrypted_body), send_wrapper)
|
||
|
||
async def _decrypt_request(
|
||
self,
|
||
scope: Scope,
|
||
request: Request,
|
||
headers: Headers,
|
||
body: bytes,
|
||
) -> tuple[Scope, bytes, dict[str, str | bytes | bool]]:
|
||
"""
|
||
解密请求并回写解密后的headers、query和body
|
||
|
||
:param scope: 当前ASGI请求作用域
|
||
:param request: FastAPI请求对象
|
||
:param headers: 当前请求头对象
|
||
:param body: 原始请求体字节串
|
||
:return: 解密后的scope、请求体与加密上下文
|
||
"""
|
||
new_scope = dict(scope)
|
||
new_scope['state'] = dict(scope.get('state', {}))
|
||
new_scope['headers'] = self._remove_header(new_scope.get('headers', []), b'accept-encoding')
|
||
content_type = headers.get('content-type', '')
|
||
|
||
query_envelope = self._extract_query_envelope(new_scope)
|
||
body_envelope = self._extract_body_envelope(content_type, body)
|
||
|
||
if query_envelope is None and body_envelope is None:
|
||
raise ValueError('未找到可解密的请求载荷')
|
||
|
||
crypto_context: dict[str, str | bytes | bool] | None = None
|
||
if query_envelope is not None:
|
||
decrypted_query = await self._decrypt_envelope(request, scope, query_envelope)
|
||
query_payload = self._loads_json_mapping(decrypted_query.plaintext.decode('utf-8'))
|
||
new_scope['query_string'] = urlencode(query_payload, doseq=True).encode('utf-8')
|
||
crypto_context = self._build_crypto_context(decrypted_query)
|
||
|
||
decrypted_body = body
|
||
if body_envelope is not None:
|
||
decrypted_body_payload = await self._decrypt_envelope(request, scope, body_envelope)
|
||
if crypto_context and crypto_context['kid'] != decrypted_body_payload.kid:
|
||
raise ValueError('请求中存在不一致的密钥版本')
|
||
if crypto_context and crypto_context['aes_key'] != decrypted_body_payload.aes_key:
|
||
raise ValueError('请求中存在不一致的会话密钥')
|
||
if crypto_context is None:
|
||
crypto_context = self._build_crypto_context(decrypted_body_payload)
|
||
|
||
if 'application/x-www-form-urlencoded' in content_type:
|
||
form_payload = self._loads_json_mapping(decrypted_body_payload.plaintext.decode('utf-8'))
|
||
decrypted_body = urlencode(form_payload, doseq=True).encode('utf-8')
|
||
else:
|
||
decrypted_body = decrypted_body_payload.plaintext
|
||
new_scope['headers'] = self._replace_header(
|
||
new_scope.get('headers', []), b'content-length', str(len(decrypted_body)).encode('utf-8')
|
||
)
|
||
|
||
if crypto_context is None:
|
||
raise ValueError('加密请求缺少可用的密钥上下文')
|
||
|
||
new_scope['state']['transport_crypto_context'] = crypto_context
|
||
return new_scope, decrypted_body, crypto_context
|
||
|
||
async def _decrypt_envelope(
|
||
self,
|
||
request: Request,
|
||
scope: Scope,
|
||
envelope: dict[str, str],
|
||
) -> DecryptedTransportEnvelope:
|
||
"""
|
||
解密单个请求信封并执行时间窗、防重放校验
|
||
|
||
:param request: 当前请求对象
|
||
:param scope: 当前ASGI请求作用域
|
||
:param envelope: 请求信封字典
|
||
:return: 解密后的请求信封对象
|
||
"""
|
||
decrypted_payload = TransportCryptoUtil.decrypt_envelope(
|
||
envelope,
|
||
expected_method=str(scope.get('method', '')).upper(),
|
||
expected_path=self._normalize_path(str(scope.get('path', ''))),
|
||
)
|
||
TransportSecurityUtil.validate_timestamp(decrypted_payload.timestamp)
|
||
await TransportSecurityUtil.validate_replay(request, decrypted_payload.kid, decrypted_payload.nonce)
|
||
return decrypted_payload
|
||
|
||
def _extract_query_envelope(self, scope: Scope) -> dict[str, str] | None:
|
||
"""
|
||
从查询参数中提取加密信封
|
||
|
||
:param scope: 当前ASGI请求作用域
|
||
:return: 查询参数中的信封字典,不存在时返回None
|
||
"""
|
||
query_params = QueryParams(scope.get('query_string', b'').decode('utf-8'))
|
||
encrypted_query = query_params.get('__enc')
|
||
if not encrypted_query:
|
||
return None
|
||
return TransportCryptoUtil.decode_query_envelope(encrypted_query)
|
||
|
||
def _extract_body_envelope(self, content_type: str, body: bytes) -> dict[str, str] | None:
|
||
"""
|
||
根据内容类型从请求体中提取加密信封
|
||
|
||
:param content_type: 当前请求内容类型
|
||
:param body: 原始请求体字节串
|
||
:return: 请求体中的信封字典,不存在时返回None
|
||
"""
|
||
if not body or 'multipart/form-data' in content_type:
|
||
return None
|
||
|
||
if 'application/json' in content_type:
|
||
body_payload = json.loads(body.decode('utf-8'))
|
||
if not isinstance(body_payload, dict):
|
||
raise ValueError('加密请求体格式不合法')
|
||
return body_payload
|
||
|
||
if 'application/x-www-form-urlencoded' in content_type:
|
||
parsed_form = parse_qs(body.decode('utf-8'), keep_blank_values=True)
|
||
body_envelope = {
|
||
key: values[-1] if isinstance(values, list) else values for key, values in parsed_form.items()
|
||
}
|
||
aad = body_envelope.get('aad')
|
||
if isinstance(aad, str) and aad:
|
||
try:
|
||
parsed_aad = json.loads(aad)
|
||
if isinstance(parsed_aad, dict):
|
||
body_envelope['aad'] = parsed_aad
|
||
except json.JSONDecodeError:
|
||
pass
|
||
return body_envelope
|
||
|
||
return None
|
||
|
||
def _build_response_encryptor(
|
||
self,
|
||
app: FastAPI | None,
|
||
scope: Scope,
|
||
send: Send,
|
||
crypto_context: dict[str, str | bytes | bool],
|
||
) -> Callable[[Message], Awaitable[None]]:
|
||
"""
|
||
构建响应加密发送器,仅对JSON响应执行加密
|
||
|
||
:param scope: 当前ASGI请求作用域
|
||
:param send: ASGI send函数
|
||
:param crypto_context: 当前请求加密上下文
|
||
:return: 包装后的ASGI send函数
|
||
"""
|
||
response_start_message: Message | None = None
|
||
buffered_json_body: list[bytes] = []
|
||
should_buffer_json = False
|
||
|
||
async def _encrypt_response(message: Message) -> None:
|
||
nonlocal response_start_message, should_buffer_json
|
||
|
||
if message['type'] == 'http.response.start':
|
||
response_start_message = message
|
||
headers = Headers(raw=message.get('headers', []))
|
||
content_type = headers.get('content-type', '')
|
||
should_buffer_json = 'application/json' in content_type
|
||
if not should_buffer_json:
|
||
await TransportCryptoMonitorUtil.record_plain_response(app)
|
||
await send(
|
||
{
|
||
**message,
|
||
'headers': self._merge_response_headers(
|
||
message.get('headers', []),
|
||
self._build_monitor_headers(
|
||
request_mode='encrypted',
|
||
response_mode='plain',
|
||
crypto_status='ok',
|
||
kid=str(crypto_context['kid']),
|
||
),
|
||
),
|
||
}
|
||
)
|
||
return
|
||
|
||
if message['type'] != 'http.response.body':
|
||
await send(message)
|
||
return
|
||
|
||
if not should_buffer_json or response_start_message is None:
|
||
await send(message)
|
||
return
|
||
|
||
buffered_json_body.append(message.get('body', b''))
|
||
if message.get('more_body', False):
|
||
return
|
||
|
||
encrypted_body = TransportCryptoUtil.encrypt_response_body(
|
||
aes_key=crypto_context['aes_key'],
|
||
payload=b''.join(buffered_json_body),
|
||
kid=str(crypto_context['kid']),
|
||
method=str(scope.get('method', '')),
|
||
path=self._normalize_path(str(scope.get('path', ''))),
|
||
)
|
||
response_headers = self._replace_header(
|
||
response_start_message.get('headers', []),
|
||
b'content-length',
|
||
str(len(encrypted_body)).encode('utf-8'),
|
||
)
|
||
response_headers = self._replace_header(response_headers, b'content-type', b'application/json')
|
||
response_headers = self._replace_header(
|
||
response_headers, self._ENCRYPT_RESPONSE_HEADER.encode('utf-8'), b'1'
|
||
)
|
||
response_headers = self._replace_header(
|
||
response_headers,
|
||
self._ENCRYPT_ALG_HEADER.encode('utf-8'),
|
||
TransportCryptoUtil.get_response_envelope_algorithm().encode('utf-8'),
|
||
)
|
||
response_headers = self._replace_header(
|
||
response_headers,
|
||
self._ENCRYPT_KID_HEADER.encode('utf-8'),
|
||
str(crypto_context['kid']).encode('utf-8'),
|
||
)
|
||
response_headers = self._merge_response_headers(
|
||
response_headers,
|
||
self._build_monitor_headers(
|
||
request_mode='encrypted',
|
||
response_mode='encrypted',
|
||
crypto_status='ok',
|
||
kid=str(crypto_context['kid']),
|
||
),
|
||
)
|
||
await TransportCryptoMonitorUtil.record_encrypted_response(app, str(crypto_context['kid']))
|
||
await send({**response_start_message, 'headers': response_headers})
|
||
await send({'type': 'http.response.body', 'body': encrypted_body, 'more_body': False})
|
||
|
||
return _encrypt_response
|
||
|
||
def _build_passthrough_response_observer(
|
||
self,
|
||
app: FastAPI | None,
|
||
send: Send,
|
||
request_mode: str,
|
||
crypto_status: str,
|
||
kid: str | None = None,
|
||
) -> Callable[[Message], Awaitable[None]]:
|
||
"""
|
||
构建明文响应观察器,为响应追加监控诊断头
|
||
|
||
:param send: ASGI send函数
|
||
:param request_mode: 请求传输模式
|
||
:param crypto_status: 当前传输层处理状态
|
||
:param kid: 可选的密钥版本
|
||
:return: 包装后的ASGI send函数
|
||
"""
|
||
has_recorded_response = False
|
||
|
||
async def _observe_response(message: Message) -> None:
|
||
nonlocal has_recorded_response
|
||
|
||
if message['type'] == 'http.response.start':
|
||
if not has_recorded_response:
|
||
await TransportCryptoMonitorUtil.record_plain_response(app)
|
||
has_recorded_response = True
|
||
await send(
|
||
{
|
||
**message,
|
||
'headers': self._merge_response_headers(
|
||
message.get('headers', []),
|
||
self._build_monitor_headers(
|
||
request_mode=request_mode,
|
||
response_mode='plain',
|
||
crypto_status=crypto_status,
|
||
kid=kid,
|
||
),
|
||
),
|
||
}
|
||
)
|
||
return
|
||
|
||
await send(message)
|
||
|
||
return _observe_response
|
||
|
||
def _build_crypto_context(self, decrypted_payload: DecryptedTransportEnvelope) -> dict[str, str | bytes | bool]:
|
||
"""
|
||
从解密结果构建请求生命周期内的加密上下文
|
||
|
||
:param decrypted_payload: 解密后的请求信封对象
|
||
:return: 请求加密上下文字典
|
||
"""
|
||
return {
|
||
'active': True,
|
||
'kid': decrypted_payload.kid,
|
||
'aes_key': decrypted_payload.aes_key,
|
||
}
|
||
|
||
def _build_error_crypto_context(
|
||
self,
|
||
scope: Scope,
|
||
headers: Headers,
|
||
body: bytes,
|
||
) -> dict[str, str | bytes | bool] | None:
|
||
"""
|
||
尝试在解密失败场景下提取AES会话密钥,以便返回加密错误响应
|
||
|
||
:param scope: 当前ASGI请求作用域
|
||
:param headers: 当前请求头对象
|
||
:param body: 原始请求体字节串
|
||
:return: 可用于构造加密错误响应的上下文字典,失败时返回None
|
||
"""
|
||
content_type = headers.get('content-type', '')
|
||
try:
|
||
query_envelope = self._extract_query_envelope(scope)
|
||
body_envelope = self._extract_body_envelope(content_type, body)
|
||
envelope = body_envelope or query_envelope
|
||
if envelope is None:
|
||
return None
|
||
TransportCryptoUtil._extract_and_validate_aad(
|
||
envelope,
|
||
expected_method=str(scope.get('method', '')).upper(),
|
||
expected_path=self._normalize_path(str(scope.get('path', ''))),
|
||
)
|
||
return {
|
||
'active': True,
|
||
'kid': str(envelope['kid']),
|
||
'aes_key': TransportCryptoUtil.decrypt_request_key(envelope),
|
||
}
|
||
except Exception:
|
||
return None
|
||
|
||
def _extract_request_kid(self, scope: Scope, headers: Headers, body: bytes) -> str | None:
|
||
"""
|
||
尝试从原始请求信封中提取密钥版本
|
||
|
||
:param scope: 当前ASGI请求作用域
|
||
:param headers: 当前请求头对象
|
||
:param body: 原始请求体字节串
|
||
:return: 密钥版本,不存在时返回None
|
||
"""
|
||
content_type = headers.get('content-type', '')
|
||
try:
|
||
query_envelope = self._extract_query_envelope(scope)
|
||
body_envelope = self._extract_body_envelope(content_type, body)
|
||
envelope = body_envelope or query_envelope
|
||
except Exception:
|
||
return None
|
||
if envelope is None or not envelope.get('kid'):
|
||
return None
|
||
return str(envelope['kid'])
|
||
|
||
@staticmethod
|
||
def _loads_json_mapping(payload: str) -> dict:
|
||
"""
|
||
将JSON字符串解析为字典,并限制结果必须为JSON对象
|
||
|
||
:param payload: JSON字符串
|
||
:return: 解析后的字典对象
|
||
"""
|
||
json_payload = json.loads(payload)
|
||
if not isinstance(json_payload, dict):
|
||
raise ValueError('解密后的请求载荷必须为JSON对象')
|
||
return json_payload
|
||
|
||
@staticmethod
|
||
async def _read_body(receive: Receive) -> bytes:
|
||
"""
|
||
从ASGI receive中读取完整请求体
|
||
|
||
:param receive: ASGI receive函数
|
||
:return: 完整请求体字节串
|
||
"""
|
||
body_chunks: list[bytes] = []
|
||
more_body = True
|
||
while more_body:
|
||
message = await receive()
|
||
if message['type'] != 'http.request':
|
||
continue
|
||
body_chunks.append(message.get('body', b''))
|
||
more_body = message.get('more_body', False)
|
||
return b''.join(body_chunks)
|
||
|
||
@staticmethod
|
||
def _build_receive(body: bytes) -> Receive:
|
||
"""
|
||
根据指定请求体重建一次性可消费的ASGI receive函数
|
||
|
||
:param body: 需要回放的请求体字节串
|
||
:return: 重建后的ASGI receive函数
|
||
"""
|
||
has_been_called = False
|
||
|
||
async def _receive() -> Message:
|
||
nonlocal has_been_called
|
||
if has_been_called:
|
||
return {'type': 'http.request', 'body': b'', 'more_body': False}
|
||
has_been_called = True
|
||
return {'type': 'http.request', 'body': body, 'more_body': False}
|
||
|
||
return _receive
|
||
|
||
@staticmethod
|
||
def _replace_header(headers: list[tuple[bytes, bytes]], key: bytes, value: bytes) -> list[tuple[bytes, bytes]]:
|
||
"""
|
||
替换或新增指定响应头
|
||
|
||
:param headers: 原始请求/响应头列表
|
||
:param key: 头名称
|
||
:param value: 头值
|
||
:return: 替换后的头列表
|
||
"""
|
||
normalized_key = key.lower()
|
||
filtered_headers = [
|
||
(header_key, header_value) for header_key, header_value in headers if header_key.lower() != normalized_key
|
||
]
|
||
filtered_headers.append((key, value))
|
||
return filtered_headers
|
||
|
||
@staticmethod
|
||
def _remove_header(headers: list[tuple[bytes, bytes]], key: bytes) -> list[tuple[bytes, bytes]]:
|
||
"""
|
||
删除指定请求头
|
||
|
||
:param headers: 原始请求头列表
|
||
:param key: 头名称
|
||
:return: 删除后的头列表
|
||
"""
|
||
normalized_key = key.lower()
|
||
return [
|
||
(header_key, header_value) for header_key, header_value in headers if header_key.lower() != normalized_key
|
||
]
|
||
|
||
@staticmethod
|
||
def _normalize_path(path: str) -> str:
|
||
"""
|
||
标准化请求路径,剥离应用根路径前缀
|
||
|
||
:param path: 原始请求路径
|
||
:return: 标准化后的业务路径
|
||
"""
|
||
app_root_path = AppConfig.app_root_path
|
||
if app_root_path and path.startswith(app_root_path):
|
||
normalized_path = path[len(app_root_path) :]
|
||
return normalized_path or '/'
|
||
return path or '/'
|
||
|
||
@classmethod
|
||
def _merge_response_headers(
|
||
cls,
|
||
headers: list[tuple[bytes, bytes]],
|
||
extra_headers: dict[str, str],
|
||
) -> list[tuple[bytes, bytes]]:
|
||
"""
|
||
将字符串响应头批量写回到原始headers列表
|
||
|
||
:param headers: 原始请求/响应头列表
|
||
:param extra_headers: 需要追加的响应头
|
||
:return: 合并后的响应头列表
|
||
"""
|
||
merged_headers = headers
|
||
for header_key, header_value in extra_headers.items():
|
||
merged_headers = cls._replace_header(
|
||
merged_headers, header_key.encode('utf-8'), header_value.encode('utf-8')
|
||
)
|
||
return merged_headers
|
||
|
||
@classmethod
|
||
def _build_monitor_headers(
|
||
cls,
|
||
request_mode: str,
|
||
response_mode: str,
|
||
crypto_status: str,
|
||
kid: str | None = None,
|
||
) -> dict[str, str]:
|
||
"""
|
||
构建传输层加解密监控响应头
|
||
|
||
:param request_mode: 请求传输模式
|
||
:param response_mode: 响应传输模式
|
||
:param crypto_status: 当前传输层处理状态
|
||
:param kid: 可选的密钥版本
|
||
:return: 监控响应头字典
|
||
"""
|
||
headers = {
|
||
cls._MONITOR_REQUEST_MODE_HEADER: request_mode,
|
||
cls._MONITOR_RESPONSE_MODE_HEADER: response_mode,
|
||
cls._MONITOR_STATUS_HEADER: crypto_status,
|
||
}
|
||
if kid:
|
||
headers[cls._MONITOR_KID_HEADER] = kid
|
||
return headers
|
||
|
||
@classmethod
|
||
async def _send_error_response(
|
||
cls,
|
||
scope: Scope,
|
||
receive: Receive,
|
||
send: Send,
|
||
message: str,
|
||
crypto_context: dict[str, str | bytes | bool] | None = None,
|
||
headers: dict[str, str] | None = None,
|
||
) -> None:
|
||
"""
|
||
发送错误响应,在存在AES会话密钥时优先返回加密错误响应
|
||
|
||
:param scope: 当前ASGI请求作用域
|
||
:param receive: ASGI receive函数
|
||
:param send: ASGI send函数
|
||
:param message: 错误信息
|
||
:param crypto_context: 可选的请求加密上下文
|
||
:param headers: 需要追加的诊断响应头
|
||
:return: None
|
||
"""
|
||
response_content = {'code': HttpStatusConstant.BAD_REQUEST, 'msg': message, 'success': False}
|
||
response = JSONResponse(status_code=HttpStatusConstant.BAD_REQUEST, content=response_content)
|
||
if crypto_context:
|
||
encrypted_body = TransportCryptoUtil.encrypt_response_body(
|
||
aes_key=crypto_context['aes_key'],
|
||
payload=json.dumps(response_content, ensure_ascii=False).encode('utf-8'),
|
||
kid=str(crypto_context['kid']),
|
||
method=str(scope.get('method', '')),
|
||
path=cls._normalize_path(str(scope.get('path', ''))),
|
||
)
|
||
response.body = encrypted_body
|
||
response.init_headers()
|
||
response.headers[cls._ENCRYPT_RESPONSE_HEADER] = '1'
|
||
response.headers[cls._ENCRYPT_ALG_HEADER] = TransportCryptoUtil.get_response_envelope_algorithm()
|
||
response.headers[cls._ENCRYPT_KID_HEADER] = str(crypto_context['kid'])
|
||
if headers:
|
||
response.headers.update(headers)
|
||
await response(scope, receive, send)
|
||
|
||
@staticmethod
|
||
def _classify_failure_reason(message: str) -> str:
|
||
"""
|
||
根据异常信息归类传输层失败原因
|
||
|
||
:param message: 原始异常信息
|
||
:return: 失败原因分类编码
|
||
"""
|
||
if not message or message == '加密请求解析失败':
|
||
return 'decrypt_failed'
|
||
failure_reason_mapping = (
|
||
('method/path与当前接口不匹配', 'aad_mismatch'),
|
||
('缺少合法的aad', 'aad_invalid'),
|
||
('已过期', 'timestamp_expired'),
|
||
('缺少必要字段', 'envelope_fields_missing'),
|
||
('协议版本不受支持', 'protocol_version_invalid'),
|
||
('算法不受支持', 'algorithm_invalid'),
|
||
('未找到可解密的请求载荷', 'envelope_missing'),
|
||
('密钥版本', 'kid_mismatch'),
|
||
)
|
||
for reason_keyword, reason_code in failure_reason_mapping:
|
||
if reason_keyword in message:
|
||
return reason_code
|
||
if '重复请求' in message or '重放' in message:
|
||
return 'replay_detected'
|
||
return 'decrypt_failed'
|
||
|
||
@classmethod
|
||
def _is_excluded_path(cls, path: str) -> bool:
|
||
"""
|
||
判断当前路径是否在传输加密排除列表内
|
||
|
||
:param path: 当前请求路径
|
||
:return: 是否命中排除列表
|
||
"""
|
||
excluded_paths = [
|
||
excluded_path.strip()
|
||
for excluded_path in TransportCryptoConfig.transport_crypto_exclude_paths.split(',')
|
||
if excluded_path.strip()
|
||
]
|
||
return any(path == excluded_path or path.startswith(f'{excluded_path}/') for excluded_path in excluded_paths)
|
||
|
||
@classmethod
|
||
def _is_required_path(cls, path: str) -> bool:
|
||
"""
|
||
判断当前路径是否在强制加密列表内
|
||
|
||
:param path: 当前请求路径
|
||
:return: 是否命中强制加密列表
|
||
"""
|
||
required_paths = [
|
||
required_path.strip()
|
||
for required_path in TransportCryptoConfig.transport_crypto_required_paths.split(',')
|
||
if required_path.strip()
|
||
]
|
||
if not required_paths:
|
||
return False
|
||
return any(path == required_path or path.startswith(f'{required_path}/') for required_path in required_paths)
|
||
|
||
@classmethod
|
||
def _is_enabled_path(cls, path: str) -> bool:
|
||
"""
|
||
判断当前路径是否在启用传输加密的列表内
|
||
|
||
:param path: 当前请求路径
|
||
:return: 当前路径是否启用传输加密
|
||
"""
|
||
enabled_paths = [
|
||
enabled_path.strip()
|
||
for enabled_path in TransportCryptoConfig.transport_crypto_enabled_paths.split(',')
|
||
if enabled_path.strip()
|
||
]
|
||
if not enabled_paths:
|
||
return True
|
||
return any(path == enabled_path or path.startswith(f'{enabled_path}/') for enabled_path in enabled_paths)
|
||
|
||
|
||
def add_transport_crypto_middleware(app: ASGIApp) -> None:
|
||
"""
|
||
添加传输层加解密中间件
|
||
|
||
:param app: FastAPI/Starlette应用对象
|
||
:return: None
|
||
"""
|
||
app.add_middleware(TransportCryptoMiddleware)
|