type: 收窄虎牙验证与设备注册类型

This commit is contained in:
yml2213
2026-08-30 20:07:12 +08:00
parent 563477fde8
commit 3f97dbe525
2 changed files with 152 additions and 72 deletions
+109 -55
View File
@@ -10,6 +10,7 @@ import time
import uuid
from collections.abc import Mapping
from http.cookies import SimpleCookie
from typing import Any, cast
from urllib.parse import parse_qs, quote, urlparse
import cv2
@@ -86,17 +87,19 @@ class HuyaVerificationSolver:
self.session.cookies.update(self.cookie)
if proxies:
self.session.proxies = dict(proxies)
self.session.headers.update({
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Origin": "https://aq.huya.com",
"Pragma": "no-cache",
"Referer": "https://aq.huya.com/",
"User-Agent": self.ua,
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
})
self.session.headers.update(
{
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Origin": "https://aq.huya.com",
"Pragma": "no-cache",
"Referer": "https://aq.huya.com/",
"User-Agent": self.ua,
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
}
)
self.timeout = timeout
self.ocr = ocr or default_ocr()
@@ -105,7 +108,7 @@ class HuyaVerificationSolver:
self.code = ""
self.url_param_map: dict[str, str] = {}
self.strategy_url = ""
self.js_ctx = None
self.js_ctx: Any = None
self.behavior_events: list[dict] = []
self.app_id = app_id
self.page_url = page_url or self._auth_page_url()
@@ -125,9 +128,13 @@ class HuyaVerificationSolver:
return base64.b64decode(base64_string)
@staticmethod
def _vertical_concat_align_left(bg_bytes: bytes, tip_bytes: bytes, image_format: str = "JPEG") -> bytes:
def _vertical_concat_align_left(
bg_bytes: bytes, tip_bytes: bytes, image_format: str = "JPEG"
) -> bytes:
bg = cv2.imdecode(np.frombuffer(bg_bytes, np.uint8), cv2.IMREAD_COLOR)
tip = cv2.imdecode(np.frombuffer(tip_bytes, np.uint8), cv2.IMREAD_COLOR)
if bg is None or tip is None:
raise HuyaVerificationError("验证码图片解码失败")
max_width = max(bg.shape[1], tip.shape[1])
def pad_right(img, target_width):
@@ -162,7 +169,9 @@ class HuyaVerificationSolver:
return image.crop(bbox), black_border_height
@classmethod
def _composite_slider_images(cls, bg_bytes: bytes, tip_bytes: bytes, y_position: int) -> bytes:
def _composite_slider_images(
cls, bg_bytes: bytes, tip_bytes: bytes, y_position: int
) -> bytes:
bg = Image.open(io.BytesIO(bg_bytes)).convert("RGB")
tip = Image.open(io.BytesIO(tip_bytes)).convert("RGBA")
tip, black_border_height = cls._remove_black_borders(tip)
@@ -174,8 +183,14 @@ class HuyaVerificationSolver:
@staticmethod
def _opencv_slider_x(target_bytes: bytes, background_bytes: bytes) -> int:
target = cv2.imdecode(np.frombuffer(target_bytes, np.uint8), cv2.IMREAD_ANYCOLOR)
background = cv2.imdecode(np.frombuffer(background_bytes, np.uint8), cv2.IMREAD_ANYCOLOR)
target = cv2.imdecode(
np.frombuffer(target_bytes, np.uint8), cv2.IMREAD_ANYCOLOR
)
background = cv2.imdecode(
np.frombuffer(background_bytes, np.uint8), cv2.IMREAD_ANYCOLOR
)
if target is None or background is None:
raise HuyaVerificationError("滑块图片解码失败")
target = cv2.cvtColor(cv2.Canny(target, 100, 200), cv2.COLOR_GRAY2RGB)
background = cv2.cvtColor(cv2.Canny(background, 100, 200), cv2.COLOR_GRAY2RGB)
result = cv2.matchTemplate(background, target, cv2.TM_CCOEFF_NORMED)
@@ -188,7 +203,9 @@ class HuyaVerificationSolver:
try:
return response.json()
except json.JSONDecodeError as exc:
raise HuyaVerificationError(f"虎牙验证接口返回不是 JSON: {response.text[:200]}") from exc
raise HuyaVerificationError(
f"虎牙验证接口返回不是 JSON: {response.text[:200]}"
) from exc
def _compile_js(self, dx_js: str) -> None:
js_data = """
@@ -205,7 +222,9 @@ class HuyaVerificationSolver:
"""
self.js_ctx = execjs.compile(js_data.replace("$$self_js$$", dx_js))
def _build_common_payload(self, extra: dict, info_override: str | None = None) -> dict:
def _build_common_payload(
self, extra: dict, info_override: str | None = None
) -> dict:
payload = {
"urlParamMap": self.url_param_map,
"wupData": "",
@@ -224,11 +243,17 @@ class HuyaVerificationSolver:
"""web 用 mouse 事件,App WebView 场景用 touch 事件。"""
if not self.use_touch_events:
return action
return {"mousedown": "touchstart", "mousemove": "touchmove", "mouseup": "touchend"}.get(action, action)
return {
"mousedown": "touchstart",
"mousemove": "touchmove",
"mouseup": "touchend",
}.get(action, action)
def _encode_behavior(self) -> str:
"""编码浏览器行为,失败重试时会携带前一次点击记录。"""
return quote(json.dumps(self.behavior_events, separators=(",", ":")), safe="~()*!.'")
return quote(
json.dumps(self.behavior_events, separators=(",", ":")), safe="~()*!.'"
)
@staticmethod
def _extract_strategy_url(challenge_payload: dict | str) -> str:
@@ -294,7 +319,9 @@ class HuyaVerificationSolver:
)
self.behavior_events = []
def _restore_slider_background(self, bg_bytes: bytes, segment: str, huyapk: str) -> bytes:
def _restore_slider_background(
self, bg_bytes: bytes, segment: str, huyapk: str
) -> bytes:
recover_list = self.js_ctx.call("get_regoin_arr", segment, huyapk)
image = Image.open(io.BytesIO(bg_bytes)).convert("RGB")
width, height = image.size
@@ -320,15 +347,21 @@ class HuyaVerificationSolver:
logger.info("虎牙验证类型: 滑块")
bg_bytes = self._decode_image(data.get("dtuStr"))
tip_bytes = self._decode_image(data.get("puzzleStr"))
restored_bg = self._restore_slider_background(bg_bytes, segment, self.huyapk)
composite = self._composite_slider_images(restored_bg, tip_bytes, int(data.get("location_y") or 0))
restored_bg = self._restore_slider_background(
bg_bytes, segment, self.huyapk
)
composite = self._composite_slider_images(
restored_bg, tip_bytes, int(data.get("location_y") or 0)
)
slider_box = self.ocr.ocr_slider_box(composite)
yolo_x = int(slider_box[0] - 13)
opencv_x = self._opencv_slider_x(tip_bytes, restored_bg)
if abs(opencv_x - yolo_x) < 10:
logger.debug("虎牙滑块识别距离: yolo={}, opencv={}", yolo_x, opencv_x)
return int((opencv_x + yolo_x) / 2)
logger.debug("虎牙滑块识别距离: yolo={}, opencv={}, 使用yolo", yolo_x, opencv_x)
logger.debug(
"虎牙滑块识别距离: yolo={}, opencv={}, 使用yolo", yolo_x, opencv_x
)
return yolo_x
logger.info("虎牙验证类型: 点选")
@@ -348,18 +381,20 @@ class HuyaVerificationSolver:
"endTime": int(track_points[-1][-1]) - int(track_points[0][-1]),
}
act = self.js_ctx.call("get_act", act_data, self.huyapk)
self.behavior_events.append({
"action": self._pointer_action("mousedown"),
"id": "",
"txt": "",
"clientX": track_points[0][0],
"clientY": track_points[0][1],
"clientWidth": 350,
"clientHeight": 268,
"isTrusted": True,
"timeStamp": random.randint(1800, 5500),
"nowTime": int(time.time() * 1000),
})
self.behavior_events.append(
{
"action": self._pointer_action("mousedown"),
"id": "",
"txt": "",
"clientX": track_points[0][0],
"clientY": track_points[0][1],
"clientWidth": 350,
"clientHeight": 268,
"isTrusted": True,
"timeStamp": random.randint(1800, 5500),
"nowTime": int(time.time() * 1000),
}
)
payload = self._build_common_payload({"act": act, "ver": 5})
return self._submit_verify(url, payload)
@@ -387,18 +422,20 @@ class HuyaVerificationSolver:
txt_list = ["", "1", "1\\n2"]
behavior_list = []
for index, item in enumerate(click_data):
behavior_list.append({
"action": self._pointer_action("mousedown"),
"id": "",
"txt": txt_list[index] if index < len(txt_list) else "",
"clientX": item[0],
"clientY": item[1],
"clientWidth": 350,
"clientHeight": 268,
"isTrusted": True,
"timeStamp": random.randint(2000, 5000),
"nowTime": item[2],
})
behavior_list.append(
{
"action": self._pointer_action("mousedown"),
"id": "",
"txt": txt_list[index] if index < len(txt_list) else "",
"clientX": item[0],
"clientY": item[1],
"clientWidth": 350,
"clientHeight": 268,
"isTrusted": True,
"timeStamp": random.randint(2000, 5000),
"nowTime": item[2],
}
)
self.behavior_events.extend(behavior_list)
payload = self._build_common_payload({"act": act, "ver": 5})
@@ -426,8 +463,13 @@ class HuyaVerificationSolver:
dx_js = ""
for index in range(2):
logger.debug("虎牙验证获取config3 ({}/2)", index + 1)
config = self._request_json("post", "https://udbrtt.huya.com/auth/client/config3", json=config_payload)
data = config.get("data") if isinstance(config.get("data"), dict) else {}
config = self._request_json(
"post",
"https://udbrtt.huya.com/auth/client/config3",
json=config_payload,
)
raw_data = config.get("data")
data = cast(dict[str, Any], raw_data) if isinstance(raw_data, dict) else {}
gurl = str(data.get("gurl") or "")
vurl = str(data.get("vurl") or "")
dx_js = str(data.get("js") or "")
@@ -440,7 +482,10 @@ class HuyaVerificationSolver:
len(dx_js),
)
if not (gurl and vurl):
logger.warning("虎牙config3未下发验证会话, 完整响应: {}", json.dumps(config, ensure_ascii=False)[:1500])
logger.warning(
"虎牙config3未下发验证会话, 完整响应: {}",
json.dumps(config, ensure_ascii=False)[:1500],
)
if gurl and vurl and dx_js:
break
if index == 0 and self.url_param_map.get("sceneType") != "4":
@@ -451,7 +496,9 @@ class HuyaVerificationSolver:
time.sleep(0.3)
if not gurl or not vurl or not dx_js:
data_summary = {k: str(v)[:80] for k, v in (data or {}).items() if k != "js"}
data_summary = {
k: str(v)[:80] for k, v in (data or {}).items() if k != "js"
}
raise HuyaNoSessionError(
"config3未下发验证会话: "
f"returnCode={config.get('returnCode')}, "
@@ -461,12 +508,17 @@ class HuyaVerificationSolver:
)
self._compile_js(dx_js)
point_payload = self._build_common_payload({"ver": 5}, info_override=self._csid() if self.use_touch_events else None)
point_payload = self._build_common_payload(
{"ver": 5}, info_override=self._csid() if self.use_touch_events else None
)
last_result: dict = {}
for index in range(3):
if index:
time.sleep(0.4)
point_payload = self._build_common_payload({"ver": 5}, info_override=self._csid() if self.use_touch_events else None)
point_payload = self._build_common_payload(
{"ver": 5},
info_override=self._csid() if self.use_touch_events else None,
)
logger.info("虎牙验证失败后重试: {}/3", index + 1)
point = self._get_track_point(gurl, point_payload)
@@ -476,7 +528,9 @@ class HuyaVerificationSolver:
else:
last_result = self._send_slider_verify(vurl, int(point))
if isinstance(last_result, dict) and (last_result.get("authId") or last_result.get("nextAuthUrl")):
if isinstance(last_result, dict) and (
last_result.get("authId") or last_result.get("nextAuthUrl")
):
return last_result
logger.info(