"""虎牙网页 Cookie 缺失字段补齐生成器。 账号 App 登录态只有 16 个 udb_* 认证字段,而活动/商城 WSS 需要完整的网页设备态。 浏览器实测(evidence/browser_cookie_harvest/report.md)确认: - ``udb_deviceid/udb_guiddata/udb_anobiztoken/udb_anouid`` 由匿名设备态协议 (core/huya/anon_device.py,middle + anonymousLogin)签发; - ``guid/_qimei_uuid42/__yamid_new/game_did`` 由页面 SDK 脚本写 document.cookie, 无服务端签发接口。其中 ``guid`` 与会话强绑定:**wsLaunch 响应返回的新 guid 必须 回写进 cookie**,否则 getConfig 无响应(2026-09-01 实测); ``_qimei_uuid42/__yamid_new/game_did`` 为设备标识,随机值格式对齐即可; - 其余统计/会话字段(SoundValue/Hm_*/rep_cnt 等)服务端仅记录,可随机生成。 2026-09-01 全链路实测:账号 id=58 补齐后 checkUserBindGameAccount 返回 status=200(和平精英已绑定)、getUserScore 返回 status=200(积分 4880)。 """ from __future__ import annotations import random import secrets import time from collections.abc import Mapping # 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐 _FILL_MAP: tuple[tuple[str, str], ...] = ( ("SoundValue", "0.50"), ("alphaValue", "0.80"), ("isInLiveRoom", "true"), ("udb_passdata", "3"), ("udb_accdata", "undefined"), ) # 浏览器实测逆向(2026-09-01,见 docs/HUYA_WEB_FIELD_TRACE.md): # - __yamid_new: room_normal.js generate32(UUID-v1 风格,1582-epoch 本地时区时间戳 # + v1 版本位 + 时钟序列 + 48 位随机 node)。浏览器 epoch = new Date(1582,10,15, # 0,0,0,0).getTime() 实测 -12216643543000(GMT+8)。CBCC 前缀是时间戳自然结果。 # - game_did: vplayerUI.js 35 位随机,字符集 64 去掉末尾 '_'(63 选 1)。 # - _qimei_uuid42: QIMEI SDK 生成,cookie 值首字节固定 '1a'(本地存储 44hex 截断)。 _SDK_EPOCH_1582 = -12216643543000 # 浏览器实测 JS Date(1582,10,15) 本地时区 ms _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: """JS returnBase(a,16):正整数转大写十六进制;a<=0 返回空串。""" if a <= 0: return "" out = "" while a > 0: out = _BASE16[a % 16] + out a //= 16 return out def _generate_bits(a: int, b: int, c: int) -> str: """JS generateBits(a,b,c):取 a 的 16 进制串 [floor(b/4)..floor(c/4)] 字符,越界补0。""" d = _return_base16(a) out = "" for g in range(b // 4, c // 4 + 1): out += d[g] if g < len(d) and d[g] != "" else "0" return out def _yamid_new_generate32(rng=None) -> str: """逐字复刻 room_normal.js generate32(__yamid_new 生成器)。""" import random r = rng if rng is not None else random.Random() now_ms = int(time.time() * 1000) c = now_ms - _SDK_EPOCH_1582 return "".join(( _generate_bits(c, 0, 31), _generate_bits(c, 32, 47), _generate_bits(c, 48, 59) + "1", _generate_bits(r.randrange(4095), 0, 7), _generate_bits(r.randrange(4095), 0, 7), _generate_bits(r.randrange(8191), 0, 7) + _generate_bits(r.randrange(8191), 8, 15) + _generate_bits(r.randrange(8191), 0, 7) + _generate_bits(r.randrange(8191), 8, 15) + _generate_bits(r.randrange(8191), 0, 15), )) def _game_did_generate(length: int = 35) -> str: """逐字复刻 vplayerUI.js game_did:35 位随机,字符集 64 去掉末尾 '_'。""" import random out = "" for _ in range(length): idx = int(random.random() * (len(_GAME_DID_CHARS) - 1)) # 63 → 排除 '_' out += _GAME_DID_CHARS[idx] 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 return f"{random.random():.17f}" def fill_web_cookie_fields( cookie: str, guid: str, uid: int | str = 0, *, keep_guid: bool = True, qimei_feature: Mapping[str, object] | None = None, ) -> str: """补齐网页 Cookie 中缺失的字段。 Args: cookie: 当前 Cookie(可只有 udb_* 认证字段)。 guid: wsLaunch 响应返回的会话 guid(32hex);缺核心字段时必填。 uid: 虎牙 uid,用于 __yaoldyyuid 等字段。 keep_guid: 已存在 guid 且非空时是否保留(默认 True)。 qimei_feature: QIMEI SDK ``getFeature()`` 结果;未提供时使用稳定默认档案。 Returns: 补齐后的 Cookie 字符串(原始字段值不变,仅补缺失项)。 """ pairs: dict[str, str] = {} for raw in (cookie or "").split(";"): item = raw.strip() if not item or "=" not in item: continue key, value = item.split("=", 1) key = key.strip() if key: pairs[key] = value.strip() def add(key: str, value: str): if value and (key not in pairs or not pairs[key]): pairs[key] = value uid = int(uid or 0) # 4 个核心设备字段 if guid: if keep_guid and pairs.get("guid"): pass # 已有真实 guid 则保留 else: add("guid", str(guid)) elif "guid" not in pairs: # guid 结构: '0a' + 10hex + '956a'/'966a' + 16hex(浏览器实测固定段,见 trace 文档) add("guid", "0a" + secrets.token_hex(5) + "966a" + secrets.token_hex(8)) # _qimei_uuid42: QIMEI SDK 产物,cookie 首字节 '1a' + UUID 布局 32hex + 8hex 附加 add("_qimei_uuid42", "1a" + secrets.token_hex(20)) # __yamid_new: room_normal.js generate32 精确复刻(UUID-v1 时间戳,含 CBCC 前缀规律) add("__yamid_new", _yamid_new_generate32()) add("game_did", _game_did_generate()) # 35 位随机(真实长度) # qimei 皮肤字段 # _qimei_fingerprint: QIMEI SDK x64hash128(浏览器能力特征,31) 32hex。 add("_qimei_fingerprint", qimei_fingerprint_generate(qimei_feature)) add("_qimei_h38", secrets.token_hex(19)) # 会话/统计字段 yasmid = _rand_float() add("__yasmid", yasmid) add("__yamid_tt1", yasmid) add("_rep_cnt", str(random.randint(0, 9))) add("rep_cnt", str(random.randint(0, 9))) add("h_unt", str(int(time.time()))) if uid: add("__yaoldyyuid", str(uid)) # _yasids 与 __yamid_new 同源(共享时间戳生成器,浏览器实测前缀一致) add("_yasids", "__rootsid%3D" + _yamid_new_generate32()) add("Hm_lvt_51700b6c722f5bb4cf39906a596ea41f", str(int(time.time()))) add("Hm_lpvt_51700b6c722f5bb4cf39906a596ea41f", str(int(time.time()))) add("HMACCOUNT", secrets.token_hex(8).upper()) add("huya_flash_rep_cnt", str(random.randint(1, 400))) add("huyasp_rep_cnt", str(random.randint(1, 40))) add("huya_hd_rep_cnt", str(random.randint(1, 40))) add("huya_web_rep_cnt", str(random.randint(300, 900))) for key, value in _FILL_MAP: add(key, value) return "; ".join(f"{key}={pairs[key]}" for key in pairs) def missing_web_cookie_fields(cookie: str) -> tuple[str, ...]: """返回缺失的网页核心字段(不含统计字段)。""" from .cookie_utils import cookie_value core = ( "guid", "_qimei_uuid42", "__yamid_new", "game_did", "_qimei_fingerprint", "_qimei_h38", "__yasmid", "__yamid_tt1", "udb_passdata", "_yasids", "HMACCOUNT", ) return tuple(key for key in core if not cookie_value(cookie, key)) def cookie_fields(cookie: str) -> dict[str, str]: """把 Cookie 字符串转 dict(用于测试/调试)。""" out = {} for raw in (cookie or "").split(";"): item = raw.strip() if not item or "=" not in item: continue key, value = item.split("=", 1) out[key.strip()] = value.strip() return out __all__ = [ "cookie_fields", "fill_web_cookie_fields", "missing_web_cookie_fields", "qimei_feature_from_device_profile", "qimei_fingerprint_generate", ]