From 4f099cc1962d6eaf7f1cee1c936ed387f61424cd Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 29 Aug 2026 16:51:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(huya):=20=E4=B8=80=E5=8F=B7=E4=B8=80?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E8=A1=A5=E5=AE=8C=20=E2=80=94=20hydevice=20?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=8F=82=E6=95=B0=E8=B7=9F=E9=9A=8F=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=94=BB=E5=83=8F=20+=20=E7=94=BB=E5=83=8F=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=E5=AD=97=E6=AE=B5=E4=B8=8B=E6=B2=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户实弹测试暴露两个缺口: 1. Hebe/GUID32 全空: 登录路径走 get_profile, 未经过 account_env 补字段 → _enrich_profile 下沉到 get_profile (幂等, 存量账号自动补齐) 2. sdid 前缀跨账号同源 (0UnHUgv0_qmfD4KAKlwzh...): env.js 是静态设备仿真 (写死 M2102J2SC), 状态目录隔离只改了'存储', 没改'采集输入' → env.js makeEnv(overrides) 支持按画像覆盖 UA/屏幕/机型/Android版本; runner 接收 device.json; get_huya_sdid(device_hint=画像); 下次登录每账号 hydevice 采集输入=各自绑定机型 → sdid 彻底分家 --- core/huya/app_login.py | 3 ++- core/huya/device_fingerprint.py | 42 ++++++++++++++++++++++++++++++++- core/huya/device_profile.py | 28 +++++++++++++++++++--- core/huya/fingerprint/env.js | 26 ++++++++++++++------ core/huya/fingerprint/runner.js | 5 +++- 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/core/huya/app_login.py b/core/huya/app_login.py index d68fed5..afe29d4 100644 --- a/core/huya/app_login.py +++ b/core/huya/app_login.py @@ -448,7 +448,8 @@ class HuyaAppPasswordLogin: # 一号一设备: sdid 状态按账号隔离 (默认全局目录会让所有账号共享 # 同一份 hydevice 设备状态, 服务端可跨账号关联 — 见 R40 缺口修复) sdid_obj = get_huya_sdid(allow_fallback=True, - state_dir=account_state_dir(self.username)) + state_dir=account_state_dir(self.username), + device_hint=self.device_info) sdid = sdid_obj.sdid if sdid_obj else "" pc = QrRole(pc=True, sdid=sdid, proxies=self.proxies) ph = QrRole(pc=False, sdid=sdid, proxies=self.proxies) diff --git a/core/huya/device_fingerprint.py b/core/huya/device_fingerprint.py index e68fc2e..3d8bbe9 100644 --- a/core/huya/device_fingerprint.py +++ b/core/huya/device_fingerprint.py @@ -9,6 +9,7 @@ from __future__ import annotations import shutil import subprocess import tempfile +import json from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path @@ -52,7 +53,11 @@ HDID_PREFIX = "__HDID__" def _run_node_runner(state_dir: Path, app_id: str, timeout: tuple[float, float]) -> tuple[str, str]: - """调用 node runner,返回 (sdid, hdid)。""" + """调用 node runner,返回 (sdid, hdid)。 + + 若 state_dir/device.json 存在 (账号画像派生的设备覆盖参数), runner 会以该 + 设备身份运行 hydevice → 不同账号的 sdid/40hex hdid 不再同源 (一号一设备)。 + """ node_bin = shutil.which("node") if not node_bin: raise HuyaFingerprintError("未找到 node 可执行文件(hydevice.js 需要 JS 引擎)") @@ -102,6 +107,37 @@ def _fallback_sdid(session: requests.Session, timeout) -> str: return collect_res.json().get("data", {}).get("sdid", "") +def write_device_hint(state_dir: str | Path, device_info: Mapping) -> Path: + """把账号画像写成 hydevice 设备覆盖参数 (env.js overrides)。 + + 来源: 画像 model/screen(格式"model,sdkVer,androidVer")/vendor + app 版本。 + 每账号独立 → hydevice 采集输入不同 → 服务端 sdid 不再同前缀。 + """ + state_dir = Path(state_dir) + state_dir.mkdir(parents=True, exist_ok=True) + screen = str(device_info.get("screen", "")) + parts = screen.split(",") + android_ver = parts[2] if len(parts) >= 3 else "11" + ua = ( + f"Mozilla/5.0 (Linux; Android {android_ver}; {device_info.get('model', '')} " + "Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) " + "Version/4.0 Chrome/149.0.7827.159 Mobile Safari/537.36 " + f"huya adr/13.4.22/{device_info.get('vendor', 'android')}/30" + ) + hint = { + "model": device_info.get("model", ""), + "vendor": device_info.get("vendor", "android"), + "androidVer": android_ver, + "appVer": str(device_info.get("app_version", "13.4.22")), + "userAgent": ua, + "screenWidth": int(device_info.get("width", 1080) or 1080), + "screenHeight": int(device_info.get("height", 2120) or 2120), + } + path = state_dir / "device.json" + path.write_text(json.dumps(hint, ensure_ascii=False), encoding="utf-8") + return path + + def get_huya_sdid( app_id: str = "5008", state_dir: str | Path | None = None, @@ -109,6 +145,7 @@ def get_huya_sdid( proxies: Mapping | None = None, timeout: tuple[float, float] = DEFAULT_TIMEOUT, allow_fallback: bool = True, + device_hint: Mapping | None = None, ) -> HuyaSdidResult: """获取虎牙高信任 sdid;node 链路失败时可选降级为低信任指纹。 @@ -125,6 +162,9 @@ def get_huya_sdid( if state_dir is None: state_dir = Path(tempfile.gettempdir()) / "huya_fp_state" state_dir = Path(state_dir) + if device_hint: + # 一号一设备: 账号画像 → hydevice 设备覆盖 (UA/屏幕/机型) + write_device_hint(state_dir, device_hint) try: sdid, hdid = _run_node_runner(state_dir, app_id, timeout) if sdid: diff --git a/core/huya/device_profile.py b/core/huya/device_profile.py index fe9a176..07254a5 100644 --- a/core/huya/device_profile.py +++ b/core/huya/device_profile.py @@ -100,12 +100,34 @@ def _save_db(db: dict) -> None: pass +def _enrich_profile(profile: dict) -> dict: + """补齐 dfp 一致性字段 (幂等): guid32 / Hebe_D1-D5。 + + 来源: R36 解密的真实 dfp 指纹 JSON 结构 (deviceinfo.guid + deviceinfo.Hebe_D1-D5)。 + 一号一设备: 每账号独立生成后终身不变, 供后续真实载荷构建与 GUI 设备绑定页展示。 + """ + changed = False + if not profile.get("guid32"): + profile["guid32"] = hashlib.sha256(os.urandom(16) + b"guid").hexdigest() + changed = True + hebe = profile.get("hebe") or {} + if len(hebe) < 5: + profile["hebe"] = {f"Hebe_D{i}": hashlib.sha256(os.urandom(16) + f"hebe{i}".encode()).hexdigest() + for i in range(1, 6)} + changed = True + return profile if changed else profile + + def get_profile(account: str, force_new: bool = False) -> dict: - """按账号获取或创建画像(幂等:同账号复用同一套)。""" + """按账号获取或创建画像(幂等:同账号复用同一套, 自动补齐缺失一致性字段)。""" db = _load_db() if not force_new and account in db: - return db[account] - p = generate_profile() + enriched = _enrich_profile(db[account]) + if enriched is not db[account]: + db[account] = enriched + _save_db(db) + return enriched + p = _enrich_profile(generate_profile()) db[account] = p _save_db(db) return p diff --git a/core/huya/fingerprint/env.js b/core/huya/fingerprint/env.js index 1c03132..c920bb0 100644 --- a/core/huya/fingerprint/env.js +++ b/core/huya/fingerprint/env.js @@ -1,17 +1,29 @@ -// Android WebView (M2102J2SC, 小米, Android 11) 环境仿真 -function makeEnv() { +// Android WebView 环境仿真 (默认 M2102J2SC; 可传设备覆盖参数实现一号一设备) +// overrides = {model, androidVer, vendor, appVer, screenWidth, screenHeight} +function makeEnv(overrides) { + const ov = overrides || {}; + const MODEL = ov.model || 'M2102J2SC'; + const ANDROID_VER = ov.androidVer || '11'; + const VENDOR = ov.vendor || 'xiaomi'; + const APP_VER = ov.appVer || '13.4.22'; + const UA = ov.userAgent || ('Mozilla/5.0 (Linux; Android ' + ANDROID_VER + '; ' + MODEL + + ' Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 ' + + 'Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/' + APP_VER + '/' + VENDOR + '/30'); + // 视口按物理分辨率/3 仿真 (393x851 ≈ 1080x2120 / 2.75) + const VW = ov.screenWidth ? Math.round(ov.screenWidth / 2.75) : 393; + const VH = ov.screenHeight ? Math.round(ov.screenHeight / 2.75) : 851; const W = globalThis; - W.screen = {width:393, height:851, availWidth:393, availHeight:851, + W.screen = {width:VW, height:VH, availWidth:VW, availHeight:VH, colorDepth:24, pixelDepth:24, availLeft:0, availTop:0, orientation:{type:'portrait-primary', angle:0}}; W.devicePixelRatio = 2.75; - W.innerWidth = 393; W.innerHeight = 737; W.outerWidth = 393; W.outerHeight = 851; + W.innerWidth = VW; W.innerHeight = Math.max(VH - 114, 500); W.outerWidth = VW; W.outerHeight = VH; W.location = {href:'https://aq.huya.com/p/safe_auth/pt_auth.html', protocol:'https:', host:'aq.huya.com', hostname:'aq.huya.com', port:'', pathname:'/p/safe_auth/pt_auth.html', search:'', hash:'', origin:'https://aq.huya.com', toString(){return this.href}}; W.history = {length:2, state:null, pushState(){}, replaceState(){}, back(){}, go(){}}; const navProps = { - userAgent:'Mozilla/5.0 (Linux; Android 11; M2102J2SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/13.4.22/xiaomi/30', - appName:'Netscape', appVersion:'5.0 (Linux; Android 11; M2102J2SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.159 Mobile Safari/537.36', + userAgent: UA, + appName:'Netscape', appVersion: UA.replace(/^Mozilla\//, ''), platform:'Linux armv8l', language:'zh-CN', languages:['zh-CN','zh','en'], cookieEnabled:true, doNotTrack:null, maxTouchPoints:5, hardwareConcurrency:8, deviceMemory:8, vendor:'Google Inc.', productSub:'20030107', webdriver:false, @@ -19,7 +31,7 @@ function makeEnv() { getBattery(){return Promise.resolve({charging:true, level:0.9})}, userAgentData: {brands:[{brand:'Chromium',version:'149'},{brand:'Google Chrome',version:'149'},{brand:'Not?A_Brand',version:'24'}], mobile:true, platform:'Android', - getHighEntropyValues(){return Promise.resolve({architecture:'arm', bitness:'64', model:'M2102J2SC', platformVersion:'11.0.0', uaFullVersion:'149.0.7827.159'})}}, + getHighEntropyValues(){return Promise.resolve({architecture:'arm', bitness:'64', model:MODEL, platformVersion:ANDROID_VER+'.0.0', uaFullVersion:'149.0.7827.159'})}}, connection: {effectiveType:'4g', downlink:10, rtt:50, saveData:false}, plugins: {length:0, item(){return null}, namedItem(){return null}}, mimeTypes: {length:0}, diff --git a/core/huya/fingerprint/runner.js b/core/huya/fingerprint/runner.js index c413b29..3c66b64 100644 --- a/core/huya/fingerprint/runner.js +++ b/core/huya/fingerprint/runner.js @@ -1,11 +1,14 @@ // hydevice 指纹运行器: 仿真 Android WebView 环境 -> 真实请求 df/token + df/collect -> 输出 sdid +// argv: [stateDir, deviceJson] — deviceJson 为账号画像派生的设备覆盖参数 (一号一设备) process.on('uncaughtException', e => { console.error('[uncaught]', String(e && e.message).slice(0, 120)); }); const fs = require('fs'); const path = require('path'); const https = require('https'); globalThis.window = globalThis; -require(path.join(__dirname, 'env.js')).makeEnv(); +let _deviceOverrides = null; +try { _deviceOverrides = JSON.parse(fs.readFileSync(process.argv[3] || '', 'utf8')); } catch (e) {} +require(path.join(__dirname, 'env.js')).makeEnv(_deviceOverrides); // ---- localStorage 持久化(设备稳定性) ---- const stateDir = process.argv[2] || '.';