整理虎牙文档并接入设备画像指纹

This commit is contained in:
yml2213
2026-09-02 13:42:32 +08:00
parent 2a1b1e642c
commit c62c025ced
11 changed files with 410 additions and 41 deletions
+7 -1
View File
@@ -839,9 +839,15 @@ class HuyaAppPasswordLogin:
# (App 渠道),缺字段注入浏览器会导致 UDB SDK 判定设备态不完整。
from .web_cookie_fields import (
fill_web_cookie_fields,
qimei_feature_from_device_profile,
)
full_cookie = fill_web_cookie_fields(cookie_str, guid="", uid=uid)
full_cookie = fill_web_cookie_fields(
cookie_str,
guid="",
uid=uid,
qimei_feature=qimei_feature_from_device_profile(self.device_info),
)
appid_fixed = "; ".join(
f"{key}={value}" if key != "udb_appid" else "udb_appid=5002"
for key, value in cookie_pairs(full_cookie)
+167 -3
View File
@@ -20,6 +20,7 @@ from __future__ import annotations
import random
import secrets
import time
from collections.abc import Mapping
# 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐
_FILL_MAP: tuple[tuple[str, str], ...] = (
@@ -42,6 +43,32 @@ _BASE16 = "0123456789ABCDEF"
_GAME_DID_CHARS = (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
)
_DEFAULT_QIMEI_FEATURE = {
"screen": {"resolution": "800x600", "availResolution": "800x600"},
"navigator": {
"deviceMemory": 16,
"pdfViewerEnabled": True,
"cookieEnabled": True,
"productSub": "20030107",
"hardwareConcurrency": 10,
},
"webgl": "Google Inc. (Apple)~ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)",
"timezoneOffset": -480,
}
_DEVICE_GPU_FEATURES = {
"xiaomi": ("Qualcomm", "Adreno (TM) 650"),
"redmi": ("Qualcomm", "Adreno (TM) 650"),
"vivo": ("Qualcomm", "Adreno (TM) 650"),
"iqoo": ("Qualcomm", "Adreno (TM) 650"),
"oppo": ("Qualcomm", "Adreno (TM) 650"),
"oneplus": ("Qualcomm", "Adreno (TM) 650"),
"samsung": ("ARM", "Mali-G78"),
"honor": ("ARM", "Mali-G78"),
"motorola": ("Qualcomm", "Adreno (TM) 642L"),
"realme": ("Qualcomm", "Adreno (TM) 650"),
"nubia": ("Qualcomm", "Adreno (TM) 650"),
"gionee": ("Qualcomm", "Adreno (TM) 506"),
}
def _return_base16(a: int) -> str:
@@ -96,6 +123,140 @@ def _game_did_generate(length: int = 35) -> str:
return out
_MASK64 = (1 << 64) - 1
def _rotl64(value: int, shift: int) -> int:
shift %= 64
if not shift:
return value & _MASK64
return ((value << shift) | (value >> (64 - shift))) & _MASK64
def _fmix64(value: int) -> int:
value ^= value >> 33
value = value * 0xFF51AFD7ED558CCD & _MASK64
value ^= value >> 33
value = value * 0xC4CEB9FE1A85EC53 & _MASK64
value ^= value >> 33
return value & _MASK64
def _x64hash128(value: str, seed: int = 0) -> str:
"""复刻 QIMEI SDK 的 x64hash128(MurmurHash3 变体)。
SDK 按 JavaScript ``charCodeAt`` 取每个 UTF-16 code unit 的低 8 位;尾部
的第一个字节也保留其实现中的高 32 位布局,而不是标准 MurmurHash 布局。
"""
encoded = value.encode("utf-16-le", "surrogatepass")
data = bytes(encoded[index] for index in range(0, len(encoded), 2))
length = len(data)
h1 = h2 = seed & _MASK64
c1 = 0x87C37B91114253D5
c2 = 0x4CF5AD432745937F
for offset in range(0, length - length % 16, 16):
k1 = int.from_bytes(data[offset : offset + 8], "little")
k2 = int.from_bytes(data[offset + 8 : offset + 16], "little")
k1 = _rotl64(k1 * c1 & _MASK64, 31) * c2 & _MASK64
h1 ^= k1
h1 = (_rotl64(h1, 27) + h2) & _MASK64
h1 = (h1 * 5 + 0x52DCE729) & _MASK64
k2 = _rotl64(k2 * c2 & _MASK64, 33) * c1 & _MASK64
h2 ^= k2
h2 = (_rotl64(h2, 31) + h1) & _MASK64
h2 = (h2 * 5 + 0x38495AB5) & _MASK64
tail = data[length - length % 16 :]
tail_len = len(tail)
k1 = k2 = 0
for index in range(tail_len - 1, 7, -1):
k2 ^= tail[index] << (8 * (index - 8))
if tail_len >= 9:
h2 ^= _rotl64(k2 * c2 & _MASK64, 33) * c1 & _MASK64
for index in range(min(tail_len, 8) - 1, 0, -1):
k1 ^= tail[index] << (8 * index)
if tail_len >= 1:
# QIMEI's JS implementation passes [charCodeAt(g)] here, placing the
# first tail byte in the high 32-bit word.
k1 ^= tail[0] << 32
h1 ^= _rotl64(k1 * c1 & _MASK64, 31) * c2 & _MASK64
h1 ^= length
h2 ^= length
h1 = (h1 + h2) & _MASK64
h2 = (h2 + h1) & _MASK64
h1 = _fmix64(h1)
h2 = _fmix64(h2)
h1 = (h1 + h2) & _MASK64
h2 = (h2 + h1) & _MASK64
return f"{h1:016x}{h2:016x}"
def _js_text(value: object) -> str:
if value is None:
return ""
if isinstance(value, bool):
return "true" if value else "false"
return str(value)
def _qimei_feature_string(feature: Mapping[str, object]) -> str:
screen = feature.get("screen")
screen = screen if isinstance(screen, Mapping) else {}
navigator = feature.get("navigator")
navigator = navigator if isinstance(navigator, Mapping) else {}
values = (
screen.get("resolution"),
screen.get("availResolution"),
navigator.get("deviceMemory"),
navigator.get("pdfViewerEnabled"),
navigator.get("cookieEnabled"),
navigator.get("productSub"),
navigator.get("hardwareConcurrency"),
feature.get("webgl"),
feature.get("timezoneOffset"),
)
return "###".join(_js_text(item) for item in values) + "###"
def qimei_fingerprint_generate(feature: Mapping[str, object] | None = None) -> str:
"""生成 QIMEI ``_qimei_fingerprint``;输入应为 SDK ``getFeature()`` 结果。"""
source = feature or _DEFAULT_QIMEI_FEATURE
return _x64hash128(_qimei_feature_string(source), 31)
def qimei_feature_from_device_profile(
device_info: Mapping[str, object] | None,
) -> dict[str, object]:
"""将 Android 设备画像映射为 QIMEI WebView 能力特征的近似值。"""
info = device_info or {}
width = str(info.get("width") or "1080")
height = str(info.get("height") or "2400")
vendor = str(info.get("vendor") or "").strip().lower()
model = str(info.get("model") or "Android WebView").strip()
gpu_vendor, renderer = _DEVICE_GPU_FEATURES.get(
vendor, ("Qualcomm", f"{model} Graphics")
)
device_memory = info.get("device_memory") or 8
hardware_concurrency = info.get("hardware_concurrency") or 8
return {
"screen": {
"resolution": f"{width}x{height}",
"availResolution": f"{width}x{height}",
},
"navigator": {
"deviceMemory": device_memory,
"pdfViewerEnabled": False,
"cookieEnabled": True,
"productSub": "20030107",
"hardwareConcurrency": hardware_concurrency,
},
"webgl": f"Google Inc. ({gpu_vendor})~ANGLE ({gpu_vendor}, {renderer})",
"timezoneOffset": -480,
}
def _rand_float() -> str:
import random
@@ -108,6 +269,7 @@ def fill_web_cookie_fields(
uid: int | str = 0,
*,
keep_guid: bool = True,
qimei_feature: Mapping[str, object] | None = None,
) -> str:
"""补齐网页 Cookie 中缺失的字段。
@@ -116,6 +278,7 @@ def fill_web_cookie_fields(
guid: wsLaunch 响应返回的会话 guid(32hex);缺核心字段时必填。
uid: 虎牙 uid,用于 __yaoldyyuid 等字段。
keep_guid: 已存在 guid 且非空时是否保留(默认 True)。
qimei_feature: QIMEI SDK ``getFeature()`` 结果;未提供时使用稳定默认档案。
Returns:
补齐后的 Cookie 字符串(原始字段值不变,仅补缺失项)。
@@ -152,9 +315,8 @@ def fill_web_cookie_fields(
add("game_did", _game_did_generate()) # 35 位随机(真实长度)
# qimei 皮肤字段
# _qimei_fingerprint: QIMEI SDK SpookyHashV2(canvas指纹,31) 32hex;设备稳定,
# 无真实 canvas 时可随机(格式 32hex 对齐),真实算法见 trace 文档
add("_qimei_fingerprint", secrets.token_hex(16))
# _qimei_fingerprint: QIMEI SDK x64hash128(浏览器能力特征,31) 32hex。
add("_qimei_fingerprint", qimei_fingerprint_generate(qimei_feature))
add("_qimei_h38", secrets.token_hex(19))
# 会话/统计字段
@@ -209,4 +371,6 @@ __all__ = [
"cookie_fields",
"fill_web_cookie_fields",
"missing_web_cookie_fields",
"qimei_feature_from_device_profile",
"qimei_fingerprint_generate",
]