diff --git a/app/adapters/network/http.py b/app/adapters/network/http.py index aeaa04f29..deec5b1bd 100644 --- a/app/adapters/network/http.py +++ b/app/adapters/network/http.py @@ -8,7 +8,6 @@ import weakref from contextlib import AsyncExitStack, asynccontextmanager, contextmanager from pathlib import Path from typing import Any, Dict, Literal, Optional, Tuple, Union, overload -from urllib.parse import quote, unquote import chardet import httpx @@ -266,29 +265,12 @@ async def aclose_shared_async_transports() -> None: _pending_eviction_tasks.difference_update(pending_evictions) -def _url_decode_if_latin(original: str) -> str: - """ - 解码URL编码的字符串,只解码文本,二进制数据保持不变 - :param original: URL编码字符串 - :return: 解码后的字符串或原始二进制数据 - """ - try: - # 先解码 - decoded = unquote(original, encoding="latin-1") - # 再完整编码 - fully_encoded = quote(decoded, safe="") - # 验证 - decoded_again = unquote(fully_encoded, encoding="latin-1") - if decoded_again == decoded: - return decoded - except Exception: - pass - return original - - def cookie_parse(cookies_str: str, array: bool = False) -> Union[list, dict]: """ - 解析cookie,转化为字典或者数组 + 将 Cookie 请求头字符串解析为字典或浏览器 Cookie 数组。 + + Cookie 值属于不透明数据,百分号编码可能是签名的一部分,必须保持浏览器原值。 + :param cookies_str: cookie字符串 :param array: 是否转化为数组 :return: 字典或者数组 @@ -301,8 +283,7 @@ def cookie_parse(cookies_str: str, array: bool = False) -> Union[list, dict]: for cookie in cookies: cstr = cookie.split("=", 1) # 只分割第一个=,因为value可能包含= if len(cstr) > 1: - # URL解码Cookie值(但保留Cookie名不解码) - cookie_dict[cstr[0].strip()] = _url_decode_if_latin(cstr[1].strip()) + cookie_dict[cstr[0].strip()] = cstr[1].strip() if array: return [{"name": k, "value": v} for k, v in cookie_dict.items()] return cookie_dict diff --git a/tests/test_request_utils.py b/tests/test_request_utils.py index 35f211225..8a485f16e 100644 --- a/tests/test_request_utils.py +++ b/tests/test_request_utils.py @@ -1,7 +1,7 @@ import requests from app.adapters.network import http as http_module -from app.adapters.network.http import AsyncRequestUtils, RequestUtils +from app.adapters.network.http import AsyncRequestUtils, RequestUtils, cookie_parse class _FakeSession: @@ -195,3 +195,34 @@ def test_request_utils_normalizes_per_request_cookie_string(monkeypatch) -> None assert result is response assert prepared_requests[0].headers["Cookie"] == "sid=1; token=two" + + +def test_cookie_parse_preserves_percent_encoded_signature_values(monkeypatch) -> None: + """Cookie 签名中的百分号编码必须按浏览器原值发送,不能被 URL 解码破坏。""" + raw_cookie = "session=abc%2Fdef%2Bghi%3D%3D; userid=123" + parsed_cookie = cookie_parse(raw_cookie) + browser_cookies = cookie_parse(raw_cookie, array=True) + response = _make_response() + session = requests.Session() + prepared_requests = [] + + def fake_send(request, **_kwargs): + prepared_requests.append(request) + return response + + monkeypatch.setattr(session, "send", fake_send) + + result = RequestUtils(session=session, cookies=raw_cookie).get_res( + "https://example.com/data" + ) + + assert result is response + assert parsed_cookie == { + "session": "abc%2Fdef%2Bghi%3D%3D", + "userid": "123", + } + assert browser_cookies == [ + {"name": "session", "value": "abc%2Fdef%2Bghi%3D%3D"}, + {"name": "userid", "value": "123"}, + ] + assert prepared_requests[0].headers["Cookie"] == raw_cookie