177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
"""虎牙网页匿名设备态协议生成器(纯 HTTP,middle + anonymousLogin)。
|
||
|
||
生成 4 个网页设备态字段(来源见 docs/HUYA_COOKIE合并审计与来源矩阵.md):
|
||
|
||
- ``udb_deviceid`` <- GET /web/middle/2.4/{rand8}/https/{hex32} 的 Set-Cookie
|
||
- ``udb_guiddata`` <- middle URL 第三段 hex32(客户端自选,服务端不校验)
|
||
- ``udb_anobiztoken``<- POST /web/anonymousLogin 响应 data.biztoken(344 位)
|
||
- ``udb_anouid`` <- anonymousLogin 响应 data.uid
|
||
|
||
用法::
|
||
|
||
python -m core.huya.anon_device # 打印 4 字段 JSON
|
||
python -m core.huya.anon_device --json # 同上,紧凑 JSON(供脚本消费)
|
||
|
||
注意: 200 / returnCode=0 只证明接口受理,不证明风控认可这些值的真实性。
|
||
``guid`` / ``_qimei_uuid42`` / ``__yamid_new`` / ``game_did`` 仍无服务端
|
||
签发接口,本模块不生成、不伪造它们。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import random
|
||
import re
|
||
import secrets
|
||
import sys
|
||
from collections.abc import Mapping
|
||
|
||
import requests
|
||
from loguru import logger
|
||
|
||
MIDDLE_URL = "https://udblgn.huya.com/web/middle/2.4/{rand8}/https/{guiddata}"
|
||
ANON_LOGIN_URL = "https://udblgn.huya.com/web/anonymousLogin"
|
||
|
||
DESKTOP_UA = (
|
||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
||
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
||
)
|
||
|
||
UDBN_DEVICEID_RE = re.compile(r"udb_deviceid=([^;]+)")
|
||
|
||
|
||
class AnonymousDeviceError(RuntimeError):
|
||
"""匿名设备态获取失败。"""
|
||
|
||
|
||
def _new_session(*, proxies: Mapping[str, str] | None = None) -> requests.Session:
|
||
session = requests.Session()
|
||
session.trust_env = False
|
||
if proxies:
|
||
session.proxies.update(dict(proxies))
|
||
return session
|
||
|
||
|
||
def _parse_set_cookie_deviceid(set_cookie: str) -> str:
|
||
match = UDBN_DEVICEID_RE.search(set_cookie or "")
|
||
if not match or not re.fullmatch(r"w_\d{19}", match.group(1)):
|
||
raise AnonymousDeviceError("middle 响应未下发合法 udb_deviceid")
|
||
return match.group(1)
|
||
|
||
|
||
def fetch_anonymous_device_state(
|
||
*,
|
||
proxies: Mapping[str, str] | None = None,
|
||
timeout: float = 15.0,
|
||
session: requests.Session | None = None,
|
||
) -> dict[str, str]:
|
||
"""换取匿名设备态四字段:udb_deviceid / udb_guiddata / udb_anobiztoken / udb_anouid。
|
||
|
||
与网页 SDK 相同的两步:middle 引导页签发 deviceid,anonymousLogin 换取
|
||
biztoken/uid。所有参数客户端随机生成,无需任何既有 Cookie。
|
||
"""
|
||
own_session = session is None
|
||
s = session or _new_session(proxies=proxies)
|
||
try:
|
||
guiddata = secrets.token_hex(16)
|
||
rand8 = str(random.randint(10_000_000, 99_999_999))
|
||
middle_url = MIDDLE_URL.format(rand8=rand8, guiddata=guiddata)
|
||
|
||
# 1) middle:签发 udb_deviceid
|
||
resp = s.get(
|
||
middle_url,
|
||
headers={"User-Agent": DESKTOP_UA, "Referer": "https://www.huya.com/"},
|
||
timeout=timeout,
|
||
allow_redirects=False,
|
||
)
|
||
if resp.status_code != 200:
|
||
raise AnonymousDeviceError(f"middle 返回 {resp.status_code}")
|
||
deviceid = _parse_set_cookie_deviceid(resp.headers.get("Set-Cookie", ""))
|
||
|
||
# 2) anonymousLogin:发放匿名 uid + biztoken(必须带 middle Referer)
|
||
context = (
|
||
"WB-"
|
||
+ secrets.token_hex(16)
|
||
+ "-" + secrets.token_hex(16)
|
||
+ "-" + secrets.token_hex(16)
|
||
)
|
||
payload = {
|
||
"uri": "10013",
|
||
"version": "2.4",
|
||
"context": context,
|
||
"appId": 5002,
|
||
"sdid": "csid_" + secrets.token_hex(16),
|
||
"lcid": 2052,
|
||
"byPass": 3,
|
||
"requestId": random.randint(10_000_000, 99_999_999),
|
||
"authId": "",
|
||
"data": {"domainList": ""},
|
||
}
|
||
anon = s.post(
|
||
ANON_LOGIN_URL,
|
||
headers={
|
||
"User-Agent": DESKTOP_UA,
|
||
"Referer": middle_url,
|
||
"Content-Type": "application/json;charset=UTF-8",
|
||
},
|
||
data=json.dumps(payload),
|
||
timeout=timeout,
|
||
allow_redirects=False,
|
||
)
|
||
if anon.status_code != 200:
|
||
raise AnonymousDeviceError(f"anonymousLogin 返回 {anon.status_code}")
|
||
try:
|
||
body = anon.json()
|
||
except ValueError as exc:
|
||
raise AnonymousDeviceError(
|
||
f"anonymousLogin 响应非 JSON: {anon.text[:80]!r}"
|
||
) from exc
|
||
if body.get("returnCode") != 0:
|
||
raise AnonymousDeviceError(
|
||
f"anonymousLogin returnCode={body.get('returnCode')} "
|
||
f"message={body.get('message')!r}"
|
||
)
|
||
data = body.get("data") or {}
|
||
anouid = str(data.get("uid") or "")
|
||
biztoken = str(data.get("biztoken") or "")
|
||
if not anouid or not biztoken:
|
||
raise AnonymousDeviceError("anonymousLogin 响应缺少 uid/biztoken")
|
||
if not 300 <= len(biztoken) <= 400:
|
||
logger.warning("[huya-anon] biztoken 长度异常: {}", len(biztoken))
|
||
|
||
logger.info(
|
||
"[huya-anon] 匿名设备态 OK: deviceid={}... guiddata={}... "
|
||
"anouid={} biztoken={}...",
|
||
deviceid[:14], guiddata[:8], anouid, biztoken[:10],
|
||
)
|
||
return {
|
||
"udb_deviceid": deviceid,
|
||
"udb_guiddata": guiddata,
|
||
"udb_anobiztoken": biztoken,
|
||
"udb_anouid": anouid,
|
||
}
|
||
finally:
|
||
if own_session:
|
||
s.close()
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser(description="虎牙匿名设备态四字段生成(纯协议)")
|
||
ap.add_argument("--json", action="store_true", help="输出紧凑 JSON")
|
||
args = ap.parse_args()
|
||
try:
|
||
state = fetch_anonymous_device_state()
|
||
except AnonymousDeviceError as exc:
|
||
print(f"❌ {exc}", file=sys.stderr)
|
||
return 2
|
||
if args.json:
|
||
print(json.dumps(state, ensure_ascii=False))
|
||
else:
|
||
for key, value in state.items():
|
||
print(f"{key} = {value}")
|
||
print("\n(4/7 字段;guid/_qimei_uuid42/__yamid_new/game_did 无服务端签发,不生成)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main()) |