整理虎牙文档并接入设备画像指纹
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user