From 55f2060664b3ba049c8a1981ada36b6b3ba8fe60 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 10:37:17 +0800 Subject: [PATCH 01/19] feat(client): add x-b3-traceid and x-xray-traceid generation methods --- src/xhshow/client.py | 36 ++++++++++++++++++++++++++++++ src/xhshow/config/config.py | 9 ++++++++ src/xhshow/utils/random_gen.py | 40 +++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index e813e72..8529c9c 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -4,6 +4,7 @@ from .config import CryptoConfig from .core.crypto import CryptoProcessor +from .utils.random_gen import RandomGenerator from .utils.url_utils import build_url, extract_uri from .utils.validators import ( validate_get_signature_params, @@ -20,6 +21,7 @@ class Xhshow: def __init__(self, config: CryptoConfig | None = None): self.config = config or CryptoConfig() self.crypto_processor = CryptoProcessor(self.config) + self.random_generator = RandomGenerator() def _build_content_string(self, method: str, uri: str, payload: dict[str, Any] | None = None) -> str: """ @@ -264,3 +266,37 @@ def build_json_body(self, payload: dict[str, Any]) -> str: '{"username":"test","password":"123456"}' """ return json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + + def get_b3_trace_id(self) -> str: + """ + Generate x-b3-traceid for HTTP request headers + + Returns: + str: 16-character hexadecimal trace ID + + Examples: + >>> client = Xhshow() + >>> client.get_b3_trace_id() + '63cd207ddeb2e360' + """ + return self.random_generator.generate_b3_trace_id() + + def get_xray_trace_id(self, timestamp: int | None = None, seq: int | None = None) -> str: + """ + Generate x-xray-traceid for HTTP request headers + + Args: + timestamp: Unix timestamp in milliseconds (defaults to current time) + seq: Sequence number 0 to 2^23-1 (defaults to random value) + + Returns: + str: 32-character hexadecimal trace ID + + Examples: + >>> client = Xhshow() + >>> client.get_xray_trace_id() + 'cd7621e82d9c24e90bfd937d92bbbd1b' + >>> client.get_xray_trace_id(timestamp=1764896636081, seq=5) + 'cd7604be588000051a7fb8ae74496a76' + """ + return self.random_generator.generate_xray_trace_id(timestamp, seq) diff --git a/src/xhshow/config/config.py b/src/xhshow/config/config.py index 7af6954..6fcf380 100644 --- a/src/xhshow/config/config.py +++ b/src/xhshow/config/config.py @@ -11,6 +11,7 @@ class CryptoConfig: # Bitwise operation constants MAX_32BIT: int = 0xFFFFFFFF MAX_SIGNED_32BIT: int = 0x7FFFFFFF + MAX_BYTE: int = 255 # Base64 encoding constants STANDARD_BASE64_ALPHABET: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" @@ -61,6 +62,14 @@ class CryptoConfig: X3_PREFIX: str = "mns0301_" XYS_PREFIX: str = "XYS_" + # Trace ID generation constants + HEX_CHARS: str = "abcdef0123456789" + XRAY_TRACE_ID_SEQ_MAX: int = 8388607 # 2^23-1 + XRAY_TRACE_ID_TIMESTAMP_SHIFT: int = 23 + XRAY_TRACE_ID_PART1_LENGTH: int = 16 + XRAY_TRACE_ID_PART2_LENGTH: int = 16 + B3_TRACE_ID_LENGTH: int = 16 + def with_overrides(self, **kwargs: Any) -> "CryptoConfig": """ Create a new config instance with overridden values diff --git a/src/xhshow/utils/random_gen.py b/src/xhshow/utils/random_gen.py index a6965c6..6a7705b 100644 --- a/src/xhshow/utils/random_gen.py +++ b/src/xhshow/utils/random_gen.py @@ -1,4 +1,5 @@ import random +import time from ..config import CryptoConfig @@ -21,7 +22,7 @@ def generate_random_bytes(self, byte_count: int) -> list[int]: Returns: list[int]: Random byte array """ - return [random.randint(0, 255) for _ in range(byte_count)] + return [random.randint(0, self.config.MAX_BYTE) for _ in range(byte_count)] def generate_random_byte_in_range(self, min_val: int, max_val: int) -> int: """ @@ -44,3 +45,40 @@ def generate_random_int(self) -> int: int: Random 32-bit integer """ return random.randint(0, self.config.MAX_32BIT) + + def generate_b3_trace_id(self) -> str: + """ + Generate x-b3-traceid (16 random hex characters) + + Returns: + str: 16-character hexadecimal trace ID + """ + return "".join(random.choice(self.config.HEX_CHARS) for _ in range(self.config.B3_TRACE_ID_LENGTH)) + + def generate_xray_trace_id(self, timestamp: int | None = None, seq: int | None = None) -> str: + """ + Generate x-xray-traceid (32 characters: 16 timestamp+seq + 16 random) + + Args: + timestamp: Unix timestamp in milliseconds (defaults to current time) + seq: Sequence number 0 to 2^23-1 (defaults to random value) + + Returns: + str: 32-character hexadecimal trace ID + """ + if timestamp is None: + timestamp = int(time.time() * 1000) + if seq is None: + seq = random.randint(0, self.config.XRAY_TRACE_ID_SEQ_MAX) + + # First 16 chars: XHS xray parameter uses timestamp bit operations + part1 = format( + ((timestamp << self.config.XRAY_TRACE_ID_TIMESTAMP_SHIFT) | seq), + f"0{self.config.XRAY_TRACE_ID_PART1_LENGTH}x", + ) + # Last 16 chars: completely random, untraceable, can be simplified + part2 = "".join( + random.choice(self.config.HEX_CHARS) for _ in range(self.config.XRAY_TRACE_ID_PART2_LENGTH) + ) + + return part1 + part2 From f19b51343067ae0799c599bd9f872a9cf04727e6 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 10:52:42 +0800 Subject: [PATCH 02/19] feat(client): add trace id generation and unified timestamp support --- src/xhshow/client.py | 40 +++++++++-- src/xhshow/core/crypto.py | 5 +- src/xhshow/utils/validators.py | 6 ++ tests/test_crypto.py | 122 +++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 5 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 8529c9c..253edec 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -1,5 +1,6 @@ import hashlib import json +import time from typing import Any, Literal from .config import CryptoConfig @@ -69,6 +70,7 @@ def _build_signature( a1_value: str, xsec_appid: str = "xhs-pc-web", string_param: str = "", + timestamp: float | None = None, ) -> str: """ Build signature @@ -78,11 +80,14 @@ def _build_signature( a1_value: a1 value from cookies xsec_appid: Application identifier string_param: String parameter + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Base64 encoded signature """ - payload_array = self.crypto_processor.build_payload_array(d_value, a1_value, xsec_appid, string_param) + payload_array = self.crypto_processor.build_payload_array( + d_value, a1_value, xsec_appid, string_param, timestamp + ) xor_result = self.crypto_processor.bit_ops.xor_transform_array(payload_array) @@ -96,6 +101,7 @@ def sign_xs( a1_value: str, xsec_appid: str = "xhs-pc-web", payload: dict[str, Any] | None = None, + timestamp: float | None = None, ) -> str: """ Generate request signature (supports GET and POST) @@ -111,6 +117,7 @@ def sign_xs( payload: Request parameters - GET request: params value - POST request: payload value + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Complete signature string @@ -127,7 +134,7 @@ def sign_xs( d_value = self._generate_d_value(content_string) signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + self._build_signature( - d_value, a1_value, xsec_appid, content_string + d_value, a1_value, xsec_appid, content_string, timestamp ) return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode( json.dumps(signature_data, separators=(",", ":"), ensure_ascii=False) @@ -140,6 +147,7 @@ def sign_xs_get( a1_value: str, xsec_appid: str = "xhs-pc-web", params: dict[str, Any] | None = None, + timestamp: float | None = None, ) -> str: """ Generate GET request signature (convenience method) @@ -151,6 +159,7 @@ def sign_xs_get( a1_value: a1 value from cookies xsec_appid: Application identifier, defaults to `xhs-pc-web` params: GET request parameters + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Complete signature string @@ -159,7 +168,7 @@ def sign_xs_get( TypeError: Parameter type error ValueError: Parameter value error """ - return self.sign_xs("GET", uri, a1_value, xsec_appid, params) + return self.sign_xs("GET", uri, a1_value, xsec_appid, payload=params, timestamp=timestamp) @validate_post_signature_params def sign_xs_post( @@ -168,6 +177,7 @@ def sign_xs_post( a1_value: str, xsec_appid: str = "xhs-pc-web", payload: dict[str, Any] | None = None, + timestamp: float | None = None, ) -> str: """ Generate POST request signature (convenience method) @@ -179,6 +189,7 @@ def sign_xs_post( a1_value: a1 value from cookies xsec_appid: Application identifier, defaults to `xhs-pc-web` payload: POST request body data + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Complete signature string @@ -187,7 +198,7 @@ def sign_xs_post( TypeError: Parameter type error ValueError: Parameter value error """ - return self.sign_xs("POST", uri, a1_value, xsec_appid, payload) + return self.sign_xs("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) def decode_x3(self, x3_signature: str) -> bytearray: """ @@ -300,3 +311,24 @@ def get_xray_trace_id(self, timestamp: int | None = None, seq: int | None = None 'cd7604be588000051a7fb8ae74496a76' """ return self.random_generator.generate_xray_trace_id(timestamp, seq) + + def get_x_t(self, timestamp: float | None = None) -> int: + """ + Generate x-t header value (Unix timestamp in milliseconds) + + Args: + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + int: Unix timestamp in milliseconds + + Examples: + >>> client = Xhshow() + >>> client.get_x_t() + 1764902784843 + >>> client.get_x_t(timestamp=1764896636.081) + 1764896636081 + """ + if timestamp is None: + timestamp = time.time() + return int(timestamp * 1000) diff --git a/src/xhshow/core/crypto.py b/src/xhshow/core/crypto.py index 7693cd2..8d3be5e 100644 --- a/src/xhshow/core/crypto.py +++ b/src/xhshow/core/crypto.py @@ -56,6 +56,7 @@ def build_payload_array( a1_value: str, app_identifier: str = "xhs-pc-web", string_param: str = "", + timestamp: float | None = None, ) -> list[int]: """ Build payload array (t.js version - exact match) @@ -65,6 +66,7 @@ def build_payload_array( a1_value (str): a1 value from cookies app_identifier (str): Application identifier, default "xhs-pc-web" string_param (str): String parameter (used for URI length calculation) + timestamp (float | None): Unix timestamp in seconds (defaults to current time) Returns: list[int]: Complete payload byte array (124 bytes) @@ -78,7 +80,8 @@ def build_payload_array( payload.extend(seed_bytes) seed_byte_0 = seed_bytes[0] - timestamp = time.time() + if timestamp is None: + timestamp = time.time() payload.extend(self.env_fingerprint_a(int(timestamp * 1000), self.config.ENV_FINGERPRINT_XOR_KEY)) time_offset = self.random_gen.generate_random_byte_in_range( diff --git a/src/xhshow/utils/validators.py b/src/xhshow/utils/validators.py index d585b1a..82ad3d1 100644 --- a/src/xhshow/utils/validators.py +++ b/src/xhshow/utils/validators.py @@ -96,6 +96,7 @@ def wrapper( a1_value: Any, xsec_appid: Any = "xhs-pc-web", payload: Any = None, + timestamp: float | None = None, ): validator = RequestSignatureValidator() @@ -112,6 +113,7 @@ def wrapper( validated_a1_value, validated_xsec_appid, validated_payload, + timestamp, ) return wrapper # type: ignore @@ -135,6 +137,7 @@ def wrapper( a1_value: Any, xsec_appid: Any = "xhs-pc-web", params: Any = None, + timestamp: float | None = None, ): validator = RequestSignatureValidator() @@ -149,6 +152,7 @@ def wrapper( validated_a1_value, validated_xsec_appid, validated_params, + timestamp, ) return wrapper # type: ignore @@ -172,6 +176,7 @@ def wrapper( a1_value: Any, xsec_appid: Any = "xhs-pc-web", payload: Any = None, + timestamp: float | None = None, ): validator = RequestSignatureValidator() @@ -186,6 +191,7 @@ def wrapper( validated_a1_value, validated_xsec_appid, validated_payload, + timestamp, ) return wrapper # type: ignore diff --git a/tests/test_crypto.py b/tests/test_crypto.py index dda704d..021964f 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -387,3 +387,125 @@ def test_sign_xs_parameter_validation(self): result = client.sign_xs(" get ", " /api/test ", " test_a1 ") # type: ignore assert isinstance(result, str) assert result.startswith("XYS_") + + def test_timestamp_parameter_support(self): + """测试时间戳参数支持""" + import time + + client = Xhshow() + uri = "/api/sns/web/v1/user_posted" + a1_value = "test_a1_value" + params = {"num": "30"} + + # Test with default timestamp + sig1 = client.sign_xs_get(uri, a1_value, params=params) + assert isinstance(sig1, str) + assert sig1.startswith("XYS_") + + # Test with custom timestamp + custom_ts = time.time() + sig2 = client.sign_xs_get(uri, a1_value, params=params, timestamp=custom_ts) + assert isinstance(sig2, str) + assert sig2.startswith("XYS_") + + # Test POST with timestamp + sig3 = client.sign_xs_post( + uri="/api/sns/web/v1/login", + a1_value=a1_value, + payload={"user": "test"}, + timestamp=custom_ts, + ) + assert isinstance(sig3, str) + assert sig3.startswith("XYS_") + + # Test sign_xs with timestamp + sig4 = client.sign_xs(method="GET", uri=uri, a1_value=a1_value, payload=params, timestamp=custom_ts) + assert isinstance(sig4, str) + assert sig4.startswith("XYS_") + + def test_trace_id_generation(self): + """测试 Trace ID 生成""" + import time + + client = Xhshow() + + # Test b3 trace id + b3_id = client.get_b3_trace_id() + assert isinstance(b3_id, str) + assert len(b3_id) == 16 + assert all(c in "0123456789abcdef" for c in b3_id) + + # Test xray trace id with default timestamp + xray_id1 = client.get_xray_trace_id() + assert isinstance(xray_id1, str) + assert len(xray_id1) == 32 + assert all(c in "0123456789abcdef" for c in xray_id1) + + # Test xray trace id with custom timestamp + custom_ts = int(time.time() * 1000) + xray_id2 = client.get_xray_trace_id(timestamp=custom_ts) + assert isinstance(xray_id2, str) + assert len(xray_id2) == 32 + + # Test xray trace id with custom timestamp and seq + xray_id3 = client.get_xray_trace_id(timestamp=custom_ts, seq=12345) + assert isinstance(xray_id3, str) + assert len(xray_id3) == 32 + + def test_unified_timestamp_usage(self): + """测试统一时间戳使用场景""" + import time + + client = Xhshow() + unified_ts = time.time() + + # Use same timestamp for signature and trace IDs + signature = client.sign_xs_post( + uri="/api/sns/web/v1/login", + a1_value="test_a1", + payload={"user": "test"}, + timestamp=unified_ts, + ) + + b3_id = client.get_b3_trace_id() + xray_id = client.get_xray_trace_id(timestamp=int(unified_ts * 1000)) + + assert isinstance(signature, str) + assert signature.startswith("XYS_") + assert isinstance(b3_id, str) + assert len(b3_id) == 16 + assert isinstance(xray_id, str) + assert len(xray_id) == 32 + + def test_get_x_t(self): + """测试 x-t header 生成""" + import time + + client = Xhshow() + + # Test with default timestamp + x_t1 = client.get_x_t() + assert isinstance(x_t1, int) + assert x_t1 > 0 + + # Test with custom timestamp + custom_ts = time.time() + x_t2 = client.get_x_t(timestamp=custom_ts) + assert isinstance(x_t2, int) + assert x_t2 == int(custom_ts * 1000) + + # Test unified timestamp with all headers + unified_ts = time.time() + x_t = client.get_x_t(timestamp=unified_ts) + signature = client.sign_xs_get( + uri="/api/test", a1_value="test_a1", params={"key": "value"}, timestamp=unified_ts + ) + b3_id = client.get_b3_trace_id() + xray_id = client.get_xray_trace_id(timestamp=int(unified_ts * 1000)) + + assert isinstance(x_t, int) + assert x_t == int(unified_ts * 1000) + assert isinstance(signature, str) + assert isinstance(b3_id, str) + assert isinstance(xray_id, str) + From cfc809a29b696dc02a40236e418ffdd7d19a4574 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 10:57:16 +0800 Subject: [PATCH 03/19] feat(client): add unified sign_headers method for complete request headers --- src/xhshow/client.py | 49 ++++++++++++++++++++++++++++++++++++++++ tests/test_crypto.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 253edec..bbe25b8 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -332,3 +332,52 @@ def get_x_t(self, timestamp: float | None = None) -> int: if timestamp is None: timestamp = time.time() return int(timestamp * 1000) + + def sign_headers( + self, + method: Literal["GET", "POST"], + uri: str, + a1_value: str, + xsec_appid: str = "xhs-pc-web", + payload: dict[str, Any] | None = None, + timestamp: float | None = None, + ) -> dict[str, str]: + """ + Generate complete request headers with signature and trace IDs + + Args: + method: Request method ("GET" or "POST") + uri: Request URI or full URL + a1_value: a1 value from cookies + xsec_appid: Application identifier, defaults to `xhs-pc-web` + payload: Request parameters (GET: params, POST: payload) + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + + Examples: + >>> client = Xhshow() + >>> headers = client.sign_headers( + ... method="GET", + ... uri="/api/sns/web/v1/user_posted", + ... a1_value="your_a1_value", + ... payload={"num": "30"} + ... ) + >>> headers.keys() + dict_keys(['x-s', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) + """ + if timestamp is None: + timestamp = time.time() + + x_s = self.sign_xs(method, uri, a1_value, xsec_appid, payload, timestamp) + x_t = self.get_x_t(timestamp) + x_b3_traceid = self.get_b3_trace_id() + x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) + + return { + "x-s": x_s, + "x-t": str(x_t), + "x-b3-traceid": x_b3_traceid, + "x-xray-traceid": x_xray_traceid, + } diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 021964f..14a5cf1 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -509,3 +509,56 @@ def test_get_x_t(self): assert isinstance(b3_id, str) assert isinstance(xray_id, str) + def test_sign_headers(self): + """测试统一 headers 生成""" + import time + + client = Xhshow() + + # Test GET request headers + headers = client.sign_headers( + method="GET", + uri="/api/sns/web/v1/user_posted", + a1_value="test_a1_value", + payload={"num": "30", "cursor": "", "user_id": "123"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-t" in headers + assert "x-b3-traceid" in headers + assert "x-xray-traceid" in headers + + assert headers["x-s"].startswith("XYS_") + assert headers["x-t"].isdigit() + assert len(headers["x-b3-traceid"]) == 16 + assert len(headers["x-xray-traceid"]) == 32 + + # Test POST request headers + headers_post = client.sign_headers( + method="POST", + uri="/api/sns/web/v1/login", + a1_value="test_a1_value", + payload={"username": "test", "password": "123456"}, + ) + + assert isinstance(headers_post, dict) + assert all(k in headers_post for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + # Test with custom timestamp + custom_ts = time.time() + headers_custom = client.sign_headers( + method="GET", + uri="/api/test", + a1_value="test_a1", + payload={"key": "value"}, + timestamp=custom_ts, + ) + + assert isinstance(headers_custom, dict) + assert headers_custom["x-t"] == str(int(custom_ts * 1000)) + + # Test all values are strings + assert all(isinstance(v, str) for v in headers.values()) + assert all(isinstance(v, str) for v in headers_post.values()) + assert all(isinstance(v, str) for v in headers_custom.values()) From d50d50c17a27f81349cadf05367a56cbef43267d Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 11:35:37 +0800 Subject: [PATCH 04/19] feat(client): improve sign_headers parameter handling and add convenience methods - fix: distinguish params/payload parameters in sign_headers method - params: for GET requests only - payload: for POST requests only - feat: add sign_headers_get convenience method for GET requests - feat: add sign_headers_post convenience method for POST requests - docs: update docstrings with clear parameter usage examples --- src/xhshow/client.py | 81 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index bbe25b8..ac63cac 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -339,6 +339,7 @@ def sign_headers( uri: str, a1_value: str, xsec_appid: str = "xhs-pc-web", + params: dict[str, Any] | None = None, payload: dict[str, Any] | None = None, timestamp: float | None = None, ) -> dict[str, str]: @@ -350,7 +351,8 @@ def sign_headers( uri: Request URI or full URL a1_value: a1 value from cookies xsec_appid: Application identifier, defaults to `xhs-pc-web` - payload: Request parameters (GET: params, POST: payload) + params: GET request parameters (only used when method="GET") + payload: POST request body data (only used when method="POST") timestamp: Unix timestamp in seconds (defaults to current time) Returns: @@ -358,11 +360,19 @@ def sign_headers( Examples: >>> client = Xhshow() + >>> # GET request >>> headers = client.sign_headers( ... method="GET", ... uri="/api/sns/web/v1/user_posted", ... a1_value="your_a1_value", - ... payload={"num": "30"} + ... params={"num": "30"} + ... ) + >>> # POST request + >>> headers = client.sign_headers( + ... method="POST", + ... uri="/api/sns/web/v1/login", + ... a1_value="your_a1_value", + ... payload={"username": "test"} ... ) >>> headers.keys() dict_keys(['x-s', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) @@ -370,7 +380,10 @@ def sign_headers( if timestamp is None: timestamp = time.time() - x_s = self.sign_xs(method, uri, a1_value, xsec_appid, payload, timestamp) + # Use params for GET, payload for POST + request_data = params if method.upper() == "GET" else payload + + x_s = self.sign_xs(method, uri, a1_value, xsec_appid, request_data, timestamp) x_t = self.get_x_t(timestamp) x_b3_traceid = self.get_b3_trace_id() x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) @@ -381,3 +394,65 @@ def sign_headers( "x-b3-traceid": x_b3_traceid, "x-xray-traceid": x_xray_traceid, } + + def sign_headers_get( + self, + uri: str, + a1_value: str, + xsec_appid: str = "xhs-pc-web", + params: dict[str, Any] | None = None, + timestamp: float | None = None, + ) -> dict[str, str]: + """ + Generate complete request headers for GET request (convenience method) + + Args: + uri: Request URI or full URL + a1_value: a1 value from cookies + xsec_appid: Application identifier, defaults to `xhs-pc-web` + params: GET request parameters + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + + Examples: + >>> client = Xhshow() + >>> headers = client.sign_headers_get( + ... uri="/api/sns/web/v1/user_posted", + ... a1_value="your_a1_value", + ... params={"num": "30"} + ... ) + """ + return self.sign_headers("GET", uri, a1_value, xsec_appid, params=params, timestamp=timestamp) + + def sign_headers_post( + self, + uri: str, + a1_value: str, + xsec_appid: str = "xhs-pc-web", + payload: dict[str, Any] | None = None, + timestamp: float | None = None, + ) -> dict[str, str]: + """ + Generate complete request headers for POST request (convenience method) + + Args: + uri: Request URI or full URL + a1_value: a1 value from cookies + xsec_appid: Application identifier, defaults to `xhs-pc-web` + payload: POST request body data + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + + Examples: + >>> client = Xhshow() + >>> headers = client.sign_headers_post( + ... uri="/api/sns/web/v1/login", + ... a1_value="your_a1_value", + ... payload={"username": "test", "password": "123456"} + ... ) + """ + return self.sign_headers("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) From a3a77169b47bacd55533359d4f1eab356c3d9aee Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 11:35:56 +0800 Subject: [PATCH 05/19] test(crypto): update tests for improved sign_headers methods - fix: use params for GET and payload for POST in sign_headers tests - test: add test_sign_headers_get for GET convenience method - test: add test_sign_headers_post for POST convenience method - test: verify timestamp parameter support in convenience methods --- tests/test_crypto.py | 60 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 14a5cf1..2053170 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -515,12 +515,12 @@ def test_sign_headers(self): client = Xhshow() - # Test GET request headers + # Test GET request headers with params headers = client.sign_headers( method="GET", uri="/api/sns/web/v1/user_posted", a1_value="test_a1_value", - payload={"num": "30", "cursor": "", "user_id": "123"}, + params={"num": "30", "cursor": "", "user_id": "123"}, ) assert isinstance(headers, dict) @@ -534,7 +534,7 @@ def test_sign_headers(self): assert len(headers["x-b3-traceid"]) == 16 assert len(headers["x-xray-traceid"]) == 32 - # Test POST request headers + # Test POST request headers with payload headers_post = client.sign_headers( method="POST", uri="/api/sns/web/v1/login", @@ -551,7 +551,7 @@ def test_sign_headers(self): method="GET", uri="/api/test", a1_value="test_a1", - payload={"key": "value"}, + params={"key": "value"}, timestamp=custom_ts, ) @@ -562,3 +562,55 @@ def test_sign_headers(self): assert all(isinstance(v, str) for v in headers.values()) assert all(isinstance(v, str) for v in headers_post.values()) assert all(isinstance(v, str) for v in headers_custom.values()) + + def test_sign_headers_get(self): + """测试 GET 请求 headers 便捷方法""" + import time + + client = Xhshow() + + # Test basic usage + headers = client.sign_headers_get( + uri="/api/sns/web/v1/user_posted", + a1_value="test_a1_value", + params={"num": "30", "cursor": "", "user_id": "123"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert headers["x-s"].startswith("XYS_") + assert all(isinstance(v, str) for v in headers.values()) + + # Test with custom timestamp + custom_ts = time.time() + headers_ts = client.sign_headers_get( + uri="/api/test", a1_value="test_a1", params={"key": "value"}, timestamp=custom_ts + ) + + assert headers_ts["x-t"] == str(int(custom_ts * 1000)) + + def test_sign_headers_post(self): + """测试 POST 请求 headers 便捷方法""" + import time + + client = Xhshow() + + # Test basic usage + headers = client.sign_headers_post( + uri="/api/sns/web/v1/login", + a1_value="test_a1_value", + payload={"username": "test", "password": "123456"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert headers["x-s"].startswith("XYS_") + assert all(isinstance(v, str) for v in headers.values()) + + # Test with custom timestamp + custom_ts = time.time() + headers_ts = client.sign_headers_post( + uri="/api/test", a1_value="test_a1", payload={"key": "value"}, timestamp=custom_ts + ) + + assert headers_ts["x-t"] == str(int(custom_ts * 1000)) From b0902eda20692e79814247dac5cfa6e1ae6a51cb Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 11:36:07 +0800 Subject: [PATCH 06/19] docs(readme): reorganize usage examples to highlight convenience methods - feat: promote sign_headers_get/post as recommended approach - feat: move build_url and build_json_body to recommended section - refactor: move sign_headers unified method to traditional methods section - refactor: collapse traditional single-field generation methods into details - improve: clearer documentation structure for better user experience --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a102dcb..4b2b513 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ pip install xhshow ## 使用方法 -### 基本用法 +### 基本用法(推荐) ```python from xhshow import Xhshow @@ -31,21 +31,53 @@ import requests client = Xhshow() -# GET请求签名 -signature = client.sign_xs_get( - uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # v0.1.3及后续版本支持自动提取uri - # uri="/api/sns/web/v1/user_posted" # v0.1.2及以前版本需要主动提取uri +# 注意: uri 参数可以传递完整 URL 或 URI 路径,会自动提取 URI +headers = client.sign_headers_get( + uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # 完整 URL(推荐) + # uri="/api/sns/web/v1/user_posted", # 或者只传 URI 路径 a1_value="your_a1_cookie_value", params={"num": "30", "cursor": "", "user_id": "123"} ) -# POST请求签名 -signature = client.sign_xs_post( +# 方式1: 使用 update 方法更新现有 headers(推荐) +base_headers = { + "User-Agent": "Mozilla/5.0...", + "Content-Type": "application/json" +} +base_headers.update(headers) +response = requests.get( + "https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", + params={"num": "30", "cursor": "", "user_id": "123"}, + headers=base_headers, + cookies={"a1": "your_a1_cookie_value"} +) + +# 方式2: 使用 ** 解包创建新 headers +response = requests.get( + "https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", + params={"num": "30", "cursor": "", "user_id": "123"}, + headers={ + "User-Agent": "Mozilla/5.0...", + "Content-Type": "application/json", + **headers # 解包签名 headers(会创建新字典) + }, + cookies={"a1": "your_a1_cookie_value"} +) + +# POST 请求示例:使用 sign_headers_post +headers_post = client.sign_headers_post( uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", a1_value="your_a1_cookie_value", payload={"username": "test", "password": "123456"} ) +response = requests.post( + "https://edith.xiaohongshu.com/api/sns/web/v1/login", + json={"username": "test", "password": "123456"}, + headers={**base_headers, **headers_post}, + cookies={"a1": "your_a1_cookie_value"} +) + # 构建符合xhs平台的GET请求链接 full_url = client.build_url( base_url="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", @@ -60,6 +92,67 @@ json_body = client.build_json_body( response = requests.post(url, data=json_body, headers=headers, cookies=cookies) ``` +
+传统方法(单独生成各个字段) + +```python +from xhshow import Xhshow +import requests + +client = Xhshow() + +# 也可以使用统一方法 sign_headers(需要手动指定 method) +headers = client.sign_headers( + method="GET", # 或 "POST" + uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", + a1_value="your_a1_cookie_value", + params={"num": "30"} # GET 请求使用 params,POST 请求使用 payload +) + +# GET 请求签名 +x_s = client.sign_xs_get( + uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # v0.1.3及后续版本支持自动提取uri + # uri="/api/sns/web/v1/user_posted" # v0.1.2及以前版本需要主动提取uri + a1_value="your_a1_cookie_value", + params={"num": "30", "cursor": "", "user_id": "123"} +) + +# POST 请求签名 +x_s = client.sign_xs_post( + uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", + a1_value="your_a1_cookie_value", + payload={"username": "test", "password": "123456"} +) + +# 生成其他 headers 字段 +x_t = client.get_x_t() # 时间戳(毫秒) +x_b3_traceid = client.get_b3_trace_id() # 16位随机 trace id +x_xray_traceid = client.get_xray_trace_id() # 32位 trace id + +# 手动构建 headers +headers = { + "x-s": x_s, + "x-t": str(x_t), + "x-b3-traceid": x_b3_traceid, + "x-xray-traceid": x_xray_traceid +} + +# 使用统一时间戳(可选,确保所有字段使用相同时间) +import time +timestamp = time.time() + +x_s = client.sign_xs_get( + uri="/api/sns/web/v1/user_posted", + a1_value="your_a1_cookie_value", + params={"num": "30"}, + timestamp=timestamp # 传入统一时间戳 +) +x_t = client.get_x_t(timestamp=timestamp) +x_xray_traceid = client.get_xray_trace_id(timestamp=int(timestamp * 1000)) +``` + +
+ ### 解密签名 ```python From b95df9631ec69e2025413bd25b874cabdfb27ad7 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 12:11:47 +0800 Subject: [PATCH 07/19] fuck style --- src/xhshow/utils/random_gen.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/xhshow/utils/random_gen.py b/src/xhshow/utils/random_gen.py index 6a7705b..0359cc9 100644 --- a/src/xhshow/utils/random_gen.py +++ b/src/xhshow/utils/random_gen.py @@ -77,8 +77,6 @@ def generate_xray_trace_id(self, timestamp: int | None = None, seq: int | None = f"0{self.config.XRAY_TRACE_ID_PART1_LENGTH}x", ) # Last 16 chars: completely random, untraceable, can be simplified - part2 = "".join( - random.choice(self.config.HEX_CHARS) for _ in range(self.config.XRAY_TRACE_ID_PART2_LENGTH) - ) + part2 = "".join(random.choice(self.config.HEX_CHARS) for _ in range(self.config.XRAY_TRACE_ID_PART2_LENGTH)) return part1 + part2 From 9ed3ebaf70d9d1eeb790892ae65734723831da73 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 12:17:29 +0800 Subject: [PATCH 08/19] feat(client): add parameter validation for sign_headers method - validate GET requests only use params, not payload - validate POST requests only use payload, not params - raise ValueError for unsupported HTTP methods (only GET/POST allowed) - add comprehensive test coverage for parameter validation - improve error messages for better developer experience --- src/xhshow/client.py | 17 ++++++++++++++--- tests/test_crypto.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index ac63cac..977a4bb 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -380,10 +380,21 @@ def sign_headers( if timestamp is None: timestamp = time.time() - # Use params for GET, payload for POST - request_data = params if method.upper() == "GET" else payload + method_upper = method.upper() + + # Validate method and parameters + if method_upper == "GET": + if payload is not None: + raise ValueError("GET requests must use 'params', not 'payload'") + request_data = params + elif method_upper == "POST": + if params is not None: + raise ValueError("POST requests must use 'payload', not 'params'") + request_data = payload + else: + raise ValueError(f"Unsupported method: {method}") - x_s = self.sign_xs(method, uri, a1_value, xsec_appid, request_data, timestamp) + x_s = self.sign_xs(method_upper, uri, a1_value, xsec_appid, request_data, timestamp) x_t = self.get_x_t(timestamp) x_b3_traceid = self.get_b3_trace_id() x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 2053170..70be442 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -614,3 +614,34 @@ def test_sign_headers_post(self): ) assert headers_ts["x-t"] == str(int(custom_ts * 1000)) + + def test_sign_headers_parameter_validation(self): + """测试 sign_headers 参数验证""" + client = Xhshow() + + # Test GET request with payload should raise error + with pytest.raises(ValueError, match="GET requests must use 'params', not 'payload'"): + client.sign_headers( + method="GET", + uri="/api/test", + a1_value="test_a1", + payload={"key": "value"}, + ) + + # Test POST request with params should raise error + with pytest.raises(ValueError, match="POST requests must use 'payload', not 'params'"): + client.sign_headers( + method="POST", + uri="/api/test", + a1_value="test_a1", + params={"key": "value"}, + ) + + # Test unsupported method should raise error + with pytest.raises(ValueError, match="Unsupported method"): + client.sign_headers( + method="PUT", + uri="/api/test", + a1_value="test_a1", + params={"key": "value"}, + ) From 7e8effb2f3fcf519d41d96f3ae906178cea452cf Mon Sep 17 00:00:00 2001 From: Edward Date: Mon, 1 Dec 2025 18:12:34 +0800 Subject: [PATCH 09/19] Add the cryptodemo library, using a custom encode_to_b64 method instead of the official standard base64 method. Add some initialization parameters to the configuration file to generate fingerprints. Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method. --- pyproject.toml | 4 +- src/xhshow/config/config.py | 34 ++- src/xhshow/generate_fingerprint.py | 351 +++++++++++++++++++++++++++++ src/xhshow/utils/encoder.py | 64 +++++- uv.lock | 39 ++++ 5 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 src/xhshow/generate_fingerprint.py diff --git a/pyproject.toml b/pyproject.toml index f33adce..6495970 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,9 @@ classifiers = [ "Topic :: Security :: Cryptography", "Typing :: Typed", ] -dependencies = [] +dependencies = [ + "pycryptodome>=3.23.0", +] [project.optional-dependencies] dev = [ diff --git a/src/xhshow/config/config.py b/src/xhshow/config/config.py index 6fcf380..80d4dbc 100644 --- a/src/xhshow/config/config.py +++ b/src/xhshow/config/config.py @@ -7,6 +7,13 @@ @dataclass(frozen=True) class CryptoConfig: """Configuration constants for cryptographic operations""" + # Gid encrypt parameters + DES_KEY = "zbp30y86" + GID_URL = "https://as.xiaohongshu.com/api/sec/v1/shield/webprofile" + DATA_PALTFORM = "Windows" + DATA_SVN = "2" + DATA_SDK_VERSION = "4.2.6" + DATA_webBuild = '4.86.0' # Bitwise operation constants MAX_32BIT: int = 0xFFFFFFFF @@ -59,7 +66,7 @@ class CryptoConfig: ) # Prefix constants - X3_PREFIX: str = "mns0301_" + X3_PREFIX: str = "mns0101_" XYS_PREFIX: str = "XYS_" # Trace ID generation constants @@ -70,6 +77,31 @@ class CryptoConfig: XRAY_TRACE_ID_PART2_LENGTH: int = 16 B3_TRACE_ID_LENGTH: int = 16 + # b1 secret key + B1_SECRET_KEY: str = "xhswebmplfbt" + + SIGNATURE_XSCOMMON_TEMPLATE: dict[str, Any] = field( + default_factory=lambda: { + "s0": 5, + "s1": "", + "x0": "1", + "x1": "4.2.6", + "x2": "Windows", + "x3": "xhs-pc-web", + "x4": "4.86.0", + "x5": "", + "x6": "", + "x7": "", + "x8": "", + "x9": -596800761, + "x10": 0, + "x11": "normal" + } + ) + + PUBLIC_USERAGENT: str = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0") + def with_overrides(self, **kwargs: Any) -> "CryptoConfig": """ Create a new config instance with overridden values diff --git a/src/xhshow/generate_fingerprint.py b/src/xhshow/generate_fingerprint.py new file mode 100644 index 0000000..0c17673 --- /dev/null +++ b/src/xhshow/generate_fingerprint.py @@ -0,0 +1,351 @@ +""" +Created on Wed Nov 5 2025 AM +@author: +Describe: generate fingerprint payload for encrypt x-s-common params, that current FP include much important information +1. user_agent 2. cookie info 3. timeZone need same current request location, just be care concurrent with proxy addr +4. encrypt code version 5.whether browser has been tampered picture hash value 6. some others detect(hook, screen) +""" +from http.cookies import SimpleCookie +from .config import CryptoConfig +from Crypto.Cipher import ARC4 +from .utils import encoder + +import urllib.parse +import hashlib +import secrets +import random +import json +import time + + +class XhsFpGenerator(object): + """XHS Fingerprint generate function""" + + def __init__(self, config: CryptoConfig): + self.config = config + self.__b1_key = self.config.B1_SECRET_KEY.encode() + + @staticmethod + def __weighted_random_choice(options: list, weights: list) -> any: + """ + Random choice a value from list according to the given weights + Argument: + options (list): option list + weights (list): weight list mapping the option list(Without Normalization) + """ + return f"{random.choices(options, weights=weights, k=1)[0]}" + + @staticmethod + def __get_renderer_info() -> list: + renderer_info_list = [ + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 400 (0x00000166) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4400 (0x00001112) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4600 (0x00000412) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 520 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 550 (0x00001512) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 6000 (0x1606) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 540 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 550 (0x1913) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640 (0x161C) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 600 (0x3E80) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 655 (0x00009BC8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe MAX Graphics (0x00009BC0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A370M (0x0000AF51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380 (0x0000AF41) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380M (0x0000AF5E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A550 (0x0000AF42) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770 (0x0000AF43) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770M (0x0000AF50) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RPL‑P GT1) (0x0000A702) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics 770 (0x00004680) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) HD Graphics 4400 (0x00001122) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (ADL‑S GT1) (0x0000A0A1) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RKL GT1) (0x0000A9A1) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics (CML GT2) (0x00009A14) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00001022) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics Family (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris Pro OpenGL Engine, OpenGL 4.1)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 645 (0x1616) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 655 (0x161E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 730 (0x0000A100) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 805 (0x0000B0A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 3 Graphics (0x000015E0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 8 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 11 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Graphics (0x00001636) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 5500 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 560 (0x000067EF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 570 (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 580 2048SP (0x00006FDF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 590 (0x000067FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 XT (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6650 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6700 XT (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6800 (0x000073BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6900 XT (0x000073C2) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 7700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5300M OpenGL Engine, OpenGL 4.1)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5500 XT OpenGL Engine, OpenGL 4.1)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon R7 370 Series (0x00006811) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, ATI Technologies Inc. AMD Radeon RX Vega 64 OpenGL Engine, OpenGL 4.1)", + + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 (0x00001C81) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti (0x00001C8C) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 6GB (0x000010DE) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1070 (0x00001B81) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 (0x00001B80) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 SUPER (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 SUPER (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x0000250F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Ti (0x00002489) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002488) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Ti (0x000028A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 (0x00002206) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Ti (0x00002208) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3090 (0x00002204) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 (0x00002882) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti (0x00002803) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Ti (0x00002857) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4080 (0x00002819) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 (0x00002684) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro RTX 5000 Ada Generation (0x000026B2) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro P400 (0x00001CB3) Direct3D11 vs_5_0 ps_5_0, D3D11)", + + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)", + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)", + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device), SwiftShader driver)", + ] + + return random.choice(renderer_info_list).split("|") + + @staticmethod + def __get_width_and_height(): + width, height = XhsFpGenerator.__weighted_random_choice( + ["1366;768", "1600;900", "1920;1080", "2560;1440", "3840;2160", "7680;4320"], + [0.25, 0.15, 0.35, 0.15, 0.08, 0.02]).split(';') + if random.choice([True, False]): + availWidth = int(width) - int( + XhsFpGenerator.__weighted_random_choice([0, 30, 60, 80], [0.1, 0.4, 0.3, 0.2])) + availHeight = height + else: + availWidth = width + availHeight = int(height) - int( + XhsFpGenerator.__weighted_random_choice([30, 60, 80, 100], [0.2, 0.5, 0.2, 0.1])) + + return width, height, availWidth, availHeight + + def generate_b1(self, fp): + config = CryptoConfig() + b64_encoder = encoder.Base64Encoder(config) + b1_fp = { + "x33": fp['x33'], + "x34": fp['x34'], + "x35": fp['x35'], + "x36": fp['x36'], + "x37": fp['x37'], + "x38": fp['x38'], + "x39": fp['x39'], + "x42": fp['x42'], + "x43": fp['x43'], + "x44": fp['x44'], + "x45": fp['x45'], + "x46": fp['x46'], + "x48": fp['x48'], + "x49": fp['x49'], + "x50": fp['x50'], + "x51": fp['x51'], + "x52": fp['x52'], + "x82": fp['x82'], + } + b1_json = json.dumps(b1_fp, separators=(',', ':'), ensure_ascii=False) + cipher = ARC4.new(self.__b1_key) + ciphertext = cipher.encrypt(b1_json.encode('utf-8')).decode('latin1') + encoded_url = urllib.parse.quote(ciphertext, safe="!*'()~_-") + b = [] + for c in encoded_url.split('%')[1:]: + chars = list(c) + b.append(int(''.join(chars[:2]), 16)) + [b.append(ord(j)) for j in chars[2:]] + + b1 = b64_encoder.encode_to_b64(bytearray(b)) + + return b1 + + @staticmethod + def get_fingerprint(cookies: dict, user_agent: str) -> dict: + cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) + + width, height, availWidth, availHeight = XhsFpGenerator.__get_width_and_height() + + is_incognito_mode = XhsFpGenerator.__weighted_random_choice(['true', 'false'], [0.95, 0.05]) + + vendor, renderer = XhsFpGenerator.__get_renderer_info() + + x78_y = random.randint(2350, 2450) + fp = { + "x1": user_agent, + "x2": "false", # navigator.webdriver + "x3": "zh-CN", # navigator.language + "x4": XhsFpGenerator.__weighted_random_choice([16, 24, 30, 32], [0.05, 0.6, 0.05, 0.3]), + # screen.colorDepth + "x5": XhsFpGenerator.__weighted_random_choice([1, 2, 4, 8, 12, 16], [0.10, 0.25, 0.4, 0.2, 0.03, 0.01]), + # navigator.deviceMemory + "x6": "24", # screen.pixelDepth + "x7": f"{vendor},{renderer}", + "x8": XhsFpGenerator.__weighted_random_choice([2, 4, 6, 8, 12, 16, 24, 32], + [0.1, 0.4, 0.2, 0.15, 0.08, 0.04, 0.02, 0.01]), + "x9": f"{width};{height}", + "x10": f"{availWidth};{availHeight}", + "x11": "-480", # new Date().getTimezoneOffset()。 + "x12": "Asia/Shanghai", # Intl.DateTimeFormat().resolvedOptions().timeZone default timezone + "x13": is_incognito_mode, # window.sessionStorage detect + "x14": is_incognito_mode, # window.localStorage detect + "x15": is_incognito_mode, # window.indexedDB detect + "x16": "false", + "x17": "false", + "x18": "un", + "x19": "Win32", + "x20": "", + "x21": "PDF Viewer,Chrome PDF Viewer,Chromium PDF Viewer,Microsoft Edge PDF Viewer,WebKit built-in PDF", + # navigator.plugins + "x22": hashlib.md5(secrets.token_bytes(32)).hexdigest(), + "x23": "false", # DOM environment detect + "x24": "false", + "x25": "false", + "x26": "false", + "x27": "false", + "x28": "0,false,false", + "x29": "4,7,8", + "x30": "swf object not loaded", # detect Flash(SWF) Boolean(navigator.plugins['Shockwave Flash']); + # "x32": "0", # haven't used + "x33": "0", # whether in WeChat browser constant + "x34": "0", # whether js paint tool is Brian Paul constant + "x35": "0", # whether did have loaded Modernizr constant + "x36": f"{random.randint(1, 20)}", # window.history.length history stack length + "x37": "0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|1|0|0|0|0|0", + "x38": "0|0|1|0|1|0|0|0|0|0|1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0", + "x39": 0, # localStorage.getItem('sc'); plus one by every fresh page + "x40": "0", # localStorage.getItem('ptt'); + "x41": "0", # localStorage.getItem('pst'); + "x42": "3.4.4", # Fingerprint.js version // constant + "x43": "742cc32c", # Detecting whether a browser has been tampered with by comparing + # the hash value of an image + + "x44": f"{int(time.time() * 1000)}", # current timestamp + "x45": "__SEC_CAV__1-1-1-1-1|__SEC_WSA__|", + # risk control SDK information if risk just show like __SEC_WSA__| + "x46": "false", + # navigator.__proto__.hasOwnProperty('webdriver'); risk control + # Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver'); // true → risk control + "x47": "1|0|0|0|0|0", # recognize different browser「unique feature」 // constant + "x48": "", + "x49": '{list:[],type:}', + "x50": "", + "x51": "", + "x52": "", + "x55": "380,380,360,400,380,400,420,380,400,400,360,360,440,420", + "x56": f"{vendor}|{renderer}|{hashlib.md5(secrets.token_bytes(32)).hexdigest()}|35", + # x7 | Fingerprint2.x64hash128(WebGLRenderingContext.getSupportedExtensions()) | WebGLRenderingContext.getSupportedExtensions().length + "x57": cookie_string, + "x58": "180", # document.getElementsByTagName('div') // count div (just above 177, not strict) constant + "x59": "2", # performance.getEntriesByType("resource").length + "x60": "63", # risk control score // constant + "x61": "1291", # Object.getOwnPropertyNames(window) .length // window object amount + "x62": "2047", # HOOK detect 1,1,1,1,1,1,1,1,1,1,1 11个1(通过) 组成二进制2047 constant + "x63": "0", # JS VMP file \n detect // constant + "x64": "0", # HOOK ToString number detect + "x65": "0", + "x66": { # navigator.userAgent + "referer": "", + "location": "https://www.xiaohongshu.com/explore", + "frame": 0 + }, + "x67": "1|0", + "x68": "0", + "x69": "326|1292|30", + "x70": [ + "location" + ], + "x71": "true", + "x72": "complete", + "x73": "1191", + "x74": "0|0|0", + "x75": "Google Inc.", # Navigator.vendor + "x76": "true", # navigator.cookieEnabled + "x77": "1|1|1|1|1|1|1|1|1|1", # constant + "x78": { + "x": 0, + "y": x78_y, + "left": 0, + "right": 290.828125, + "bottom": x78_y + 18, + "height": 18, + "top": x78_y, + "width": 290.828125, + "font": "system-ui, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\", -apple-system, \"Segoe UI\", Roboto, Ubuntu, Cantarell, \"Noto Sans\", sans-serif, BlinkMacSystemFont, \"Helvetica Neue\", Arial, \"PingFang SC\", \"PingFang TC\", \"PingFang HK\", \"Microsoft Yahei\", \"Microsoft JhengHei\"" + }, + "x82": "_0x17a2|_0x1954", # get iframe.contentWindow | contentWindow.window + "x31": "124.04347527516074", + "x79": "144|599565058866", + # navigator.webkitTemporaryStorage.queryUsageAndQuota(used, granted) + "x53": hashlib.md5(secrets.token_bytes(32)).hexdigest(), + # "235c6559af50acefe4755120d05570a0" if "edge/" in user_agent else "993da9a681fd3994c9f53de11f2903b3", + # speechSynthesis.getVoices() Fingerprint2.x64hash128 + "x54": "10311144241322244122", + "x80": "1|[object FileSystemDirectoryHandle]", + } + + return fp + + @staticmethod + def update_fingerprint(fp: dict, cookies: dict, url: str) -> None: + cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) + + fp.update({ + "x39": 0, # localStorage.getItem('p1'); Add the value with every request +1 + "x44": f"{time.time() * 1000}", # current timestamp multiply 1000 + "x57": cookie_string, + "x66": { # navigator.userAgent + "referer": "https://www.xiaohongshu.com/explore", + "location": url, + "frame": 0 + } + }) + + +# test +# if __name__ == '__main__': +# cookie_str = ( +# 'abRequestId=6824d155-7a52-5954-8e0b-52621534b645; a1=199326716575ch0ke4fatjw4p6s5f36p9gyb1z0ep50000367754' +# '; webId=236dbdde593bd45175a14b220bf2cda8; gid' +# '=yjjqJKWqYSjWyjjqJKWyKku82W2Sx8Td4i09vdUVE4UKMD282iqKUy888qKWW2482dfW0SJf; x-user-id-creator.xiaohongshu' +# '.com=62653884000000001000d895; customerClientId=211311472456037; access-token-creator.xiaohongshu.com' +# '=customer.creator.AT-68c5175501602299901050976ci4wzrowbjolncn; galaxy_creator_session_id' +# '=dv0q3EYVV52XFb03dqv9wFqc5wOrcnZBBG6q; galaxy.creator.beaker.session.id=1757908666550096704063; webBuild=4' +# '.81.0; web_session=040069b9c0b8fa37713448b6e43a4b16f2f1b5; xsecappid=xhs-pc-web; loadts=1758606105469; ' +# 'acw_tc=0ad586d517586066572636904e2fbedce995fe2d22709cedcfeabec5ab856d; websectiga' +# '=10f9a40ba454a07755a08f27ef8194c53637eba4551cf9751c009d9afb564467; sec_poison_id=7d693d02-b858-4476-a432' +# '-5c003b3ff9f0; unread={%22ub%22:%2268d13701000000000b03cfc1%22%2C%22ue%22:%2268c26c99000000001d026ca5%22%2C' +# '%22uc%22:24}') +# c = SimpleCookie() +# c.load(cookie_str) +# cookie_dict = {k: morsel.value for k, morsel in c.items()} +# +# headers = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 " +# "Safari/537.36") +# +# crypto_config = CryptoConfig() +# gfp = XhsFpGenerator(crypto_config) +# fp = gfp.get_fingerprint(cookies=cookie_dict, user_agent=headers) +# b1 = gfp.generate_b1(fp) +# print(b1) diff --git a/src/xhshow/utils/encoder.py b/src/xhshow/utils/encoder.py index 8395c7e..dd1c8e9 100644 --- a/src/xhshow/utils/encoder.py +++ b/src/xhshow/utils/encoder.py @@ -12,23 +12,75 @@ class Base64Encoder: def __init__(self, config: CryptoConfig): self.config = config - def encode(self, data_to_encode: str) -> str: + def encode_to_b64(self, data_to_encode: str | bytes, alphabet: str = CryptoConfig.CUSTOM_BASE64_ALPHABET) -> str: """ Encode a string using custom Base64 alphabet Args: data_to_encode: Original UTF-8 string to be encoded + alphabet: Base64 alphabet to use for encoding Returns: Base64 string encoded using custom alphabet """ - data_bytes = data_to_encode.encode("utf-8") - standard_encoded_bytes = base64.b64encode(data_bytes) - standard_encoded_string = standard_encoded_bytes.decode("utf-8") + if isinstance(data_to_encode, str): + b = data_to_encode.encode("utf-8") + elif isinstance(data_to_encode, (bytes, bytearray, memoryview)): + b = bytes(data_to_encode) + else: + try: + b = bytes((int(x) & 0xFF for x in data_to_encode)) # type: ignore[arg-type] + except TypeError as e: + raise TypeError( + f"unsupported type: {type(data_to_encode)} (expected bytes/str/Iterable[int])" + ) from e + + n = len(b) + rem = n % 3 + stop = n - rem + out_parts: list[str] = [] + + CHUNK = 16383 + for i in range(0, stop, CHUNK): + end = min(i + CHUNK, stop) + j = i + chunk_out: list[str] = [] + while j < end: + val = (b[j] << 16) | (b[j + 1] << 8) | b[j + 2] + chunk_out.append( + alphabet[(val >> 18) & 63] + + alphabet[(val >> 12) & 63] + + alphabet[(val >> 6) & 63] + + alphabet[val & 63] + ) + j += 3 + out_parts.append("".join(chunk_out)) + + if rem == 1: + e = b[-1] + out_parts.append(alphabet[e >> 2] + alphabet[(e << 4) & 63] + "==") + elif rem == 2: + e = (b[-2] << 8) | b[-1] + out_parts.append( + alphabet[(e >> 10) & 63] + + alphabet[(e >> 4) & 63] + + alphabet[(e << 2) & 63] + + "=" + ) + + return "".join(out_parts) - translation_table = str.maketrans(self.config.STANDARD_BASE64_ALPHABET, self.config.CUSTOM_BASE64_ALPHABET) + def encode(self, data_to_encode: str) -> str: + """ + Encode a string using custom Base64 alphabet (compatibility method) - return standard_encoded_string.translate(translation_table) + Args: + data_to_encode: Original UTF-8 string to be encoded + + Returns: + Base64 string encoded using custom alphabet + """ + return self.encode_to_b64(data_to_encode, self.config.CUSTOM_BASE64_ALPHABET) def decode(self, encoded_string: str) -> str: """ diff --git a/uv.lock b/uv.lock index dc4b790..4bf4396 100644 --- a/uv.lock +++ b/uv.lock @@ -132,6 +132,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -248,6 +283,9 @@ wheels = [ [[package]] name = "xhshow" source = { editable = "." } +dependencies = [ + { name = "pycryptodome" }, +] [package.optional-dependencies] dev = [ @@ -267,6 +305,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, From daaa086517630cc76485ad021489bba0d56070ff Mon Sep 17 00:00:00 2001 From: Edward Date: Fri, 5 Dec 2025 16:24:35 +0800 Subject: [PATCH 10/19] Add the cryptodemo library, using a custom encode_to_b64 method instead of the official standard base64 method. Add some initialization parameters to the configuration file to generate fingerprints. ADD CRC32_encrypt for gen xs-common Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method. Use case: cookie = "you cookie dict or string" xs_common = client.sign_xsc(cookie) --- src/__init__.py | 0 src/xhshow/client.py | 59 +++++++- src/xhshow/config/config.py | 2 +- src/xhshow/core/crc32_encrypt.py | 130 ++++++++++++++++++ src/xhshow/utils/encoder.py | 64 +++++++++ .../{ => utils}/generate_fingerprint.py | 6 +- src/xhshow/utils/validators.py | 45 ++++++ tests/test_crypto.py | 2 +- 8 files changed, 302 insertions(+), 6 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/xhshow/core/crc32_encrypt.py rename src/xhshow/{ => utils}/generate_fingerprint.py (99%) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 977a4bb..2631500 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -2,15 +2,19 @@ import json import time from typing import Any, Literal +from http.cookies import SimpleCookie from .config import CryptoConfig +from .core.crc32_encrypt import CRC32 from .core.crypto import CryptoProcessor from .utils.random_gen import RandomGenerator +from .utils.generate_fingerprint import XhsFpGenerator from .utils.url_utils import build_url, extract_uri from .utils.validators import ( validate_get_signature_params, validate_post_signature_params, validate_signature_params, + validate_xs_common_params, ) __all__ = ["Xhshow"] @@ -136,10 +140,43 @@ def sign_xs( signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + self._build_signature( d_value, a1_value, xsec_appid, content_string, timestamp ) - return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode( + return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode_to_b64( json.dumps(signature_data, separators=(",", ":"), ensure_ascii=False) ) + def sign_xs_common( + self, + cookie_dict: dict[str, Any] | str, + ) -> str: + """ + Convenience wrapper to generate the `x-s-common` signature. + + Args: + cookie_dict: complete cookie dictionary + + Returns: + Encoded signature string suitable for the header. + """ + if isinstance(cookie_dict, str): + ck = SimpleCookie() + ck.load(cookie_dict) + cookie_dict = {k: morsel.value for k, morsel in ck.items()} + + gfp = XhsFpGenerator(self.config) + a1_value = cookie_dict['a1'] + fingerprint = gfp.get_fingerprint(cookies=cookie_dict, user_agent=self.crypto_processor.config.PUBLIC_USERAGENT) + b1 = gfp.generate_b1(fingerprint) + + x9 = CRC32.crc32_js_int(b1) + sign_struct = CryptoConfig.SIGNATURE_XSCOMMON_TEMPLATE + sign_struct['x5'] = a1_value + sign_struct['x8'] = b1 + sign_struct['x9'] = x9 + sign_json = json.dumps(sign_struct, separators=(",", ":"), ensure_ascii=False) + sign_list = list(sign_json.encode('utf-8')) + xs_common = self.crypto_processor.b64encoder.custom_to_b64(sign_list) + return xs_common + @validate_get_signature_params def sign_xs_get( self, @@ -200,6 +237,26 @@ def sign_xs_post( """ return self.sign_xs("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) + @validate_xs_common_params + def sign_xsc( + self, + cookie_dict: dict[str, Any] | str, + ) -> str: + """ + Convenience wrapper to generate the `x-s-common` signature. + + Args: + cookie_dict: Enter your complete cookie dictionary + + Returns: + Encoded signature string suitable for the `x-s-common` header. + + Raises: + TypeError: Parameter type error + ValueError: Parameter value error + """ + return self.sign_xs_common(cookie_dict) + def decode_x3(self, x3_signature: str) -> bytearray: """ Decrypt x3 signature (Base64 format) diff --git a/src/xhshow/config/config.py b/src/xhshow/config/config.py index 80d4dbc..4686c93 100644 --- a/src/xhshow/config/config.py +++ b/src/xhshow/config/config.py @@ -13,7 +13,7 @@ class CryptoConfig: DATA_PALTFORM = "Windows" DATA_SVN = "2" DATA_SDK_VERSION = "4.2.6" - DATA_webBuild = '4.86.0' + DATA_webBuild = '5.0.3' # Bitwise operation constants MAX_32BIT: int = 0xFFFFFFFF diff --git a/src/xhshow/core/crc32_encrypt.py b/src/xhshow/core/crc32_encrypt.py new file mode 100644 index 0000000..7adb4b7 --- /dev/null +++ b/src/xhshow/core/crc32_encrypt.py @@ -0,0 +1,130 @@ +""" +Custom CRC32 helper wrapped in a class. + +Main entry: + CRC32.crc32_js_int(data) + +This implements a JavaScript-style CRC32 variant compatible with: + + (-1 ^ c ^ 0xEDB88320) >>> 0 + +where `c` is the intermediate CRC state produced by the core CRC32 loop. +""" + +from __future__ import annotations +from typing import Iterable, Union + + +DataLike = Union[str, bytes, bytearray, memoryview, Iterable[int]] +__all__ = ["CRC32"] + + +class CRC32: + """CRC32 calculator with a JS-compatible static entry.""" + + MASK32: int = 0xFFFFFFFF + POLY: int = 0xEDB88320 + _TABLE: list[int] | None = None + + @classmethod + def _ensure_table(cls) -> None: + """Lazy-init CRC32 lookup table once.""" + if cls._TABLE is not None: + return + + tbl = [0] * 256 + for d in range(256): + r = d + for _ in range(8): + r = ((r >> 1) ^ cls.POLY) if (r & 1) else (r >> 1) + r &= cls.MASK32 + tbl[d] = r + cls._TABLE = tbl + + @classmethod + def _crc32_core(cls, data: DataLike, *, string_mode: str = "js") -> int: + """ + Core CRC32 state update (no final NOT/XOR). + + Args: + data: + - str: interpreted depending on ``string_mode``: + * "js": lower 8 bits of ord(char) (JS charCodeAt behavior) + * "utf8": UTF-8 encode string to bytes first + - bytes / bytearray / memoryview: used as-is + - Iterable[int]: each value will be ``& 0xFF`` and treated as a byte + string_mode: How to handle string input ("js" or "utf8"). + + Returns: + Intermediate CRC state `c` (before final bitwise NOT / XOR). + """ + cls._ensure_table() + assert cls._TABLE is not None # for type checkers + + c = cls.MASK32 + + if isinstance(data, (bytes, bytearray, memoryview)): + it = bytes(data) + elif isinstance(data, str): + if string_mode.lower() == "utf8": + it = data.encode("utf-8") + else: # "js" mode: charCodeAt & 0xFF + it = (ord(ch) & 0xFF for ch in data) + else: + it = ((int(b) & 0xFF) for b in data) + + for b in it: + c = (cls._TABLE[((c & 0xFF) ^ b) & 0xFF] ^ (c >> 8)) & cls.MASK32 + + return c + + @staticmethod + def _to_signed32(u: int) -> int: + """ + Convert an unsigned 32-bit int to signed 32-bit representation. + + Args: + u: Unsigned 32-bit integer (0..0xFFFFFFFF). + + Returns: + Signed 32-bit integer in range [-2^31, 2^31-1]. + """ + return u - 0x100000000 if (u & 0x80000000) else u + + @classmethod + def crc32_js_int( + cls, + data: DataLike, + *, + string_mode: str = "js", + signed: bool = True, + ) -> int: + """ + JavaScript-style CRC32 public entry. + + This matches the JS expression: + + (-1 ^ c ^ 0xEDB88320) >>> 0 + + where `c` is the intermediate CRC state from `_crc32_core`. + + Args: + data: Input data (str/bytes/iterable of ints). + string_mode: How to treat string input ("js" or "utf8"). + signed: + If True, return signed 32-bit integer; + If False, return unsigned 32-bit integer (0..0xFFFFFFFF). + + Returns: + CRC32 value as 32-bit integer (signed or unsigned). + """ + c = cls._crc32_core(data, string_mode=string_mode) + a = cls.POLY + u = ((cls.MASK32 ^ c) ^ a) & cls.MASK32 # (-1 ^ c ^ a) >>> 0 + return cls._to_signed32(u) if signed else u + + +# Example quick usage: +# if __name__ == "__main__": +# s = "I38rHdgsjopgIvesdVwgIC+oIELmBZ5e3VwXLgFTIxS3bqwErFeexd0ekncAzMFYnqthIhJeSBMDKutRI3KsYorWHPtGrbV0P9W" +# print(CRC32.crc32_js_int(s)) diff --git a/src/xhshow/utils/encoder.py b/src/xhshow/utils/encoder.py index dd1c8e9..2ae4b26 100644 --- a/src/xhshow/utils/encoder.py +++ b/src/xhshow/utils/encoder.py @@ -3,6 +3,7 @@ import base64 import binascii +from typing import Iterable from ..config import CryptoConfig __all__ = ["Base64Encoder"] @@ -82,6 +83,69 @@ def encode(self, data_to_encode: str) -> str: """ return self.encode_to_b64(data_to_encode, self.config.CUSTOM_BASE64_ALPHABET) + @staticmethod + def custom_to_b64(data: bytes | str | Iterable[int]) -> str: + """ + XHS official encrypt method (tripletToBase64). same as above function, but FP gen must be deal binary string + support: + - str: use UTF-8 encode to byte + - bytes/bytearray/memory view: use it straight + - Iterable[int] (eg: list[int]): bitwise with & 0xFF trans to single bit + + Returns: + Base64 string encoded using custom alphabet + """ + alphabet = CryptoConfig.CUSTOM_BASE64_ALPHABET + + # —— all datas convert to bytes —— # + if isinstance(data, str): + b = data.encode("utf-8") + elif isinstance(data, (bytes, bytearray, memoryview)): + b = bytes(data) + else: + try: + # allow list/tuple/any could be iterable int, auto & 0xFF auto filter the value not between 0..255 + b = bytes((int(x) & 0xFF for x in data)) # type: ignore[arg-type] + except TypeError as e: + raise TypeError( + f"unsupported type: {type(data)} (expected bytes/str/Iterable[int])" + ) from e + + n = len(b) + rem = n % 3 + stop = n - rem + out_parts: list[str] = [] + + CHUNK = 16383 + for i in range(0, stop, CHUNK): + end = min(i + CHUNK, stop) + j = i + chunk_out: list[str] = [] + while j < end: + val = (b[j] << 16) | (b[j + 1] << 8) | b[j + 2] + chunk_out.append( + alphabet[(val >> 18) & 63] + + alphabet[(val >> 12) & 63] + + alphabet[(val >> 6) & 63] + + alphabet[val & 63] + ) + j += 3 + out_parts.append("".join(chunk_out)) + + if rem == 1: + e = b[-1] + out_parts.append(alphabet[e >> 2] + alphabet[(e << 4) & 63] + "==") + elif rem == 2: + e = (b[-2] << 8) | b[-1] + out_parts.append( + alphabet[(e >> 10) & 63] + + alphabet[(e >> 4) & 63] + + alphabet[(e << 2) & 63] + + "=" + ) + + return "".join(out_parts) + def decode(self, encoded_string: str) -> str: """ Decode string using custom Base64 alphabet diff --git a/src/xhshow/generate_fingerprint.py b/src/xhshow/utils/generate_fingerprint.py similarity index 99% rename from src/xhshow/generate_fingerprint.py rename to src/xhshow/utils/generate_fingerprint.py index 0c17673..e3cb351 100644 --- a/src/xhshow/generate_fingerprint.py +++ b/src/xhshow/utils/generate_fingerprint.py @@ -6,9 +6,9 @@ 4. encrypt code version 5.whether browser has been tampered picture hash value 6. some others detect(hook, screen) """ from http.cookies import SimpleCookie -from .config import CryptoConfig +from src.xhshow.config import CryptoConfig +from src.xhshow.utils import encoder from Crypto.Cipher import ARC4 -from .utils import encoder import urllib.parse import hashlib @@ -177,7 +177,7 @@ def generate_b1(self, fp): b.append(int(''.join(chars[:2]), 16)) [b.append(ord(j)) for j in chars[2:]] - b1 = b64_encoder.encode_to_b64(bytearray(b)) + b1 = b64_encoder.custom_to_b64(bytearray(b)) return b1 diff --git a/src/xhshow/utils/validators.py b/src/xhshow/utils/validators.py index 82ad3d1..6570dce 100644 --- a/src/xhshow/utils/validators.py +++ b/src/xhshow/utils/validators.py @@ -12,6 +12,7 @@ "validate_signature_params", "validate_get_signature_params", "validate_post_signature_params", + "validate_xs_common_params", ] @@ -76,6 +77,18 @@ def validate_payload(payload: Any) -> dict[str, Any] | None: return payload + @staticmethod + def validate_cookie(cookie: Any) -> dict[str, Any] | str: + """Validate cookie parameter""" + if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)): + raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}") + # detect cookie dict validation + if cookie is not None and isinstance(cookie, dict): + for key in cookie.keys(): + if not isinstance(key, str): + raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'") + return cookie + def validate_signature_params(func: F) -> F: # type: ignore # noqa: UP047 """ @@ -195,3 +208,35 @@ def wrapper( ) return wrapper # type: ignore + + +def validate_xs_common_params(func: F) -> F: # type: ignore[misc] # noqa: UP047 + """ + Parameter validation decorator for the `sign_xsc` method. + + This wrapper normalizes and validates the arguments before delegating to + the underlying signing implementation. + + Args: + func: Method to be decorated. + + Returns: + Wrapped method with validated parameters. + """ + + @wraps(func) + def wrapper( + self, + cookie_dict: dict[str, Any] | None = None, + ) -> str: + validator = RequestSignatureValidator() + + # Reuse existing validators where possible + validated_cookie_dict = validator.validate_cookie(cookie_dict) + + return func( + self, + validated_cookie_dict, + ) + + return wrapper # type: ignore diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 70be442..a32ed58 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -54,7 +54,7 @@ def test_base58_encoder(self): def test_base64_encoder(self): """测试自定义Base64编码""" test_string = "Hello, World!" - result = self.crypto.b64encoder.encode(test_string) + result = self.crypto.b64encoder.encode_to_b64(test_string) assert isinstance(result, str) assert len(result) > 0 From 497a686b872156c30db16d9ddb150e1b32a062eb Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 21:43:59 +0800 Subject: [PATCH 11/19] style: format code to match project standards --- src/xhshow/client.py | 14 +- src/xhshow/config/config.py | 11 +- src/xhshow/core/crc32_encrypt.py | 6 +- src/xhshow/utils/encoder.py | 32 +--- src/xhshow/utils/generate_fingerprint.py | 200 ++++++++++++----------- 5 files changed, 127 insertions(+), 136 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 2631500..10d4259 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -1,14 +1,14 @@ import hashlib import json import time -from typing import Any, Literal from http.cookies import SimpleCookie +from typing import Any, Literal from .config import CryptoConfig from .core.crc32_encrypt import CRC32 from .core.crypto import CryptoProcessor -from .utils.random_gen import RandomGenerator from .utils.generate_fingerprint import XhsFpGenerator +from .utils.random_gen import RandomGenerator from .utils.url_utils import build_url, extract_uri from .utils.validators import ( validate_get_signature_params, @@ -163,17 +163,17 @@ def sign_xs_common( cookie_dict = {k: morsel.value for k, morsel in ck.items()} gfp = XhsFpGenerator(self.config) - a1_value = cookie_dict['a1'] + a1_value = cookie_dict["a1"] fingerprint = gfp.get_fingerprint(cookies=cookie_dict, user_agent=self.crypto_processor.config.PUBLIC_USERAGENT) b1 = gfp.generate_b1(fingerprint) x9 = CRC32.crc32_js_int(b1) sign_struct = CryptoConfig.SIGNATURE_XSCOMMON_TEMPLATE - sign_struct['x5'] = a1_value - sign_struct['x8'] = b1 - sign_struct['x9'] = x9 + sign_struct["x5"] = a1_value + sign_struct["x8"] = b1 + sign_struct["x9"] = x9 sign_json = json.dumps(sign_struct, separators=(",", ":"), ensure_ascii=False) - sign_list = list(sign_json.encode('utf-8')) + sign_list = list(sign_json.encode("utf-8")) xs_common = self.crypto_processor.b64encoder.custom_to_b64(sign_list) return xs_common diff --git a/src/xhshow/config/config.py b/src/xhshow/config/config.py index 4686c93..ded396e 100644 --- a/src/xhshow/config/config.py +++ b/src/xhshow/config/config.py @@ -7,13 +7,14 @@ @dataclass(frozen=True) class CryptoConfig: """Configuration constants for cryptographic operations""" + # Gid encrypt parameters DES_KEY = "zbp30y86" GID_URL = "https://as.xiaohongshu.com/api/sec/v1/shield/webprofile" DATA_PALTFORM = "Windows" DATA_SVN = "2" DATA_SDK_VERSION = "4.2.6" - DATA_webBuild = '5.0.3' + DATA_webBuild = "5.0.3" # Bitwise operation constants MAX_32BIT: int = 0xFFFFFFFF @@ -95,12 +96,14 @@ class CryptoConfig: "x8": "", "x9": -596800761, "x10": 0, - "x11": "normal" + "x11": "normal", } ) - PUBLIC_USERAGENT: str = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0") + PUBLIC_USERAGENT: str = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0" + ) def with_overrides(self, **kwargs: Any) -> "CryptoConfig": """ diff --git a/src/xhshow/core/crc32_encrypt.py b/src/xhshow/core/crc32_encrypt.py index 7adb4b7..aaba3a4 100644 --- a/src/xhshow/core/crc32_encrypt.py +++ b/src/xhshow/core/crc32_encrypt.py @@ -12,10 +12,10 @@ """ from __future__ import annotations -from typing import Iterable, Union +from collections.abc import Iterable -DataLike = Union[str, bytes, bytearray, memoryview, Iterable[int]] +DataLike = str | bytes | bytearray | memoryview | Iterable[int] __all__ = ["CRC32"] @@ -63,7 +63,7 @@ def _crc32_core(cls, data: DataLike, *, string_mode: str = "js") -> int: c = cls.MASK32 - if isinstance(data, (bytes, bytearray, memoryview)): + if isinstance(data, bytes | bytearray | memoryview): it = bytes(data) elif isinstance(data, str): if string_mode.lower() == "utf8": diff --git a/src/xhshow/utils/encoder.py b/src/xhshow/utils/encoder.py index 2ae4b26..a06c5d5 100644 --- a/src/xhshow/utils/encoder.py +++ b/src/xhshow/utils/encoder.py @@ -2,8 +2,8 @@ import base64 import binascii +from collections.abc import Iterable -from typing import Iterable from ..config import CryptoConfig __all__ = ["Base64Encoder"] @@ -26,15 +26,13 @@ def encode_to_b64(self, data_to_encode: str | bytes, alphabet: str = CryptoConfi """ if isinstance(data_to_encode, str): b = data_to_encode.encode("utf-8") - elif isinstance(data_to_encode, (bytes, bytearray, memoryview)): + elif isinstance(data_to_encode, bytes | bytearray | memoryview): b = bytes(data_to_encode) else: try: - b = bytes((int(x) & 0xFF for x in data_to_encode)) # type: ignore[arg-type] + b = bytes(int(x) & 0xFF for x in data_to_encode) # type: ignore[arg-type] except TypeError as e: - raise TypeError( - f"unsupported type: {type(data_to_encode)} (expected bytes/str/Iterable[int])" - ) from e + raise TypeError(f"unsupported type: {type(data_to_encode)} (expected bytes/str/Iterable[int])") from e n = len(b) rem = n % 3 @@ -62,12 +60,7 @@ def encode_to_b64(self, data_to_encode: str | bytes, alphabet: str = CryptoConfi out_parts.append(alphabet[e >> 2] + alphabet[(e << 4) & 63] + "==") elif rem == 2: e = (b[-2] << 8) | b[-1] - out_parts.append( - alphabet[(e >> 10) & 63] - + alphabet[(e >> 4) & 63] - + alphabet[(e << 2) & 63] - + "=" - ) + out_parts.append(alphabet[(e >> 10) & 63] + alphabet[(e >> 4) & 63] + alphabet[(e << 2) & 63] + "=") return "".join(out_parts) @@ -100,16 +93,14 @@ def custom_to_b64(data: bytes | str | Iterable[int]) -> str: # —— all datas convert to bytes —— # if isinstance(data, str): b = data.encode("utf-8") - elif isinstance(data, (bytes, bytearray, memoryview)): + elif isinstance(data, bytes | bytearray | memoryview): b = bytes(data) else: try: # allow list/tuple/any could be iterable int, auto & 0xFF auto filter the value not between 0..255 - b = bytes((int(x) & 0xFF for x in data)) # type: ignore[arg-type] + b = bytes(int(x) & 0xFF for x in data) # type: ignore[arg-type] except TypeError as e: - raise TypeError( - f"unsupported type: {type(data)} (expected bytes/str/Iterable[int])" - ) from e + raise TypeError(f"unsupported type: {type(data)} (expected bytes/str/Iterable[int])") from e n = len(b) rem = n % 3 @@ -137,12 +128,7 @@ def custom_to_b64(data: bytes | str | Iterable[int]) -> str: out_parts.append(alphabet[e >> 2] + alphabet[(e << 4) & 63] + "==") elif rem == 2: e = (b[-2] << 8) | b[-1] - out_parts.append( - alphabet[(e >> 10) & 63] - + alphabet[(e >> 4) & 63] - + alphabet[(e << 2) & 63] - + "=" - ) + out_parts.append(alphabet[(e >> 10) & 63] + alphabet[(e >> 4) & 63] + alphabet[(e << 2) & 63] + "=") return "".join(out_parts) diff --git a/src/xhshow/utils/generate_fingerprint.py b/src/xhshow/utils/generate_fingerprint.py index e3cb351..6a7e44e 100644 --- a/src/xhshow/utils/generate_fingerprint.py +++ b/src/xhshow/utils/generate_fingerprint.py @@ -5,20 +5,22 @@ 1. user_agent 2. cookie info 3. timeZone need same current request location, just be care concurrent with proxy addr 4. encrypt code version 5.whether browser has been tampered picture hash value 6. some others detect(hook, screen) """ -from http.cookies import SimpleCookie -from src.xhshow.config import CryptoConfig -from src.xhshow.utils import encoder -from Crypto.Cipher import ARC4 -import urllib.parse import hashlib -import secrets -import random import json +import random +import secrets import time +import urllib.parse +from typing import Any +from Crypto.Cipher import ARC4 -class XhsFpGenerator(object): +from ..config import CryptoConfig +from . import encoder + + +class XhsFpGenerator: """XHS Fingerprint generate function""" def __init__(self, config: CryptoConfig): @@ -26,7 +28,7 @@ def __init__(self, config: CryptoConfig): self.__b1_key = self.config.B1_SECRET_KEY.encode() @staticmethod - def __weighted_random_choice(options: list, weights: list) -> any: + def __weighted_random_choice(options: list, weights: list) -> Any: """ Random choice a value from list according to the given weights Argument: @@ -45,16 +47,16 @@ def __get_renderer_info() -> list: "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 550 (0x00001512) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 6000 (0x1606) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 540 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 550 (0x1913) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640 (0x161C) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 540 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 550 (0x1913) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640 (0x161C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 600 (0x3E80) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 655 (0x00009BC8) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe MAX Graphics (0x00009BC0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe MAX Graphics (0x00009BC0) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A370M (0x0000AF51) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380 (0x0000AF41) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380M (0x0000AF5E) Direct3D11 vs_5_0 ps_5_0, D3D11)", @@ -67,14 +69,13 @@ def __get_renderer_info() -> list: "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (ADL‑S GT1) (0x0000A0A1) OpenGL 4.6)", "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RKL GT1) (0x0000A9A1) OpenGL 4.6)", "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics (CML GT2) (0x00009A14) OpenGL 4.6)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00001022) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics Family (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00001022) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics Family (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris Pro OpenGL Engine, OpenGL 4.1)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 645 (0x1616) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 655 (0x161E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 645 (0x1616) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 655 (0x161E) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 730 (0x0000A100) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 805 (0x0000B0A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 3 Graphics (0x000015E0) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 8 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 11 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", @@ -95,33 +96,31 @@ def __get_renderer_info() -> list: "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5500 XT OpenGL Engine, OpenGL 4.1)", "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon R7 370 Series (0x00006811) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (AMD)|ANGLE (AMD, ATI Technologies Inc. AMD Radeon RX Vega 64 OpenGL Engine, OpenGL 4.1)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 (0x00001C81) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti (0x00001C8C) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 6GB (0x000010DE) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti (0x00001C8C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 6GB (0x000010DE) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1070 (0x00001B81) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 (0x00001B80) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 SUPER (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 SUPER (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 SUPER (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 SUPER (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x0000250F) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Ti (0x00002489) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Ti (0x00002489) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002488) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Ti (0x000028A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Ti (0x000028A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 (0x00002206) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Ti (0x00002208) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Ti (0x00002208) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3090 (0x00002204) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 (0x00002882) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti (0x00002803) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti (0x00002803) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Ti (0x00002857) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Ti (0x00002857) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4080 (0x00002819) Direct3D11 vs_5_0 ps_5_0, D3D11)", "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 (0x00002684) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro RTX 5000 Ada Generation (0x000026B2) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro RTX 5000 Ada Generation (0x000026B2) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro P400 (0x00001CB3) Direct3D11 vs_5_0 ps_5_0, D3D11)", - - "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)", + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)", # noqa: E501 "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)", "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device), SwiftShader driver)", ] @@ -132,15 +131,18 @@ def __get_renderer_info() -> list: def __get_width_and_height(): width, height = XhsFpGenerator.__weighted_random_choice( ["1366;768", "1600;900", "1920;1080", "2560;1440", "3840;2160", "7680;4320"], - [0.25, 0.15, 0.35, 0.15, 0.08, 0.02]).split(';') + [0.25, 0.15, 0.35, 0.15, 0.08, 0.02], + ).split(";") if random.choice([True, False]): availWidth = int(width) - int( - XhsFpGenerator.__weighted_random_choice([0, 30, 60, 80], [0.1, 0.4, 0.3, 0.2])) + XhsFpGenerator.__weighted_random_choice([0, 30, 60, 80], [0.1, 0.4, 0.3, 0.2]) + ) availHeight = height else: availWidth = width availHeight = int(height) - int( - XhsFpGenerator.__weighted_random_choice([30, 60, 80, 100], [0.2, 0.5, 0.2, 0.1])) + XhsFpGenerator.__weighted_random_choice([30, 60, 80, 100], [0.2, 0.5, 0.2, 0.1]) + ) return width, height, availWidth, availHeight @@ -148,33 +150,33 @@ def generate_b1(self, fp): config = CryptoConfig() b64_encoder = encoder.Base64Encoder(config) b1_fp = { - "x33": fp['x33'], - "x34": fp['x34'], - "x35": fp['x35'], - "x36": fp['x36'], - "x37": fp['x37'], - "x38": fp['x38'], - "x39": fp['x39'], - "x42": fp['x42'], - "x43": fp['x43'], - "x44": fp['x44'], - "x45": fp['x45'], - "x46": fp['x46'], - "x48": fp['x48'], - "x49": fp['x49'], - "x50": fp['x50'], - "x51": fp['x51'], - "x52": fp['x52'], - "x82": fp['x82'], + "x33": fp["x33"], + "x34": fp["x34"], + "x35": fp["x35"], + "x36": fp["x36"], + "x37": fp["x37"], + "x38": fp["x38"], + "x39": fp["x39"], + "x42": fp["x42"], + "x43": fp["x43"], + "x44": fp["x44"], + "x45": fp["x45"], + "x46": fp["x46"], + "x48": fp["x48"], + "x49": fp["x49"], + "x50": fp["x50"], + "x51": fp["x51"], + "x52": fp["x52"], + "x82": fp["x82"], } - b1_json = json.dumps(b1_fp, separators=(',', ':'), ensure_ascii=False) + b1_json = json.dumps(b1_fp, separators=(",", ":"), ensure_ascii=False) cipher = ARC4.new(self.__b1_key) - ciphertext = cipher.encrypt(b1_json.encode('utf-8')).decode('latin1') + ciphertext = cipher.encrypt(b1_json.encode("utf-8")).decode("latin1") encoded_url = urllib.parse.quote(ciphertext, safe="!*'()~_-") b = [] - for c in encoded_url.split('%')[1:]: + for c in encoded_url.split("%")[1:]: chars = list(c) - b.append(int(''.join(chars[:2]), 16)) + b.append(int("".join(chars[:2]), 16)) [b.append(ord(j)) for j in chars[2:]] b1 = b64_encoder.custom_to_b64(bytearray(b)) @@ -187,7 +189,7 @@ def get_fingerprint(cookies: dict, user_agent: str) -> dict: width, height, availWidth, availHeight = XhsFpGenerator.__get_width_and_height() - is_incognito_mode = XhsFpGenerator.__weighted_random_choice(['true', 'false'], [0.95, 0.05]) + is_incognito_mode = XhsFpGenerator.__weighted_random_choice(["true", "false"], [0.95, 0.05]) vendor, renderer = XhsFpGenerator.__get_renderer_info() @@ -202,12 +204,13 @@ def get_fingerprint(cookies: dict, user_agent: str) -> dict: # navigator.deviceMemory "x6": "24", # screen.pixelDepth "x7": f"{vendor},{renderer}", - "x8": XhsFpGenerator.__weighted_random_choice([2, 4, 6, 8, 12, 16, 24, 32], - [0.1, 0.4, 0.2, 0.15, 0.08, 0.04, 0.02, 0.01]), + "x8": XhsFpGenerator.__weighted_random_choice( + [2, 4, 6, 8, 12, 16, 24, 32], [0.1, 0.4, 0.2, 0.15, 0.08, 0.04, 0.02, 0.01] + ), "x9": f"{width};{height}", "x10": f"{availWidth};{availHeight}", - "x11": "-480", # new Date().getTimezoneOffset()。 - "x12": "Asia/Shanghai", # Intl.DateTimeFormat().resolvedOptions().timeZone default timezone + "x11": "-480", # new Date().getTimezoneOffset()。 + "x12": "Asia/Shanghai", # Intl.DateTimeFormat().resolvedOptions().timeZone default timezone "x13": is_incognito_mode, # window.sessionStorage detect "x14": is_incognito_mode, # window.localStorage detect "x15": is_incognito_mode, # window.indexedDB detect @@ -228,19 +231,18 @@ def get_fingerprint(cookies: dict, user_agent: str) -> dict: "x29": "4,7,8", "x30": "swf object not loaded", # detect Flash(SWF) Boolean(navigator.plugins['Shockwave Flash']); # "x32": "0", # haven't used - "x33": "0", # whether in WeChat browser constant - "x34": "0", # whether js paint tool is Brian Paul constant - "x35": "0", # whether did have loaded Modernizr constant - "x36": f"{random.randint(1, 20)}", # window.history.length history stack length + "x33": "0", # whether in WeChat browser constant + "x34": "0", # whether js paint tool is Brian Paul constant + "x35": "0", # whether did have loaded Modernizr constant + "x36": f"{random.randint(1, 20)}", # window.history.length history stack length "x37": "0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|1|0|0|0|0|0", "x38": "0|0|1|0|1|0|0|0|0|0|1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0", - "x39": 0, # localStorage.getItem('sc'); plus one by every fresh page - "x40": "0", # localStorage.getItem('ptt'); - "x41": "0", # localStorage.getItem('pst'); - "x42": "3.4.4", # Fingerprint.js version // constant - "x43": "742cc32c", # Detecting whether a browser has been tampered with by comparing - # the hash value of an image - + "x39": 0, # localStorage.getItem('sc'); plus one by every fresh page + "x40": "0", # localStorage.getItem('ptt'); + "x41": "0", # localStorage.getItem('pst'); + "x42": "3.4.4", # Fingerprint.js version // constant + "x43": "742cc32c", # Detecting whether a browser has been tampered with by comparing + # the hash value of an image "x44": f"{int(time.time() * 1000)}", # current timestamp "x45": "__SEC_CAV__1-1-1-1-1|__SEC_WSA__|", # risk control SDK information if risk just show like __SEC_WSA__| @@ -249,40 +251,38 @@ def get_fingerprint(cookies: dict, user_agent: str) -> dict: # Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver'); // true → risk control "x47": "1|0|0|0|0|0", # recognize different browser「unique feature」 // constant "x48": "", - "x49": '{list:[],type:}', + "x49": "{list:[],type:}", "x50": "", "x51": "", "x52": "", "x55": "380,380,360,400,380,400,420,380,400,400,360,360,440,420", "x56": f"{vendor}|{renderer}|{hashlib.md5(secrets.token_bytes(32)).hexdigest()}|35", - # x7 | Fingerprint2.x64hash128(WebGLRenderingContext.getSupportedExtensions()) | WebGLRenderingContext.getSupportedExtensions().length + # x7 | Fingerprint2.x64hash128(WebGLRenderingContext.getSupportedExtensions()) | WebGLRenderingContext.getSupportedExtensions().length # noqa: E501 "x57": cookie_string, - "x58": "180", # document.getElementsByTagName('div') // count div (just above 177, not strict) constant - "x59": "2", # performance.getEntriesByType("resource").length - "x60": "63", # risk control score // constant + "x58": "180", # document.getElementsByTagName('div') // count div (just above 177, not strict) constant + "x59": "2", # performance.getEntriesByType("resource").length + "x60": "63", # risk control score // constant "x61": "1291", # Object.getOwnPropertyNames(window) .length // window object amount "x62": "2047", # HOOK detect 1,1,1,1,1,1,1,1,1,1,1 11个1(通过) 组成二进制2047 constant - "x63": "0", # JS VMP file \n detect // constant - "x64": "0", # HOOK ToString number detect + "x63": "0", # JS VMP file \n detect // constant + "x64": "0", # HOOK ToString number detect "x65": "0", - "x66": { # navigator.userAgent + "x66": { # navigator.userAgent "referer": "", "location": "https://www.xiaohongshu.com/explore", - "frame": 0 + "frame": 0, }, "x67": "1|0", "x68": "0", "x69": "326|1292|30", - "x70": [ - "location" - ], + "x70": ["location"], "x71": "true", "x72": "complete", "x73": "1191", "x74": "0|0|0", - "x75": "Google Inc.", # Navigator.vendor - "x76": "true", # navigator.cookieEnabled - "x77": "1|1|1|1|1|1|1|1|1|1", # constant + "x75": "Google Inc.", # Navigator.vendor + "x76": "true", # navigator.cookieEnabled + "x77": "1|1|1|1|1|1|1|1|1|1", # constant "x78": { "x": 0, "y": x78_y, @@ -292,7 +292,7 @@ def get_fingerprint(cookies: dict, user_agent: str) -> dict: "height": 18, "top": x78_y, "width": 290.828125, - "font": "system-ui, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\", -apple-system, \"Segoe UI\", Roboto, Ubuntu, Cantarell, \"Noto Sans\", sans-serif, BlinkMacSystemFont, \"Helvetica Neue\", Arial, \"PingFang SC\", \"PingFang TC\", \"PingFang HK\", \"Microsoft Yahei\", \"Microsoft JhengHei\"" + "font": 'system-ui, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, BlinkMacSystemFont, "Helvetica Neue", Arial, "PingFang SC", "PingFang TC", "PingFang HK", "Microsoft Yahei", "Microsoft JhengHei"', # noqa: E501 }, "x82": "_0x17a2|_0x1954", # get iframe.contentWindow | contentWindow.window "x31": "124.04347527516074", @@ -311,16 +311,18 @@ def get_fingerprint(cookies: dict, user_agent: str) -> dict: def update_fingerprint(fp: dict, cookies: dict, url: str) -> None: cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) - fp.update({ - "x39": 0, # localStorage.getItem('p1'); Add the value with every request +1 - "x44": f"{time.time() * 1000}", # current timestamp multiply 1000 - "x57": cookie_string, - "x66": { # navigator.userAgent - "referer": "https://www.xiaohongshu.com/explore", - "location": url, - "frame": 0 + fp.update( + { + "x39": 0, # localStorage.getItem('p1'); Add the value with every request +1 + "x44": f"{time.time() * 1000}", # current timestamp multiply 1000 + "x57": cookie_string, + "x66": { # navigator.userAgent + "referer": "https://www.xiaohongshu.com/explore", + "location": url, + "frame": 0, + }, } - }) + ) # test From 1cf90f80d89374d2428deb2025c3225b99fdb131 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 01:52:21 +0800 Subject: [PATCH 12/19] refactor(core): split fingerprint generation into separate modules --- src/__init__.py | 0 src/xhshow/core/common_sign.py | 49 +++ src/xhshow/data/__init__.py | 25 ++ src/xhshow/data/fingerprint_data.py | 133 +++++++ src/xhshow/generators/__init__.py | 5 + src/xhshow/generators/fingerprint.py | 217 ++++++++++++ src/xhshow/generators/fingerprint_helpers.py | 92 +++++ src/xhshow/utils/generate_fingerprint.py | 353 ------------------- 8 files changed, 521 insertions(+), 353 deletions(-) delete mode 100644 src/__init__.py create mode 100644 src/xhshow/core/common_sign.py create mode 100644 src/xhshow/data/__init__.py create mode 100644 src/xhshow/data/fingerprint_data.py create mode 100644 src/xhshow/generators/__init__.py create mode 100644 src/xhshow/generators/fingerprint.py create mode 100644 src/xhshow/generators/fingerprint_helpers.py delete mode 100644 src/xhshow/utils/generate_fingerprint.py diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/xhshow/core/common_sign.py b/src/xhshow/core/common_sign.py new file mode 100644 index 0000000..2787a09 --- /dev/null +++ b/src/xhshow/core/common_sign.py @@ -0,0 +1,49 @@ +"""x-s-common signature generation""" + +import json +from typing import Any + +from ..config import CryptoConfig +from ..core.crc32_encrypt import CRC32 +from ..generators.fingerprint import FingerprintGenerator +from ..utils.encoder import Base64Encoder + +__all__ = ["XsCommonSigner"] + + +class XsCommonSigner: + """Generate x-s-common signatures""" + + def __init__(self, config: CryptoConfig | None = None): + self.config = config or CryptoConfig() + self._fp_generator = FingerprintGenerator(self.config) + self._encoder = Base64Encoder(self.config) + + def sign(self, cookie_dict: dict[str, Any]) -> str: + """ + Generate x-s-common signature + + Args: + cookie_dict: Cookie dictionary (must be dict, not string) + + Returns: + x-s-common signature string + + Raises: + KeyError: If 'a1' cookie is missing + """ + a1_value = cookie_dict["a1"] + fingerprint = self._fp_generator.generate(cookies=cookie_dict, user_agent=self.config.PUBLIC_USERAGENT) + b1 = self._fp_generator.generate_b1(fingerprint) + + x9 = CRC32.crc32_js_int(b1) + + sign_struct = dict(self.config.SIGNATURE_XSCOMMON_TEMPLATE) + sign_struct["x5"] = a1_value + sign_struct["x8"] = b1 + sign_struct["x9"] = x9 + + sign_json = json.dumps(sign_struct, separators=(",", ":"), ensure_ascii=False) + xs_common = self._encoder.encode(sign_json) + + return xs_common diff --git a/src/xhshow/data/__init__.py b/src/xhshow/data/__init__.py new file mode 100644 index 0000000..9dd696c --- /dev/null +++ b/src/xhshow/data/__init__.py @@ -0,0 +1,25 @@ +"""Data constants module""" + +from .fingerprint_data import ( + BROWSER_PLUGINS, + CANVAS_HASH, + COLOR_DEPTH_OPTIONS, + CORE_OPTIONS, + DEVICE_MEMORY_OPTIONS, + FONTS, + GPU_VENDORS, + SCREEN_RESOLUTIONS, + VOICE_HASH_OPTIONS, +) + +__all__ = [ + "GPU_VENDORS", + "SCREEN_RESOLUTIONS", + "COLOR_DEPTH_OPTIONS", + "DEVICE_MEMORY_OPTIONS", + "CORE_OPTIONS", + "BROWSER_PLUGINS", + "CANVAS_HASH", + "VOICE_HASH_OPTIONS", + "FONTS", +] diff --git a/src/xhshow/data/fingerprint_data.py b/src/xhshow/data/fingerprint_data.py new file mode 100644 index 0000000..a3197ed --- /dev/null +++ b/src/xhshow/data/fingerprint_data.py @@ -0,0 +1,133 @@ +"""Browser fingerprint data constants""" + +from typing import Final + +__all__ = [ + "GPU_VENDORS", + "SCREEN_RESOLUTIONS", + "COLOR_DEPTH_OPTIONS", + "DEVICE_MEMORY_OPTIONS", + "CORE_OPTIONS", + "BROWSER_PLUGINS", + "CANVAS_HASH", + "VOICE_HASH_OPTIONS", + "FONTS", +] + +GPU_VENDORS: Final[list[str]] = [ + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 400 (0x00000166) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4400 (0x00001112) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4600 (0x00000412) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 520 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 550 (0x00001512) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 6000 (0x1606) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 540 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 550 (0x1913) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640 (0x161C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 600 (0x3E80) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 655 (0x00009BC8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe MAX Graphics (0x00009BC0) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A370M (0x0000AF51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380 (0x0000AF41) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380M (0x0000AF5E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A550 (0x0000AF42) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770 (0x0000AF43) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770M (0x0000AF50) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RPL‑P GT1) (0x0000A702) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics 770 (0x00004680) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) HD Graphics 4400 (0x00001122) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (ADL‑S GT1) (0x0000A0A1) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RKL GT1) (0x0000A9A1) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics (CML GT2) (0x00009A14) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00001022) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics Family (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris Pro OpenGL Engine, OpenGL 4.1)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 645 (0x1616) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 655 (0x161E) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 730 (0x0000A100) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 805 (0x0000B0A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 3 Graphics (0x000015E0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 8 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 11 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Graphics (0x00001636) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 5500 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 560 (0x000067EF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 570 (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 580 2048SP (0x00006FDF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 590 (0x000067FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 XT (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6650 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6700 XT (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6800 (0x000073BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6900 XT (0x000073C2) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 7700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5300M OpenGL Engine, OpenGL 4.1)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5500 XT OpenGL Engine, OpenGL 4.1)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon R7 370 Series (0x00006811) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, ATI Technologies Inc. AMD Radeon RX Vega 64 OpenGL Engine, OpenGL 4.1)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 (0x00001C81) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti (0x00001C8C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 6GB (0x000010DE) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1070 (0x00001B81) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 (0x00001B80) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 SUPER (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 SUPER (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x0000250F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Ti (0x00002489) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002488) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Ti (0x000028A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 (0x00002206) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Ti (0x00002208) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3090 (0x00002204) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 (0x00002882) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti (0x00002803) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Ti (0x00002857) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4080 (0x00002819) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 (0x00002684) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro RTX 5000 Ada Generation (0x000026B2) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro P400 (0x00001CB3) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)", # noqa: E501 + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)", + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device), SwiftShader driver)", +] + +SCREEN_RESOLUTIONS: Final[dict[str, list]] = { + "resolutions": ["1366;768", "1600;900", "1920;1080", "2560;1440", "3840;2160", "7680;4320"], + "weights": [0.25, 0.15, 0.35, 0.15, 0.08, 0.02], +} + +COLOR_DEPTH_OPTIONS: Final[dict[str, list]] = { + "values": [16, 24, 30, 32], + "weights": [0.05, 0.6, 0.05, 0.3], +} + +DEVICE_MEMORY_OPTIONS: Final[dict[str, list]] = { + "values": [1, 2, 4, 8, 12, 16], + "weights": [0.10, 0.25, 0.4, 0.2, 0.03, 0.01], +} + +CORE_OPTIONS: Final[dict[str, list]] = { + "values": [2, 4, 6, 8, 12, 16, 24, 32], + "weights": [0.1, 0.4, 0.2, 0.15, 0.08, 0.04, 0.02, 0.01], +} + +BROWSER_PLUGINS: Final[str] = ( + "PDF Viewer,Chrome PDF Viewer,Chromium PDF Viewer,Microsoft Edge PDF Viewer,WebKit built-in PDF" +) + +CANVAS_HASH: Final[str] = "742cc32c" + +VOICE_HASH_OPTIONS: Final[str] = "10311144241322244122" + +FONTS: Final[str] = ( + 'system-ui, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, BlinkMacSystemFont, "Helvetica Neue", Arial, "PingFang SC", "PingFang TC", "PingFang HK", "Microsoft Yahei", "Microsoft JhengHei"' # noqa: E501 +) diff --git a/src/xhshow/generators/__init__.py b/src/xhshow/generators/__init__.py new file mode 100644 index 0000000..8f8e2ee --- /dev/null +++ b/src/xhshow/generators/__init__.py @@ -0,0 +1,5 @@ +"""Generators module""" + +from .fingerprint import FingerprintGenerator + +__all__ = ["FingerprintGenerator"] diff --git a/src/xhshow/generators/fingerprint.py b/src/xhshow/generators/fingerprint.py new file mode 100644 index 0000000..db2b967 --- /dev/null +++ b/src/xhshow/generators/fingerprint.py @@ -0,0 +1,217 @@ +"""Browser fingerprint generator""" + +import hashlib +import json +import random +import secrets +import time +import urllib.parse + +from Crypto.Cipher import ARC4 + +from ..config import CryptoConfig +from ..data import fingerprint_data as FPData +from ..utils import encoder +from . import fingerprint_helpers as helpers + +__all__ = ["FingerprintGenerator"] + + +class FingerprintGenerator: + """XHS Fingerprint generation function""" + + def __init__(self, config: CryptoConfig): + self.config = config + self._b1_key = self.config.B1_SECRET_KEY.encode() + self._encoder = encoder.Base64Encoder(self.config) + + def generate_b1(self, fp: dict) -> str: + """ + Generate b1 parameter from fingerprint + + Args: + fp: Fingerprint dictionary + + Returns: + Base64 encoded b1 string + """ + b1_fp = { + "x33": fp["x33"], + "x34": fp["x34"], + "x35": fp["x35"], + "x36": fp["x36"], + "x37": fp["x37"], + "x38": fp["x38"], + "x39": fp["x39"], + "x42": fp["x42"], + "x43": fp["x43"], + "x44": fp["x44"], + "x45": fp["x45"], + "x46": fp["x46"], + "x48": fp["x48"], + "x49": fp["x49"], + "x50": fp["x50"], + "x51": fp["x51"], + "x52": fp["x52"], + "x82": fp["x82"], + } + b1_json = json.dumps(b1_fp, separators=(",", ":"), ensure_ascii=False) + cipher = ARC4.new(self._b1_key) + ciphertext = cipher.encrypt(b1_json.encode("utf-8")).decode("latin1") + encoded_url = urllib.parse.quote(ciphertext, safe="!*'()~_-") + b = [] + for c in encoded_url.split("%")[1:]: + chars = list(c) + b.append(int("".join(chars[:2]), 16)) + [b.append(ord(j)) for j in chars[2:]] + + b1 = self._encoder.encode(json.dumps(b, separators=(",", ":"))) + + return b1 + + def generate(self, cookies: dict, user_agent: str) -> dict: + """ + Generate browser fingerprint + + Args: + cookies: Cookie dictionary + user_agent: User agent string + + Returns: + Complete fingerprint dictionary + """ + cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) + + screen_config = helpers.get_screen_config() + is_incognito_mode = helpers.weighted_random_choice(["true", "false"], [0.95, 0.05]) + vendor, renderer = helpers.get_renderer_info() + + x78_y = random.randint(2350, 2450) + fp = { + "x1": user_agent, + "x2": "false", + "x3": "zh-CN", + "x4": helpers.weighted_random_choice( + FPData.COLOR_DEPTH_OPTIONS["values"], + FPData.COLOR_DEPTH_OPTIONS["weights"], + ), + "x5": helpers.weighted_random_choice( + FPData.DEVICE_MEMORY_OPTIONS["values"], + FPData.DEVICE_MEMORY_OPTIONS["weights"], + ), + "x6": "24", + "x7": f"{vendor},{renderer}", + "x8": helpers.weighted_random_choice(FPData.CORE_OPTIONS["values"], FPData.CORE_OPTIONS["weights"]), + "x9": f"{screen_config['width']};{screen_config['height']}", + "x10": f"{screen_config['availWidth']};{screen_config['availHeight']}", + "x11": "-480", + "x12": "Asia/Shanghai", + "x13": is_incognito_mode, + "x14": is_incognito_mode, + "x15": is_incognito_mode, + "x16": "false", + "x17": "false", + "x18": "un", + "x19": "Win32", + "x20": "", + "x21": FPData.BROWSER_PLUGINS, + "x22": helpers.generate_webgl_hash(), + "x23": "false", + "x24": "false", + "x25": "false", + "x26": "false", + "x27": "false", + "x28": "0,false,false", + "x29": "4,7,8", + "x30": "swf object not loaded", + "x33": "0", + "x34": "0", + "x35": "0", + "x36": f"{random.randint(1, 20)}", + "x37": "0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|1|0|0|0|0|0", + "x38": "0|0|1|0|1|0|0|0|0|0|1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0", + "x39": 0, + "x40": "0", + "x41": "0", + "x42": "3.4.4", + "x43": helpers.generate_canvas_hash(), + "x44": f"{int(time.time() * 1000)}", + "x45": "__SEC_CAV__1-1-1-1-1|__SEC_WSA__|", + "x46": "false", + "x47": "1|0|0|0|0|0", + "x48": "", + "x49": "{list:[],type:}", + "x50": "", + "x51": "", + "x52": "", + "x55": "380,380,360,400,380,400,420,380,400,400,360,360,440,420", + "x56": f"{vendor}|{renderer}|{helpers.generate_webgl_hash()}|35", + "x57": cookie_string, + "x58": "180", + "x59": "2", + "x60": "63", + "x61": "1291", + "x62": "2047", + "x63": "0", + "x64": "0", + "x65": "0", + "x66": { + "referer": "", + "location": "https://www.xiaohongshu.com/explore", + "frame": 0, + }, + "x67": "1|0", + "x68": "0", + "x69": "326|1292|30", + "x70": ["location"], + "x71": "true", + "x72": "complete", + "x73": "1191", + "x74": "0|0|0", + "x75": "Google Inc.", + "x76": "true", + "x77": "1|1|1|1|1|1|1|1|1|1", + "x78": { + "x": 0, + "y": x78_y, + "left": 0, + "right": 290.828125, + "bottom": x78_y + 18, + "height": 18, + "top": x78_y, + "width": 290.828125, + "font": FPData.FONTS, + }, + "x82": "_0x17a2|_0x1954", + "x31": "124.04347527516074", + "x79": "144|599565058866", + "x53": hashlib.md5(secrets.token_bytes(32)).hexdigest(), + "x54": FPData.VOICE_HASH_OPTIONS, + "x80": "1|[object FileSystemDirectoryHandle]", + } + + return fp + + def update(self, fp: dict, cookies: dict, url: str) -> None: + """ + Update fingerprint with new cookies and URL + + Args: + fp: Fingerprint dictionary to update + cookies: Updated cookie dictionary + url: Current URL + """ + cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) + + fp.update( + { + "x39": 0, + "x44": f"{time.time() * 1000}", + "x57": cookie_string, + "x66": { + "referer": "https://www.xiaohongshu.com/explore", + "location": url, + "frame": 0, + }, + } + ) diff --git a/src/xhshow/generators/fingerprint_helpers.py b/src/xhshow/generators/fingerprint_helpers.py new file mode 100644 index 0000000..b8fede4 --- /dev/null +++ b/src/xhshow/generators/fingerprint_helpers.py @@ -0,0 +1,92 @@ +"""Fingerprint generation helper functions""" + +import hashlib +import random +import secrets +from typing import Any + +from ..data import fingerprint_data as FPData + +__all__ = [ + "weighted_random_choice", + "get_renderer_info", + "get_screen_config", + "generate_canvas_hash", + "generate_webgl_hash", +] + + +def weighted_random_choice(options: list, weights: list) -> Any: + """ + Random choice a value from list according to the given weights + + Args: + options: Option list + weights: Weight list mapping the option list (without normalization) + + Returns: + Randomly chosen value from options + """ + return f"{random.choices(options, weights=weights, k=1)[0]}" + + +def get_renderer_info() -> tuple[str, str]: + """ + Get random GPU renderer information + + Returns: + Tuple of (vendor, renderer) + """ + renderer_str = random.choice(FPData.GPU_VENDORS) + vendor, renderer = renderer_str.split("|") + return vendor, renderer + + +def get_screen_config() -> dict[str, Any]: + """ + Get random screen configuration with width, height, and available dimensions + + Returns: + Dictionary containing screen configuration + """ + width_str, height_str = weighted_random_choice( + FPData.SCREEN_RESOLUTIONS["resolutions"], + FPData.SCREEN_RESOLUTIONS["weights"], + ).split(";") + + width = int(width_str) + height = int(height_str) + + if random.choice([True, False]): + avail_width = width - int(weighted_random_choice([0, 30, 60, 80], [0.1, 0.4, 0.3, 0.2])) + avail_height = height + else: + avail_width = width + avail_height = height - int(weighted_random_choice([30, 60, 80, 100], [0.2, 0.5, 0.2, 0.1])) + + return { + "width": width, + "height": height, + "availWidth": avail_width, + "availHeight": avail_height, + } + + +def generate_canvas_hash() -> str: + """ + Generate canvas fingerprint hash + + Returns: + Canvas hash string + """ + return FPData.CANVAS_HASH + + +def generate_webgl_hash() -> str: + """ + Generate WebGL fingerprint hash + + Returns: + WebGL hash (MD5 hex string) + """ + return hashlib.md5(secrets.token_bytes(32)).hexdigest() diff --git a/src/xhshow/utils/generate_fingerprint.py b/src/xhshow/utils/generate_fingerprint.py deleted file mode 100644 index 6a7e44e..0000000 --- a/src/xhshow/utils/generate_fingerprint.py +++ /dev/null @@ -1,353 +0,0 @@ -""" -Created on Wed Nov 5 2025 AM -@author: -Describe: generate fingerprint payload for encrypt x-s-common params, that current FP include much important information -1. user_agent 2. cookie info 3. timeZone need same current request location, just be care concurrent with proxy addr -4. encrypt code version 5.whether browser has been tampered picture hash value 6. some others detect(hook, screen) -""" - -import hashlib -import json -import random -import secrets -import time -import urllib.parse -from typing import Any - -from Crypto.Cipher import ARC4 - -from ..config import CryptoConfig -from . import encoder - - -class XhsFpGenerator: - """XHS Fingerprint generate function""" - - def __init__(self, config: CryptoConfig): - self.config = config - self.__b1_key = self.config.B1_SECRET_KEY.encode() - - @staticmethod - def __weighted_random_choice(options: list, weights: list) -> Any: - """ - Random choice a value from list according to the given weights - Argument: - options (list): option list - weights (list): weight list mapping the option list(Without Normalization) - """ - return f"{random.choices(options, weights=weights, k=1)[0]}" - - @staticmethod - def __get_renderer_info() -> list: - renderer_info_list = [ - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 400 (0x00000166) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4400 (0x00001112) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4600 (0x00000412) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 520 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 550 (0x00001512) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 6000 (0x1606) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 540 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 550 (0x1913) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640 (0x161C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 600 (0x3E80) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 655 (0x00009BC8) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe MAX Graphics (0x00009BC0) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A370M (0x0000AF51) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380 (0x0000AF41) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380M (0x0000AF5E) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A550 (0x0000AF42) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770 (0x0000AF43) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770M (0x0000AF50) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RPL‑P GT1) (0x0000A702) OpenGL 4.6)", - "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics 770 (0x00004680) OpenGL 4.6)", - "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) HD Graphics 4400 (0x00001122) OpenGL 4.6)", - "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (ADL‑S GT1) (0x0000A0A1) OpenGL 4.6)", - "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RKL GT1) (0x0000A9A1) OpenGL 4.6)", - "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics (CML GT2) (0x00009A14) OpenGL 4.6)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00001022) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics Family (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris Pro OpenGL Engine, OpenGL 4.1)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 645 (0x1616) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 655 (0x161E) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 730 (0x0000A100) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 805 (0x0000B0A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 3 Graphics (0x000015E0) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 8 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 11 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Graphics (0x00001636) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 5500 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 560 (0x000067EF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 570 (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 580 2048SP (0x00006FDF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 590 (0x000067FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 XT (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6650 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6700 XT (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6800 (0x000073BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6900 XT (0x000073C2) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 7700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5300M OpenGL Engine, OpenGL 4.1)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5500 XT OpenGL Engine, OpenGL 4.1)", - "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon R7 370 Series (0x00006811) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (AMD)|ANGLE (AMD, ATI Technologies Inc. AMD Radeon RX Vega 64 OpenGL Engine, OpenGL 4.1)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 (0x00001C81) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti (0x00001C8C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 6GB (0x000010DE) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1070 (0x00001B81) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 (0x00001B80) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 SUPER (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 SUPER (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x0000250F) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Ti (0x00002489) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002488) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Ti (0x000028A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 (0x00002206) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Ti (0x00002208) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3090 (0x00002204) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 (0x00002882) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti (0x00002803) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Ti (0x00002857) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4080 (0x00002819) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 (0x00002684) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro RTX 5000 Ada Generation (0x000026B2) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 - "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro P400 (0x00001CB3) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)", # noqa: E501 - "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)", - "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device), SwiftShader driver)", - ] - - return random.choice(renderer_info_list).split("|") - - @staticmethod - def __get_width_and_height(): - width, height = XhsFpGenerator.__weighted_random_choice( - ["1366;768", "1600;900", "1920;1080", "2560;1440", "3840;2160", "7680;4320"], - [0.25, 0.15, 0.35, 0.15, 0.08, 0.02], - ).split(";") - if random.choice([True, False]): - availWidth = int(width) - int( - XhsFpGenerator.__weighted_random_choice([0, 30, 60, 80], [0.1, 0.4, 0.3, 0.2]) - ) - availHeight = height - else: - availWidth = width - availHeight = int(height) - int( - XhsFpGenerator.__weighted_random_choice([30, 60, 80, 100], [0.2, 0.5, 0.2, 0.1]) - ) - - return width, height, availWidth, availHeight - - def generate_b1(self, fp): - config = CryptoConfig() - b64_encoder = encoder.Base64Encoder(config) - b1_fp = { - "x33": fp["x33"], - "x34": fp["x34"], - "x35": fp["x35"], - "x36": fp["x36"], - "x37": fp["x37"], - "x38": fp["x38"], - "x39": fp["x39"], - "x42": fp["x42"], - "x43": fp["x43"], - "x44": fp["x44"], - "x45": fp["x45"], - "x46": fp["x46"], - "x48": fp["x48"], - "x49": fp["x49"], - "x50": fp["x50"], - "x51": fp["x51"], - "x52": fp["x52"], - "x82": fp["x82"], - } - b1_json = json.dumps(b1_fp, separators=(",", ":"), ensure_ascii=False) - cipher = ARC4.new(self.__b1_key) - ciphertext = cipher.encrypt(b1_json.encode("utf-8")).decode("latin1") - encoded_url = urllib.parse.quote(ciphertext, safe="!*'()~_-") - b = [] - for c in encoded_url.split("%")[1:]: - chars = list(c) - b.append(int("".join(chars[:2]), 16)) - [b.append(ord(j)) for j in chars[2:]] - - b1 = b64_encoder.custom_to_b64(bytearray(b)) - - return b1 - - @staticmethod - def get_fingerprint(cookies: dict, user_agent: str) -> dict: - cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) - - width, height, availWidth, availHeight = XhsFpGenerator.__get_width_and_height() - - is_incognito_mode = XhsFpGenerator.__weighted_random_choice(["true", "false"], [0.95, 0.05]) - - vendor, renderer = XhsFpGenerator.__get_renderer_info() - - x78_y = random.randint(2350, 2450) - fp = { - "x1": user_agent, - "x2": "false", # navigator.webdriver - "x3": "zh-CN", # navigator.language - "x4": XhsFpGenerator.__weighted_random_choice([16, 24, 30, 32], [0.05, 0.6, 0.05, 0.3]), - # screen.colorDepth - "x5": XhsFpGenerator.__weighted_random_choice([1, 2, 4, 8, 12, 16], [0.10, 0.25, 0.4, 0.2, 0.03, 0.01]), - # navigator.deviceMemory - "x6": "24", # screen.pixelDepth - "x7": f"{vendor},{renderer}", - "x8": XhsFpGenerator.__weighted_random_choice( - [2, 4, 6, 8, 12, 16, 24, 32], [0.1, 0.4, 0.2, 0.15, 0.08, 0.04, 0.02, 0.01] - ), - "x9": f"{width};{height}", - "x10": f"{availWidth};{availHeight}", - "x11": "-480", # new Date().getTimezoneOffset()。 - "x12": "Asia/Shanghai", # Intl.DateTimeFormat().resolvedOptions().timeZone default timezone - "x13": is_incognito_mode, # window.sessionStorage detect - "x14": is_incognito_mode, # window.localStorage detect - "x15": is_incognito_mode, # window.indexedDB detect - "x16": "false", - "x17": "false", - "x18": "un", - "x19": "Win32", - "x20": "", - "x21": "PDF Viewer,Chrome PDF Viewer,Chromium PDF Viewer,Microsoft Edge PDF Viewer,WebKit built-in PDF", - # navigator.plugins - "x22": hashlib.md5(secrets.token_bytes(32)).hexdigest(), - "x23": "false", # DOM environment detect - "x24": "false", - "x25": "false", - "x26": "false", - "x27": "false", - "x28": "0,false,false", - "x29": "4,7,8", - "x30": "swf object not loaded", # detect Flash(SWF) Boolean(navigator.plugins['Shockwave Flash']); - # "x32": "0", # haven't used - "x33": "0", # whether in WeChat browser constant - "x34": "0", # whether js paint tool is Brian Paul constant - "x35": "0", # whether did have loaded Modernizr constant - "x36": f"{random.randint(1, 20)}", # window.history.length history stack length - "x37": "0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|1|0|0|0|0|0", - "x38": "0|0|1|0|1|0|0|0|0|0|1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0", - "x39": 0, # localStorage.getItem('sc'); plus one by every fresh page - "x40": "0", # localStorage.getItem('ptt'); - "x41": "0", # localStorage.getItem('pst'); - "x42": "3.4.4", # Fingerprint.js version // constant - "x43": "742cc32c", # Detecting whether a browser has been tampered with by comparing - # the hash value of an image - "x44": f"{int(time.time() * 1000)}", # current timestamp - "x45": "__SEC_CAV__1-1-1-1-1|__SEC_WSA__|", - # risk control SDK information if risk just show like __SEC_WSA__| - "x46": "false", - # navigator.__proto__.hasOwnProperty('webdriver'); risk control - # Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver'); // true → risk control - "x47": "1|0|0|0|0|0", # recognize different browser「unique feature」 // constant - "x48": "", - "x49": "{list:[],type:}", - "x50": "", - "x51": "", - "x52": "", - "x55": "380,380,360,400,380,400,420,380,400,400,360,360,440,420", - "x56": f"{vendor}|{renderer}|{hashlib.md5(secrets.token_bytes(32)).hexdigest()}|35", - # x7 | Fingerprint2.x64hash128(WebGLRenderingContext.getSupportedExtensions()) | WebGLRenderingContext.getSupportedExtensions().length # noqa: E501 - "x57": cookie_string, - "x58": "180", # document.getElementsByTagName('div') // count div (just above 177, not strict) constant - "x59": "2", # performance.getEntriesByType("resource").length - "x60": "63", # risk control score // constant - "x61": "1291", # Object.getOwnPropertyNames(window) .length // window object amount - "x62": "2047", # HOOK detect 1,1,1,1,1,1,1,1,1,1,1 11个1(通过) 组成二进制2047 constant - "x63": "0", # JS VMP file \n detect // constant - "x64": "0", # HOOK ToString number detect - "x65": "0", - "x66": { # navigator.userAgent - "referer": "", - "location": "https://www.xiaohongshu.com/explore", - "frame": 0, - }, - "x67": "1|0", - "x68": "0", - "x69": "326|1292|30", - "x70": ["location"], - "x71": "true", - "x72": "complete", - "x73": "1191", - "x74": "0|0|0", - "x75": "Google Inc.", # Navigator.vendor - "x76": "true", # navigator.cookieEnabled - "x77": "1|1|1|1|1|1|1|1|1|1", # constant - "x78": { - "x": 0, - "y": x78_y, - "left": 0, - "right": 290.828125, - "bottom": x78_y + 18, - "height": 18, - "top": x78_y, - "width": 290.828125, - "font": 'system-ui, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, BlinkMacSystemFont, "Helvetica Neue", Arial, "PingFang SC", "PingFang TC", "PingFang HK", "Microsoft Yahei", "Microsoft JhengHei"', # noqa: E501 - }, - "x82": "_0x17a2|_0x1954", # get iframe.contentWindow | contentWindow.window - "x31": "124.04347527516074", - "x79": "144|599565058866", - # navigator.webkitTemporaryStorage.queryUsageAndQuota(used, granted) - "x53": hashlib.md5(secrets.token_bytes(32)).hexdigest(), - # "235c6559af50acefe4755120d05570a0" if "edge/" in user_agent else "993da9a681fd3994c9f53de11f2903b3", - # speechSynthesis.getVoices() Fingerprint2.x64hash128 - "x54": "10311144241322244122", - "x80": "1|[object FileSystemDirectoryHandle]", - } - - return fp - - @staticmethod - def update_fingerprint(fp: dict, cookies: dict, url: str) -> None: - cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) - - fp.update( - { - "x39": 0, # localStorage.getItem('p1'); Add the value with every request +1 - "x44": f"{time.time() * 1000}", # current timestamp multiply 1000 - "x57": cookie_string, - "x66": { # navigator.userAgent - "referer": "https://www.xiaohongshu.com/explore", - "location": url, - "frame": 0, - }, - } - ) - - -# test -# if __name__ == '__main__': -# cookie_str = ( -# 'abRequestId=6824d155-7a52-5954-8e0b-52621534b645; a1=199326716575ch0ke4fatjw4p6s5f36p9gyb1z0ep50000367754' -# '; webId=236dbdde593bd45175a14b220bf2cda8; gid' -# '=yjjqJKWqYSjWyjjqJKWyKku82W2Sx8Td4i09vdUVE4UKMD282iqKUy888qKWW2482dfW0SJf; x-user-id-creator.xiaohongshu' -# '.com=62653884000000001000d895; customerClientId=211311472456037; access-token-creator.xiaohongshu.com' -# '=customer.creator.AT-68c5175501602299901050976ci4wzrowbjolncn; galaxy_creator_session_id' -# '=dv0q3EYVV52XFb03dqv9wFqc5wOrcnZBBG6q; galaxy.creator.beaker.session.id=1757908666550096704063; webBuild=4' -# '.81.0; web_session=040069b9c0b8fa37713448b6e43a4b16f2f1b5; xsecappid=xhs-pc-web; loadts=1758606105469; ' -# 'acw_tc=0ad586d517586066572636904e2fbedce995fe2d22709cedcfeabec5ab856d; websectiga' -# '=10f9a40ba454a07755a08f27ef8194c53637eba4551cf9751c009d9afb564467; sec_poison_id=7d693d02-b858-4476-a432' -# '-5c003b3ff9f0; unread={%22ub%22:%2268d13701000000000b03cfc1%22%2C%22ue%22:%2268c26c99000000001d026ca5%22%2C' -# '%22uc%22:24}') -# c = SimpleCookie() -# c.load(cookie_str) -# cookie_dict = {k: morsel.value for k, morsel in c.items()} -# -# headers = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 " -# "Safari/537.36") -# -# crypto_config = CryptoConfig() -# gfp = XhsFpGenerator(crypto_config) -# fp = gfp.get_fingerprint(cookies=cookie_dict, user_agent=headers) -# b1 = gfp.generate_b1(fp) -# print(b1) From 3c9ba5bb9facc9d9b8632c46d59aab51a2115e51 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 01:52:35 +0800 Subject: [PATCH 13/19] perf(encoder): optimize base64 encoding with cached translation tables --- src/xhshow/utils/encoder.py | 143 +++++++----------------------------- 1 file changed, 25 insertions(+), 118 deletions(-) diff --git a/src/xhshow/utils/encoder.py b/src/xhshow/utils/encoder.py index a06c5d5..0d105c9 100644 --- a/src/xhshow/utils/encoder.py +++ b/src/xhshow/utils/encoder.py @@ -2,7 +2,6 @@ import base64 import binascii -from collections.abc import Iterable from ..config import CryptoConfig @@ -12,61 +11,27 @@ class Base64Encoder: def __init__(self, config: CryptoConfig): self.config = config - - def encode_to_b64(self, data_to_encode: str | bytes, alphabet: str = CryptoConfig.CUSTOM_BASE64_ALPHABET) -> str: - """ - Encode a string using custom Base64 alphabet - - Args: - data_to_encode: Original UTF-8 string to be encoded - alphabet: Base64 alphabet to use for encoding - - Returns: - Base64 string encoded using custom alphabet - """ - if isinstance(data_to_encode, str): - b = data_to_encode.encode("utf-8") - elif isinstance(data_to_encode, bytes | bytearray | memoryview): - b = bytes(data_to_encode) - else: - try: - b = bytes(int(x) & 0xFF for x in data_to_encode) # type: ignore[arg-type] - except TypeError as e: - raise TypeError(f"unsupported type: {type(data_to_encode)} (expected bytes/str/Iterable[int])") from e - - n = len(b) - rem = n % 3 - stop = n - rem - out_parts: list[str] = [] - - CHUNK = 16383 - for i in range(0, stop, CHUNK): - end = min(i + CHUNK, stop) - j = i - chunk_out: list[str] = [] - while j < end: - val = (b[j] << 16) | (b[j + 1] << 8) | b[j + 2] - chunk_out.append( - alphabet[(val >> 18) & 63] - + alphabet[(val >> 12) & 63] - + alphabet[(val >> 6) & 63] - + alphabet[val & 63] - ) - j += 3 - out_parts.append("".join(chunk_out)) - - if rem == 1: - e = b[-1] - out_parts.append(alphabet[e >> 2] + alphabet[(e << 4) & 63] + "==") - elif rem == 2: - e = (b[-2] << 8) | b[-1] - out_parts.append(alphabet[(e >> 10) & 63] + alphabet[(e >> 4) & 63] + alphabet[(e << 2) & 63] + "=") - - return "".join(out_parts) + # Cache translation tables for better performance + self._custom_encode_table = str.maketrans( + config.STANDARD_BASE64_ALPHABET, + config.CUSTOM_BASE64_ALPHABET, + ) + self._custom_decode_table = str.maketrans( + config.CUSTOM_BASE64_ALPHABET, + config.STANDARD_BASE64_ALPHABET, + ) + self._x3_encode_table = str.maketrans( + config.STANDARD_BASE64_ALPHABET, + config.X3_BASE64_ALPHABET, + ) + self._x3_decode_table = str.maketrans( + config.X3_BASE64_ALPHABET, + config.STANDARD_BASE64_ALPHABET, + ) def encode(self, data_to_encode: str) -> str: """ - Encode a string using custom Base64 alphabet (compatibility method) + Encode a string using custom Base64 alphabet Args: data_to_encode: Original UTF-8 string to be encoded @@ -74,63 +39,11 @@ def encode(self, data_to_encode: str) -> str: Returns: Base64 string encoded using custom alphabet """ - return self.encode_to_b64(data_to_encode, self.config.CUSTOM_BASE64_ALPHABET) + data_bytes = data_to_encode.encode("utf-8") + standard_encoded_bytes = base64.b64encode(data_bytes) + standard_encoded_string = standard_encoded_bytes.decode("utf-8") - @staticmethod - def custom_to_b64(data: bytes | str | Iterable[int]) -> str: - """ - XHS official encrypt method (tripletToBase64). same as above function, but FP gen must be deal binary string - support: - - str: use UTF-8 encode to byte - - bytes/bytearray/memory view: use it straight - - Iterable[int] (eg: list[int]): bitwise with & 0xFF trans to single bit - - Returns: - Base64 string encoded using custom alphabet - """ - alphabet = CryptoConfig.CUSTOM_BASE64_ALPHABET - - # —— all datas convert to bytes —— # - if isinstance(data, str): - b = data.encode("utf-8") - elif isinstance(data, bytes | bytearray | memoryview): - b = bytes(data) - else: - try: - # allow list/tuple/any could be iterable int, auto & 0xFF auto filter the value not between 0..255 - b = bytes(int(x) & 0xFF for x in data) # type: ignore[arg-type] - except TypeError as e: - raise TypeError(f"unsupported type: {type(data)} (expected bytes/str/Iterable[int])") from e - - n = len(b) - rem = n % 3 - stop = n - rem - out_parts: list[str] = [] - - CHUNK = 16383 - for i in range(0, stop, CHUNK): - end = min(i + CHUNK, stop) - j = i - chunk_out: list[str] = [] - while j < end: - val = (b[j] << 16) | (b[j + 1] << 8) | b[j + 2] - chunk_out.append( - alphabet[(val >> 18) & 63] - + alphabet[(val >> 12) & 63] - + alphabet[(val >> 6) & 63] - + alphabet[val & 63] - ) - j += 3 - out_parts.append("".join(chunk_out)) - - if rem == 1: - e = b[-1] - out_parts.append(alphabet[e >> 2] + alphabet[(e << 4) & 63] + "==") - elif rem == 2: - e = (b[-2] << 8) | b[-1] - out_parts.append(alphabet[(e >> 10) & 63] + alphabet[(e >> 4) & 63] + alphabet[(e << 2) & 63] + "=") - - return "".join(out_parts) + return standard_encoded_string.translate(self._custom_encode_table) def decode(self, encoded_string: str) -> str: """ @@ -145,11 +58,8 @@ def decode(self, encoded_string: str) -> str: Raises: ValueError: Base64 decoding failed """ - reverse_translation_table = str.maketrans( - self.config.CUSTOM_BASE64_ALPHABET, self.config.STANDARD_BASE64_ALPHABET - ) + standard_encoded_string = encoded_string.translate(self._custom_decode_table) - standard_encoded_string = encoded_string.translate(reverse_translation_table) try: decoded_bytes = base64.b64decode(standard_encoded_string) except (binascii.Error, ValueError) as e: @@ -169,9 +79,8 @@ def decode_x3(self, encoded_string: str) -> bytes: Raises: ValueError: Base64 decoding failed """ - reverse_translation_table = str.maketrans(self.config.X3_BASE64_ALPHABET, self.config.STANDARD_BASE64_ALPHABET) + standard_encoded_string = encoded_string.translate(self._x3_decode_table) - standard_encoded_string = encoded_string.translate(reverse_translation_table) try: decoded_bytes = base64.b64decode(standard_encoded_string) except (binascii.Error, ValueError) as e: @@ -191,6 +100,4 @@ def encode_x3(self, input_bytes: bytes | bytearray) -> str: standard_encoded_bytes = base64.b64encode(input_bytes) standard_encoded_string = standard_encoded_bytes.decode("utf-8") - translation_table = str.maketrans(self.config.STANDARD_BASE64_ALPHABET, self.config.X3_BASE64_ALPHABET) - - return standard_encoded_string.translate(translation_table) + return standard_encoded_string.translate(self._x3_encode_table) From aefa827b511180dbf331d82be1b8b7d47ad5827c Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 01:52:52 +0800 Subject: [PATCH 14/19] feat(client): add unified cookie parsing and x-s-common signature support --- src/xhshow/client.py | 97 ++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 10d4259..d71ede2 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -1,13 +1,11 @@ import hashlib import json import time -from http.cookies import SimpleCookie from typing import Any, Literal from .config import CryptoConfig -from .core.crc32_encrypt import CRC32 +from .core.common_sign import XsCommonSigner from .core.crypto import CryptoProcessor -from .utils.generate_fingerprint import XhsFpGenerator from .utils.random_gen import RandomGenerator from .utils.url_utils import build_url, extract_uri from .utils.validators import ( @@ -140,7 +138,7 @@ def sign_xs( signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + self._build_signature( d_value, a1_value, xsec_appid, content_string, timestamp ) - return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode_to_b64( + return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode( json.dumps(signature_data, separators=(",", ":"), ensure_ascii=False) ) @@ -149,33 +147,17 @@ def sign_xs_common( cookie_dict: dict[str, Any] | str, ) -> str: """ - Convenience wrapper to generate the `x-s-common` signature. + Generate x-s-common signature Args: - cookie_dict: complete cookie dictionary + cookie_dict: Complete cookie dictionary or cookie string Returns: - Encoded signature string suitable for the header. + Encoded x-s-common signature string """ - if isinstance(cookie_dict, str): - ck = SimpleCookie() - ck.load(cookie_dict) - cookie_dict = {k: morsel.value for k, morsel in ck.items()} - - gfp = XhsFpGenerator(self.config) - a1_value = cookie_dict["a1"] - fingerprint = gfp.get_fingerprint(cookies=cookie_dict, user_agent=self.crypto_processor.config.PUBLIC_USERAGENT) - b1 = gfp.generate_b1(fingerprint) - - x9 = CRC32.crc32_js_int(b1) - sign_struct = CryptoConfig.SIGNATURE_XSCOMMON_TEMPLATE - sign_struct["x5"] = a1_value - sign_struct["x8"] = b1 - sign_struct["x9"] = x9 - sign_json = json.dumps(sign_struct, separators=(",", ":"), ensure_ascii=False) - sign_list = list(sign_json.encode("utf-8")) - xs_common = self.crypto_processor.b64encoder.custom_to_b64(sign_list) - return xs_common + parsed_cookies = self._parse_cookies(cookie_dict) + signer = XsCommonSigner(self.config) + return signer.sign(parsed_cookies) @validate_get_signature_params def sign_xs_get( @@ -390,11 +372,29 @@ def get_x_t(self, timestamp: float | None = None) -> int: timestamp = time.time() return int(timestamp * 1000) + def _parse_cookies(self, cookies: dict[str, Any] | str) -> dict[str, Any]: + """ + Parse cookies to dict format + + Args: + cookies: Cookie dictionary or cookie string + + Returns: + dict: Parsed cookie dictionary + """ + if isinstance(cookies, str): + from http.cookies import SimpleCookie + + ck = SimpleCookie() + ck.load(cookies) + return {k: morsel.value for k, morsel in ck.items()} + return cookies + def sign_headers( self, method: Literal["GET", "POST"], uri: str, - a1_value: str, + cookies: dict[str, Any] | str, xsec_appid: str = "xhs-pc-web", params: dict[str, Any] | None = None, payload: dict[str, Any] | None = None, @@ -406,33 +406,34 @@ def sign_headers( Args: method: Request method ("GET" or "POST") uri: Request URI or full URL - a1_value: a1 value from cookies + cookies: Complete cookie dictionary or cookie string xsec_appid: Application identifier, defaults to `xhs-pc-web` params: GET request parameters (only used when method="GET") payload: POST request body data (only used when method="POST") timestamp: Unix timestamp in seconds (defaults to current time) Returns: - dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + dict: Complete headers including x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid Examples: >>> client = Xhshow() + >>> cookies = {"a1": "your_a1_value", "web_session": "..."} >>> # GET request >>> headers = client.sign_headers( ... method="GET", ... uri="/api/sns/web/v1/user_posted", - ... a1_value="your_a1_value", + ... cookies=cookies, ... params={"num": "30"} ... ) >>> # POST request >>> headers = client.sign_headers( ... method="POST", ... uri="/api/sns/web/v1/login", - ... a1_value="your_a1_value", + ... cookies=cookies, ... payload={"username": "test"} ... ) >>> headers.keys() - dict_keys(['x-s', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) + dict_keys(['x-s', 'x-s-common', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) """ if timestamp is None: timestamp = time.time() @@ -451,13 +452,21 @@ def sign_headers( else: raise ValueError(f"Unsupported method: {method}") + cookie_dict = self._parse_cookies(cookies) + + a1_value = cookie_dict.get("a1") + if not a1_value: + raise ValueError("Missing 'a1' in cookies") + x_s = self.sign_xs(method_upper, uri, a1_value, xsec_appid, request_data, timestamp) + x_s_common = self.sign_xs_common(cookie_dict) x_t = self.get_x_t(timestamp) x_b3_traceid = self.get_b3_trace_id() x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) return { - "x-s": x_s, + # "x-s": x_s, + "x-s-common": x_s_common, "x-t": str(x_t), "x-b3-traceid": x_b3_traceid, "x-xray-traceid": x_xray_traceid, @@ -466,7 +475,7 @@ def sign_headers( def sign_headers_get( self, uri: str, - a1_value: str, + cookies: dict[str, Any] | str, xsec_appid: str = "xhs-pc-web", params: dict[str, Any] | None = None, timestamp: float | None = None, @@ -476,28 +485,29 @@ def sign_headers_get( Args: uri: Request URI or full URL - a1_value: a1 value from cookies + cookies: Complete cookie dictionary or cookie string xsec_appid: Application identifier, defaults to `xhs-pc-web` params: GET request parameters timestamp: Unix timestamp in seconds (defaults to current time) Returns: - dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + dict: Complete headers including x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid Examples: >>> client = Xhshow() + >>> cookies = {"a1": "your_a1_value", "web_session": "..."} >>> headers = client.sign_headers_get( ... uri="/api/sns/web/v1/user_posted", - ... a1_value="your_a1_value", + ... cookies=cookies, ... params={"num": "30"} ... ) """ - return self.sign_headers("GET", uri, a1_value, xsec_appid, params=params, timestamp=timestamp) + return self.sign_headers("GET", uri, cookies, xsec_appid, params=params, timestamp=timestamp) def sign_headers_post( self, uri: str, - a1_value: str, + cookies: dict[str, Any] | str, xsec_appid: str = "xhs-pc-web", payload: dict[str, Any] | None = None, timestamp: float | None = None, @@ -507,20 +517,21 @@ def sign_headers_post( Args: uri: Request URI or full URL - a1_value: a1 value from cookies + cookies: Complete cookie dictionary or cookie string xsec_appid: Application identifier, defaults to `xhs-pc-web` payload: POST request body data timestamp: Unix timestamp in seconds (defaults to current time) Returns: - dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + dict: Complete headers including x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid Examples: >>> client = Xhshow() + >>> cookies = {"a1": "your_a1_value", "web_session": "..."} >>> headers = client.sign_headers_post( ... uri="/api/sns/web/v1/login", - ... a1_value="your_a1_value", + ... cookies=cookies, ... payload={"username": "test", "password": "123456"} ... ) """ - return self.sign_headers("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) + return self.sign_headers("POST", uri, cookies, xsec_appid, payload=payload, timestamp=timestamp) From ace356c31575e4b16d2485eb2e11a53b8c3bc44b Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 01:53:08 +0800 Subject: [PATCH 15/19] test: add comprehensive cookie parsing tests and update crypto tests --- tests/test_cookie_parsing.py | 425 +++++++++++++++++++++++++++++++++++ tests/test_crypto.py | 87 +++++-- 2 files changed, 498 insertions(+), 14 deletions(-) create mode 100644 tests/test_cookie_parsing.py diff --git a/tests/test_cookie_parsing.py b/tests/test_cookie_parsing.py new file mode 100644 index 0000000..532df01 --- /dev/null +++ b/tests/test_cookie_parsing.py @@ -0,0 +1,425 @@ +"""Tests for cookie parsing and sign_headers functionality""" + +import pytest + +from xhshow import Xhshow +from xhshow.core.common_sign import XsCommonSigner + + +class TestCookieParsing: + """测试 Cookie 解析功能""" + + def setup_method(self): + self.client = Xhshow() + + def test_parse_cookies_from_dict(self): + """测试从字典解析 cookies""" + cookies_dict = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + + result = self.client._parse_cookies(cookies_dict) + + assert isinstance(result, dict) + assert result == cookies_dict + assert result is cookies_dict # Should return the same object for dict input + + def test_parse_cookies_from_string(self): + """测试从字符串解析 cookies""" + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + assert "webId" in result + assert result["a1"] == "test_a1_value" + assert result["web_session"] == "test_session" + assert result["webId"] == "test_web_id" + + def test_parse_cookies_from_string_with_spaces(self): + """测试解析带空格的 cookie 字符串""" + cookie_string = "a1=value1; web_session=value2 ;webId=value3" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + assert "webId" in result + + def test_parse_cookies_from_string_with_special_chars(self): + """测试解析包含特殊字符的 cookie""" + cookie_string = 'a1=abc123_-.; web_session="quoted value"; key=value=' + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + + def test_parse_cookies_empty_dict(self): + """测试空字典解析""" + result = self.client._parse_cookies({}) + + assert isinstance(result, dict) + assert len(result) == 0 + + def test_parse_cookies_empty_string(self): + """测试空字符串解析""" + result = self.client._parse_cookies("") + + assert isinstance(result, dict) + assert len(result) == 0 + + +class TestSignXsCommon: + """测试 x-s-common 签名功能""" + + def setup_method(self): + self.client = Xhshow() + + def test_sign_xs_common_with_dict(self): + """测试使用字典生成 x-s-common""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + + result = self.client.sign_xs_common(cookies) + + assert isinstance(result, str) + assert len(result) > 0 + + def test_sign_xs_common_with_string(self): + """测试使用字符串生成 x-s-common""" + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + result = self.client.sign_xs_common(cookie_string) + + assert isinstance(result, str) + assert len(result) > 0 + + def test_sign_xs_common_consistency(self): + """测试字典和字符串输入的一致性""" + cookies_dict = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + result_dict = self.client.sign_xs_common(cookies_dict) + result_string = self.client.sign_xs_common(cookie_string) + + # Both should produce valid results + assert isinstance(result_dict, str) + assert isinstance(result_string, str) + assert len(result_dict) > 0 + assert len(result_string) > 0 + + def test_sign_xs_common_missing_a1(self): + """测试缺少 a1 时的异常处理""" + cookies = { + "web_session": "test_session", + "webId": "test_web_id", + } + + with pytest.raises(KeyError, match="a1"): + self.client.sign_xs_common(cookies) + + def test_sign_xsc_alias(self): + """测试 sign_xsc 别名方法""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + result = self.client.sign_xsc(cookies) + + assert isinstance(result, str) + assert len(result) > 0 + + +class TestXsCommonSigner: + """测试 XsCommonSigner 类""" + + def setup_method(self): + self.signer = XsCommonSigner() + + def test_sign_with_dict_only(self): + """测试只接受字典参数""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + + result = self.signer.sign(cookies) + + assert isinstance(result, str) + assert len(result) > 0 + + def test_sign_missing_a1(self): + """测试缺少 a1 的异常""" + cookies = { + "web_session": "test_session", + } + + with pytest.raises(KeyError, match="a1"): + self.signer.sign(cookies) + + def test_sign_reproducibility(self): + """测试签名的可重现性""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + result1 = self.signer.sign(cookies) + result2 = self.signer.sign(cookies) + + # Note: May contain random elements, so just check format + assert isinstance(result1, str) + assert isinstance(result2, str) + assert len(result1) > 0 + assert len(result2) > 0 + + +class TestSignHeaders: + """测试 sign_headers 系列方法""" + + def setup_method(self): + self.client = Xhshow() + + def test_sign_headers_get_with_dict(self): + """测试 GET 请求使用字典 cookies""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers( + method="GET", + uri="/api/sns/web/v1/user_posted", + cookies=cookies, + params={"num": "30"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-s-common" in headers + assert "x-t" in headers + assert "x-b3-traceid" in headers + assert "x-xray-traceid" in headers + + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + assert headers["x-t"].isdigit() + assert len(headers["x-b3-traceid"]) == 16 + assert len(headers["x-xray-traceid"]) == 32 + + def test_sign_headers_get_with_string(self): + """测试 GET 请求使用字符串 cookies""" + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + headers = self.client.sign_headers( + method="GET", + uri="/api/sns/web/v1/user_posted", + cookies=cookie_string, + params={"num": "30"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-s-common" in headers + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + + def test_sign_headers_post_with_dict(self): + """测试 POST 请求使用字典 cookies""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers( + method="POST", + uri="/api/sns/web/v1/login", + cookies=cookies, + payload={"username": "test", "password": "123456"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + + def test_sign_headers_post_with_string(self): + """测试 POST 请求使用字符串 cookies""" + cookie_string = "a1=test_a1_value; web_session=test_session" + + headers = self.client.sign_headers( + method="POST", + uri="/api/sns/web/v1/login", + cookies=cookie_string, + payload={"username": "test"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-s-common" in headers + assert headers["x-s"].startswith("XYS_") + + def test_sign_headers_missing_a1(self): + """测试缺少 a1 cookie 的异常处理""" + cookies = { + "web_session": "test_session", + } + + with pytest.raises(ValueError, match="Missing 'a1' in cookies"): + self.client.sign_headers( + method="GET", + uri="/api/test", + cookies=cookies, + params={"key": "value"}, + ) + + def test_sign_headers_get_convenience(self): + """测试 sign_headers_get 便捷方法""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers_get( + uri="/api/sns/web/v1/user_posted", + cookies=cookies, + params={"num": "30"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + def test_sign_headers_post_convenience(self): + """测试 sign_headers_post 便捷方法""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers_post( + uri="/api/sns/web/v1/login", + cookies=cookies, + payload={"username": "test"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + def test_sign_headers_with_timestamp(self): + """测试使用自定义时间戳""" + import time + + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + custom_ts = time.time() + + headers = self.client.sign_headers( + method="GET", + uri="/api/test", + cookies=cookies, + params={"key": "value"}, + timestamp=custom_ts, + ) + + assert headers["x-t"] == str(int(custom_ts * 1000)) + + def test_cookie_parsing_only_once(self): + """测试 cookie 只被解析一次(性能测试)""" + # This is more of a design verification test + # We can't directly test parse count, but we verify the result is correct + + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + headers = self.client.sign_headers( + method="GET", + uri="/api/test", + cookies=cookie_string, + params={"key": "value"}, + ) + + # Both x-s and x-s-common should work correctly + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + + # Verify all expected headers are present + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + +class TestCookieParsingEdgeCases: + """测试 Cookie 解析的边界情况""" + + def setup_method(self): + self.client = Xhshow() + + def test_cookie_with_equals_in_value(self): + """测试 cookie 值中包含等号""" + cookie_string = "a1=abc=def=ghi; web_session=test" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + # SimpleCookie handles this - value should be everything after first = + assert "=" in result["a1"] or result["a1"] == "abc" + + def test_cookie_with_semicolon_in_quoted_value(self): + """测试引号内包含分号的 cookie""" + cookie_string = 'a1="value;with;semicolons"; web_session=test' + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + + def test_cookie_with_unicode(self): + """测试包含 Unicode 字符的 cookie""" + cookies_dict = { + "a1": "test_a1_值", + "web_session": "测试", + } + + result = self.client._parse_cookies(cookies_dict) + + assert result == cookies_dict + assert result["a1"] == "test_a1_值" + assert result["web_session"] == "测试" + + def test_cookie_string_only_one_cookie(self): + """测试只有一个 cookie 的字符串""" + cookie_string = "a1=test_value" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert len(result) == 1 + assert result["a1"] == "test_value" + + def test_cookie_with_path_and_domain(self): + """测试带有 path 和 domain 的 cookie 字符串""" + # SimpleCookie can handle full cookie attributes + cookie_string = "a1=value1; Path=/; Domain=.example.com; web_session=value2" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + # Should extract cookie names and values, ignoring attributes + assert "a1" in result or "Path" in result # SimpleCookie behavior diff --git a/tests/test_crypto.py b/tests/test_crypto.py index a32ed58..5bad44c 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -1,6 +1,7 @@ import pytest from xhshow import CryptoProcessor, Xhshow +from xhshow.core.crc32_encrypt import CRC32 class TestCryptoProcessor: @@ -54,7 +55,7 @@ def test_base58_encoder(self): def test_base64_encoder(self): """测试自定义Base64编码""" test_string = "Hello, World!" - result = self.crypto.b64encoder.encode_to_b64(test_string) + result = self.crypto.b64encoder.encode(test_string) assert isinstance(result, str) assert len(result) > 0 @@ -516,15 +517,17 @@ def test_sign_headers(self): client = Xhshow() # Test GET request headers with params + cookies = {"a1": "test_a1_value", "web_session": "test_session"} headers = client.sign_headers( method="GET", uri="/api/sns/web/v1/user_posted", - a1_value="test_a1_value", + cookies=cookies, params={"num": "30", "cursor": "", "user_id": "123"}, ) assert isinstance(headers, dict) assert "x-s" in headers + assert "x-s-common" in headers assert "x-t" in headers assert "x-b3-traceid" in headers assert "x-xray-traceid" in headers @@ -538,19 +541,19 @@ def test_sign_headers(self): headers_post = client.sign_headers( method="POST", uri="/api/sns/web/v1/login", - a1_value="test_a1_value", + cookies=cookies, payload={"username": "test", "password": "123456"}, ) assert isinstance(headers_post, dict) - assert all(k in headers_post for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert all(k in headers_post for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) # Test with custom timestamp custom_ts = time.time() headers_custom = client.sign_headers( method="GET", uri="/api/test", - a1_value="test_a1", + cookies=cookies, params={"key": "value"}, timestamp=custom_ts, ) @@ -570,21 +573,22 @@ def test_sign_headers_get(self): client = Xhshow() # Test basic usage + cookies = {"a1": "test_a1_value", "web_session": "test_session"} headers = client.sign_headers_get( uri="/api/sns/web/v1/user_posted", - a1_value="test_a1_value", + cookies=cookies, params={"num": "30", "cursor": "", "user_id": "123"}, ) assert isinstance(headers, dict) - assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) assert headers["x-s"].startswith("XYS_") assert all(isinstance(v, str) for v in headers.values()) # Test with custom timestamp custom_ts = time.time() headers_ts = client.sign_headers_get( - uri="/api/test", a1_value="test_a1", params={"key": "value"}, timestamp=custom_ts + uri="/api/test", cookies=cookies, params={"key": "value"}, timestamp=custom_ts ) assert headers_ts["x-t"] == str(int(custom_ts * 1000)) @@ -596,21 +600,22 @@ def test_sign_headers_post(self): client = Xhshow() # Test basic usage + cookies = {"a1": "test_a1_value", "web_session": "test_session"} headers = client.sign_headers_post( uri="/api/sns/web/v1/login", - a1_value="test_a1_value", + cookies=cookies, payload={"username": "test", "password": "123456"}, ) assert isinstance(headers, dict) - assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) assert headers["x-s"].startswith("XYS_") assert all(isinstance(v, str) for v in headers.values()) # Test with custom timestamp custom_ts = time.time() headers_ts = client.sign_headers_post( - uri="/api/test", a1_value="test_a1", payload={"key": "value"}, timestamp=custom_ts + uri="/api/test", cookies=cookies, payload={"key": "value"}, timestamp=custom_ts ) assert headers_ts["x-t"] == str(int(custom_ts * 1000)) @@ -619,12 +624,14 @@ def test_sign_headers_parameter_validation(self): """测试 sign_headers 参数验证""" client = Xhshow() + cookies = {"a1": "test_a1", "web_session": "test_session"} + # Test GET request with payload should raise error with pytest.raises(ValueError, match="GET requests must use 'params', not 'payload'"): client.sign_headers( method="GET", uri="/api/test", - a1_value="test_a1", + cookies=cookies, payload={"key": "value"}, ) @@ -633,7 +640,7 @@ def test_sign_headers_parameter_validation(self): client.sign_headers( method="POST", uri="/api/test", - a1_value="test_a1", + cookies=cookies, params={"key": "value"}, ) @@ -642,6 +649,58 @@ def test_sign_headers_parameter_validation(self): client.sign_headers( method="PUT", uri="/api/test", - a1_value="test_a1", + cookies=cookies, params={"key": "value"}, ) + + +class TestCRC32: + """测试 CRC32 加密功能""" + + def test_crc32_js_int_basic(self): + """测试基本的 CRC32 计算""" + test_string = ( + "I38rHdgsjopgIvesdVwgIC+oIELmBZ5e3VwXLgFTIxS3bqwErFeexd0ekncAzMFYnqthIhJeSBMDKutRI3KsYorWHPtGrbV0P9W" + ) + result = CRC32.crc32_js_int(test_string) + + assert isinstance(result, int) + assert result == 679790455 + + def test_crc32_signed_unsigned(self): + """测试有符号和无符号结果""" + test_data = "test_data" + + signed_result = CRC32.crc32_js_int(test_data, signed=True) + unsigned_result = CRC32.crc32_js_int(test_data, signed=False) + + assert isinstance(signed_result, int) + assert isinstance(unsigned_result, int) + assert -2147483648 <= signed_result <= 2147483647 + assert 0 <= unsigned_result <= 0xFFFFFFFF + + def test_crc32_string_modes(self): + """测试不同的字符串模式""" + test_string = "测试中文" + + js_result = CRC32.crc32_js_int(test_string, string_mode="js") + utf8_result = CRC32.crc32_js_int(test_string, string_mode="utf8") + + assert isinstance(js_result, int) + assert isinstance(utf8_result, int) + # JS mode 和 UTF8 mode 对中文的处理应该不同 + assert js_result != utf8_result + + def test_crc32_bytes_input(self): + """测试字节输入""" + test_bytes = b"test_bytes" + result = CRC32.crc32_js_int(test_bytes) + + assert isinstance(result, int) + + def test_crc32_iterable_input(self): + """测试可迭代输入""" + test_list = [72, 101, 108, 108, 111] # "Hello" + result = CRC32.crc32_js_int(test_list) + + assert isinstance(result, int) From a32b6a5ae622a1277dc89a5dcb936b39e071fb01 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 01:53:32 +0800 Subject: [PATCH 16/19] chore(crc32): remove example code comments --- src/xhshow/core/crc32_encrypt.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/xhshow/core/crc32_encrypt.py b/src/xhshow/core/crc32_encrypt.py index aaba3a4..e74650f 100644 --- a/src/xhshow/core/crc32_encrypt.py +++ b/src/xhshow/core/crc32_encrypt.py @@ -122,9 +122,3 @@ def crc32_js_int( a = cls.POLY u = ((cls.MASK32 ^ c) ^ a) & cls.MASK32 # (-1 ^ c ^ a) >>> 0 return cls._to_signed32(u) if signed else u - - -# Example quick usage: -# if __name__ == "__main__": -# s = "I38rHdgsjopgIvesdVwgIC+oIELmBZ5e3VwXLgFTIxS3bqwErFeexd0ekncAzMFYnqthIhJeSBMDKutRI3KsYorWHPtGrbV0P9W" -# print(CRC32.crc32_js_int(s)) From 3ddfc15c70e4f375bfcc8fd12c557de7964da4f6 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 01:53:41 +0800 Subject: [PATCH 17/19] docs: update API documentation for cookie-based authentication --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4b2b513..8defa5c 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,32 @@ import requests client = Xhshow() +# 准备 cookies(支持字典或字符串格式) +cookies = { + "a1": "your_a1_value", + "web_session": "your_web_session", + "webId": "your_web_id" +} +# 或使用 Cookie 字符串格式: +# cookies = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id" + # 注意: uri 参数可以传递完整 URL 或 URI 路径,会自动提取 URI headers = client.sign_headers_get( uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # 完整 URL(推荐) # uri="/api/sns/web/v1/user_posted", # 或者只传 URI 路径 - a1_value="your_a1_cookie_value", + cookies=cookies, # 传入完整 cookies params={"num": "30", "cursor": "", "user_id": "123"} ) +# 返回的 headers 包含以下字段: +# { +# "x-s": "XYS_...", +# "x-s-common": "...", +# "x-t": "1234567890", +# "x-b3-traceid": "...", +# "x-xray-traceid": "..." +# } + # 方式1: 使用 update 方法更新现有 headers(推荐) base_headers = { "User-Agent": "Mozilla/5.0...", @@ -49,7 +67,7 @@ response = requests.get( "https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", params={"num": "30", "cursor": "", "user_id": "123"}, headers=base_headers, - cookies={"a1": "your_a1_cookie_value"} + cookies=cookies ) # 方式2: 使用 ** 解包创建新 headers @@ -61,13 +79,13 @@ response = requests.get( "Content-Type": "application/json", **headers # 解包签名 headers(会创建新字典) }, - cookies={"a1": "your_a1_cookie_value"} + cookies=cookies ) # POST 请求示例:使用 sign_headers_post headers_post = client.sign_headers_post( uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", - a1_value="your_a1_cookie_value", + cookies=cookies, payload={"username": "test", "password": "123456"} ) @@ -75,7 +93,7 @@ response = requests.post( "https://edith.xiaohongshu.com/api/sns/web/v1/login", json={"username": "test", "password": "123456"}, headers={**base_headers, **headers_post}, - cookies={"a1": "your_a1_cookie_value"} + cookies=cookies ) # 构建符合xhs平台的GET请求链接 @@ -101,26 +119,30 @@ import requests client = Xhshow() -# 也可以使用统一方法 sign_headers(需要手动指定 method) +# 注意: sign_headers_* 系列方法使用 cookies 参数 +# 而 sign_xs_* 系列方法使用 a1_value 参数 + +# 使用统一方法 sign_headers(需要手动指定 method) +cookies = {"a1": "your_a1_value", "web_session": "..."} headers = client.sign_headers( method="GET", # 或 "POST" uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", - a1_value="your_a1_cookie_value", + cookies=cookies, # sign_headers 使用 cookies 参数 params={"num": "30"} # GET 请求使用 params,POST 请求使用 payload ) -# GET 请求签名 +# GET 请求签名(使用 sign_xs_get - 只需要 a1_value) x_s = client.sign_xs_get( uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # v0.1.3及后续版本支持自动提取uri # uri="/api/sns/web/v1/user_posted" # v0.1.2及以前版本需要主动提取uri - a1_value="your_a1_cookie_value", + a1_value="your_a1_cookie_value", # sign_xs_* 系列使用 a1_value params={"num": "30", "cursor": "", "user_id": "123"} ) -# POST 请求签名 +# POST 请求签名(使用 sign_xs_post - 只需要 a1_value) x_s = client.sign_xs_post( uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", - a1_value="your_a1_cookie_value", + a1_value="your_a1_cookie_value", # sign_xs_* 系列使用 a1_value payload={"username": "test", "password": "123456"} ) @@ -149,6 +171,30 @@ x_s = client.sign_xs_get( ) x_t = client.get_x_t(timestamp=timestamp) x_xray_traceid = client.get_xray_trace_id(timestamp=int(timestamp * 1000)) + +# 生成 x-s-common 签名 +# x-s-common 签名需要完整的 cookies +cookies = { + "a1": "your_a1_value", + "web_session": "your_web_session", + "webId": "your_web_id" +} + +# 方式1: 使用 sign_xsc 别名方法(推荐) +xs_common = client.sign_xsc(cookie_dict=cookies) + +# 方式2: 使用完整方法名 +xs_common = client.sign_xs_common(cookie_dict=cookies) + +# 方式3: 支持 Cookie 字符串格式 +cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id" +xs_common = client.sign_xsc(cookie_dict=cookie_string) + +# 使用在请求中 +headers = { + "x-s-common": xs_common, + # ... 其他 headers +} ``` @@ -180,10 +226,25 @@ client = Xhshow(config=custom_config) ## 参数说明 -- `uri`: 请求URI(去除https域名和查询参数) -- `a1_value`: cookie中的a1值 +### **sign_headers** 系列方法(推荐使用) +- `uri`: 请求 URI 或完整 URL(会自动提取 URI) +- `cookies`: 完整的 cookie 字典或 cookie 字符串 + - 字典格式: `{"a1": "...", "web_session": "...", "webId": "..."}` + - 字符串格式: `"a1=...; web_session=...; webId=..."` - `xsec_appid`: 应用标识符,默认为 `xhs-pc-web` -- `params/payload`: 请求参数(GET用params,POST用payload) +- `params`: GET 请求参数(仅在 method="GET" 时使用) +- `payload`: POST 请求参数(仅在 method="POST" 时使用) +- `timestamp`: 可选的统一时间戳(秒) + +### **sign_xs** 系列方法 +- `uri`: 请求 URI 或完整 URL +- `a1_value`: cookie 中的 a1 值(字符串) +- `xsec_appid`: 应用标识符,默认为 `xhs-pc-web` +- `params/payload`: 请求参数(GET 用 params,POST 用 payload) +- `timestamp`: 可选的统一时间戳(秒) + +### **sign_xsc** 系列方法 +- `cookie_dict`: 完整的 cookie 字典或 cookie 字符串 ## 开发环境 From 764654c1d3d3a9162c996f7e73190e380f25e1ad Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 01:53:51 +0800 Subject: [PATCH 18/19] chore(deps): update project dependencies --- pyproject.toml | 1 + uv.lock | 123 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6495970..c084b7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ + "curl-cffi>=0.13.0", "pycryptodome>=3.23.0", ] diff --git a/uv.lock b/uv.lock index 4bf4396..4efe10c 100644 --- a/uv.lock +++ b/uv.lock @@ -45,6 +45,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "click" version = "8.2.1" @@ -66,6 +157,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "curl-cffi" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" }, + { url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" }, + { url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" }, + { url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -132,6 +244,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + [[package]] name = "pycryptodome" version = "3.23.0" @@ -284,6 +405,7 @@ wheels = [ name = "xhshow" source = { editable = "." } dependencies = [ + { name = "curl-cffi" }, { name = "pycryptodome" }, ] @@ -305,6 +427,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "curl-cffi", specifier = ">=0.13.0" }, { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, From 09b9e512b720015490688da823cc6a0793aba608 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 9 Dec 2025 11:11:05 +0800 Subject: [PATCH 19/19] Encode b1 from raw bytes (bytearray) instead of string/text to avoid byte corruption during encoding. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This restores parity with browser/VM output: the base64 result must start with I38r…. Prevents downstream requests from being rejected due to an invalid b1 value. --- src/xhshow/generators/fingerprint.py | 2 +- src/xhshow/utils/encoder.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/xhshow/generators/fingerprint.py b/src/xhshow/generators/fingerprint.py index db2b967..38957e7 100644 --- a/src/xhshow/generators/fingerprint.py +++ b/src/xhshow/generators/fingerprint.py @@ -65,7 +65,7 @@ def generate_b1(self, fp: dict) -> str: b.append(int("".join(chars[:2]), 16)) [b.append(ord(j)) for j in chars[2:]] - b1 = self._encoder.encode(json.dumps(b, separators=(",", ":"))) + b1 = self._encoder.encode(bytearray(b)) return b1 diff --git a/src/xhshow/utils/encoder.py b/src/xhshow/utils/encoder.py index 0d105c9..c803fa3 100644 --- a/src/xhshow/utils/encoder.py +++ b/src/xhshow/utils/encoder.py @@ -3,6 +3,7 @@ import base64 import binascii +from typing import Iterable from ..config import CryptoConfig __all__ = ["Base64Encoder"] @@ -29,7 +30,7 @@ def __init__(self, config: CryptoConfig): config.STANDARD_BASE64_ALPHABET, ) - def encode(self, data_to_encode: str) -> str: + def encode(self, data_to_encode: bytes | str | Iterable[int]) -> str: """ Encode a string using custom Base64 alphabet @@ -39,7 +40,9 @@ def encode(self, data_to_encode: str) -> str: Returns: Base64 string encoded using custom alphabet """ - data_bytes = data_to_encode.encode("utf-8") + data_bytes = data_to_encode + if not isinstance(data_to_encode, bytearray): + data_bytes = data_to_encode.encode("utf-8") standard_encoded_bytes = base64.b64encode(data_bytes) standard_encoded_string = standard_encoded_bytes.decode("utf-8")