cdp_a1_login.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. """A1 password login automation: real Edge browser (Playwright) + jfbym cloud slider
  2. recognition + Playwright realistic mouse drag (Bezier curve) + HTTP 996box byPwd.
  3. Why this exists:
  4. Pure HTTP w-parameter generation gets rejected by 996box geetest server
  5. (result=fail: synthetic trajectory detected). Solution: a real browser drags the
  6. slider with human-like motion; jfbym decides *where* to drag, browser does
  7. *how* to drag. The captchaOutput from the page is then plugged into the
  8. 996box byPwd HTTP call we already wrote.
  9. """
  10. from __future__ import annotations
  11. import argparse
  12. import asyncio
  13. import base64
  14. import json
  15. import os
  16. import random
  17. import sys
  18. import time
  19. from pathlib import Path
  20. _HERE = Path(__file__).resolve().parent
  21. sys.path.insert(0, str(_HERE.parent / "src"))
  22. from cq2_sdk.env_loader import load_env
  23. load_env(_HERE.parent / ".env")
  24. from cq2_sdk.login.jfbym import JfbymSliderSolver
  25. from cq2_sdk.login.box_996 import Box996Error, CaptchaRequiredError, LOGIN_BASE
  26. from cq2_sdk.store import UserStore, connection
  27. from cq2_sdk.debug_log import DebugLogger
  28. EDGE = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
  29. LOGIN_URL = "https://pc.hzwlan.com/login/account"
  30. def human_drag(start_x: int, start_y: int, end_x: int, end_y: int,
  31. steps: int = 25, jitter_px: float = 1.5) -> list[tuple[int, int, int]]:
  32. """Build a realistic mouse-drag trajectory: bezier-curve + variable speed + micro-jitter.
  33. Returns: list of (x, y, dt_ms) tuples for Input.dispatchMouseEvent sequence.
  34. """
  35. pts = []
  36. # bezier control points to make a slight arc (humans don't drag in straight line)
  37. cx1 = start_x + (end_x - start_x) * 0.3 + random.uniform(-15, 15)
  38. cy1 = start_y - random.uniform(8, 18)
  39. cx2 = start_x + (end_x - start_x) * 0.7 + random.uniform(-10, 10)
  40. cy2 = start_y + random.uniform(-5, 5)
  41. total_ms = random.randint(380, 650) # human drag duration: 0.4-0.65s
  42. dt_per = total_ms // steps
  43. # variable speed: slower at start (overcome inertia), fast in middle, slow at end
  44. for i in range(steps + 1):
  45. t = i / steps
  46. # ease-in-out cubic
  47. s = 3*t*t - 2*t*t*t if t < 0.5 else 1 - (-2*t + 2)**3 / 2
  48. # de Casteljau bezier
  49. def bez(t, p0, p1, p2, p3):
  50. return (1-t)**3*p0 + 3*(1-t)**2*t*p1 + 3*(1-t)*t**2*p2 + t**3*p3
  51. bx = bez(s, start_x, cx1, cx2, end_x)
  52. by = bez(s, start_y, cy1, cy2, end_y)
  53. # micro jitter
  54. bx += random.uniform(-jitter_px, jitter_px)
  55. by += random.uniform(-jitter_px/2, jitter_px/2)
  56. # timing: extra dwell at start + end (humans pause to confirm)
  57. if i in (0, 1, steps - 1, steps):
  58. dt_per += random.randint(15, 45)
  59. pts.append((int(bx), int(by), int(dt_per)))
  60. return pts
  61. async def solve_captcha_and_login(
  62. username: str, password: str, jfbym_token: str, log: DebugLogger
  63. ) -> str:
  64. from playwright.async_api import async_playwright
  65. solver = JfbymSliderSolver(token=jfbym_token)
  66. async with async_playwright() as p:
  67. # Use system Edge (real browser, full CDP stack)
  68. browser = await p.chromium.launch(
  69. executable_path=EDGE,
  70. headless=True,
  71. args=["--disable-blink-features=AutomationControlled"],
  72. )
  73. ctx = await browser.new_context(
  74. viewport={"width": 1366, "height": 768},
  75. user_agent="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 "
  76. "(KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 "
  77. "Edg/108.0.1462.54",
  78. locale="zh-CN",
  79. )
  80. # stealth: hide webdriver flag
  81. await ctx.add_init_script("""
  82. Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
  83. """)
  84. page = await ctx.new_page()
  85. log.info("opening_login_page", url=LOGIN_URL)
  86. await page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=30000)
  87. # page.evaluate takes ONE arg; bundle the two strings into a list and
  88. # destructure inside the JS function. Then set the React-friendly
  89. # native value via the prototype setter, dispatch input + change so
  90. # Ant Design's controlled input + Form re-validates and clears the
  91. # disabled class on the submit button.
  92. await page.evaluate(
  93. "([u, p]) => {"
  94. " const setVal = (sel, v) => {"
  95. " const el = document.querySelector(sel);"
  96. " const nativeSetter = Object.getOwnPropertyDescriptor("
  97. " window.HTMLInputElement.prototype, 'value').set;"
  98. " nativeSetter.call(el, v);"
  99. " el.dispatchEvent(new Event('input', {bubbles: true}));"
  100. " el.dispatchEvent(new Event('change', {bubbles: true}));"
  101. " };"
  102. " setVal('input#username', u);"
  103. " setVal('input#password', p);"
  104. " const btn = document.querySelector("
  105. " 'button.login-form_box__submit_button');"
  106. " if (btn) { btn.disabled = false; btn.classList.remove('disabled'); }"
  107. "}",
  108. [username, password],
  109. )
  110. await page.wait_for_timeout(300)
  111. # Click via Playwright (DOM click still dispatches).
  112. login_btn = page.locator("button.login-form_box__submit_button").first
  113. await login_btn.click(force=True)
  114. log.info("login_button_clicked")
  115. # Wait for the geetest widget (iframe/canvas) to appear
  116. # geetest v4 typical: canvas with class .geetest_canvas_slice / .geetest_canvas_bg
  117. try:
  118. await page.wait_for_selector(
  119. ".geetest_canvas_bg, .geetest_slice, canvas",
  120. timeout=15000,
  121. )
  122. except Exception as e:
  123. log.warn("geetest_widget_not_found", error=str(e)[:80])
  124. log.info("geetest_widget_visible")
  125. # Let the puzzle fully render
  126. await asyncio.sleep(0.8)
  127. # 1) Screenshot bg image (the background with the gap)
  128. bg_path = _HERE / "jfbym_diag" / "a1_cdp_bg.png"
  129. bg_path.parent.mkdir(exist_ok=True)
  130. bg_locator = page.locator(".geetest_canvas_bg").first
  131. await bg_locator.screenshot(path=str(bg_path))
  132. log.info("bg_screenshot_saved", path=str(bg_path))
  133. # 2) Get slider track and pointer for the drag start
  134. slider_loc = page.locator(".geetest_canvas_slice, .geetest_slider_button").first
  135. box = await slider_loc.bounding_box()
  136. if not box:
  137. raise RuntimeError("slider track not found")
  138. start_x = int(box["x"] + box["width"] / 2)
  139. start_y = int(box["y"] + box["height"] / 2)
  140. log.info("slider_start", x=start_x, y=start_y)
  141. # 3) Upload bg to jfbym for distance
  142. with open(bg_path, "rb") as f:
  143. bg_bytes = f.read()
  144. gap_x = await asyncio.to_thread(
  145. solver.solve_distance_sync,
  146. base64.b64encode(bg_bytes).decode(),
  147. "true", # extra=true mode: "background only" -> return gap x
  148. )
  149. # The slider starts at left edge (typically a few px in). gap_x is
  150. # the absolute x of the gap. Drag distance = gap_x - slider_start_x.
  151. drag_distance = int(gap_x) - start_x + int(box["x"]) # gap_x is page-relative
  152. if drag_distance <= 0:
  153. drag_distance = 50 # sane fallback
  154. log.info("jfbym_gap_calculated", gap_x=int(gap_x), drag=drag_distance)
  155. # 4) Realistic mouse drag using bezier + variable speed
  156. trajectory = human_drag(start_x, start_y, start_x + drag_distance, start_y)
  157. log.info("mouse_drag_start", points=len(trajectory), total_dx=drag_distance)
  158. await page.mouse.move(start_x, start_y)
  159. await page.mouse.down()
  160. for x, y, dt in trajectory:
  161. await page.mouse.move(x, y)
  162. await asyncio.sleep(dt / 1000.0)
  163. await page.mouse.up()
  164. log.info("mouse_drag_done")
  165. # 5) Wait for geetest verification (network round trip to gcaptcha4)
  166. # The seccode gets stored in window.captchaObj or similar
  167. await asyncio.sleep(3.5) # generous — geetest verify can be 1-3s
  168. # 6) Extract seccode from the page's JS context
  169. seccode_json = await page.evaluate("""() => {
  170. // geetest v4 stores result in window.captchaObj.captchaResult
  171. // or accessible via the iframe's contentWindow
  172. if (window.captchaObj && window.captchaObj.captchaResult) {
  173. return JSON.stringify(window.captchaObj.captchaResult);
  174. }
  175. const ifr = document.querySelector('iframe[src*="geetest"]')
  176. || document.querySelector('#geetest-iframe')
  177. || document.querySelector('iframe');
  178. if (ifr && ifr.contentWindow) {
  179. try {
  180. if (ifr.contentWindow.captchaObj) {
  181. return JSON.stringify(ifr.contentWindow.captchaObj);
  182. }
  183. } catch (e) { return JSON.stringify({error: e.message}); }
  184. }
  185. return JSON.stringify({error: 'captchaObj not found'});
  186. }""")
  187. log.info("seccode_extracted", preview=seccode_json[:200])
  188. seccode = json.loads(seccode_json)
  189. if "captcha_output" not in seccode:
  190. raise RuntimeError(f"failed to extract seccode: {seccode}")
  191. # 7) Close browser (don't need it anymore)
  192. await browser.close()
  193. # 8) POST to 996box byPwd with extracted captchaOutput
  194. captcha_payload = {
  195. "captchaId": seccode["captcha_id"],
  196. "captchaOutput": seccode["captcha_output"],
  197. "genTime": seccode["gen_time"],
  198. "lotNumber": seccode["lot_number"],
  199. "passToken": seccode["pass_token"],
  200. }
  201. captcha_b64 = base64.b64encode(
  202. json.dumps(captcha_payload, separators=(",", ":")).encode()
  203. ).decode()
  204. log.info("calling_996box_bypwd", captcha_id=seccode["captcha_id"])
  205. from cq2_sdk.login.box_996 import Box996Login
  206. box = Box996Login(
  207. geetest=None, device_id="00:15:5d:0a:04:00",
  208. )
  209. session = await box.login_with_password(
  210. username, password,
  211. captcha={
  212. "loginInfo": "",
  213. "channel_id": "",
  214. "game_id": "",
  215. "inviter_uid": "",
  216. "needConfirmLogin": True,
  217. "phone": "",
  218. "code": "",
  219. "_captcha_": captcha_b64,
  220. },
  221. )
  222. log.info("login_success", uid=session.uid)
  223. return session.token
  224. async def main_async() -> int:
  225. ap = argparse.ArgumentParser()
  226. ap.add_argument("--user", default=os.environ.get("CQ2_ACCOUNT"))
  227. ap.add_argument("--password", default=os.environ.get("CQ2_PASSWORD"))
  228. ap.add_argument("--jfbym-token", default=os.environ.get("CQ2_JFBYM_TOKEN"))
  229. ap.add_argument("--check", action="store_true",
  230. help="just verify environment (no browser launch)")
  231. args = ap.parse_args()
  232. log = DebugLogger(extra={"task": "cdp_a1_login", "user": args.user})
  233. if args.check:
  234. ok = True
  235. print("[check] playwright:", end=" ")
  236. try:
  237. from playwright.async_api import async_playwright
  238. print("OK")
  239. except ImportError:
  240. print("MISSING")
  241. ok = False
  242. print("[check] Edge binary:", end=" ")
  243. from pathlib import Path
  244. print("OK" if Path(EDGE).exists() else "MISSING")
  245. print("[check] jfbym token:", end=" ")
  246. print("OK" if args.jfbym_token else "MISSING (set CQ2_JFBYM_TOKEN)")
  247. return 0 if ok else 1
  248. if not all([args.user, args.password, args.jfbym_token]):
  249. print("ERROR: missing --user / --password / --jfbym-token")
  250. return 1
  251. try:
  252. token = await solve_captcha_and_login(
  253. args.user, args.password, args.jfbym_token, log,
  254. )
  255. except Exception as e:
  256. log.error("a1_failed", error=str(e)[:200])
  257. print(f"[FAIL] {type(e).__name__}: {e}")
  258. return 1
  259. # Persist
  260. with connection() as conn:
  261. u = UserStore(conn).get_by_username(args.user)
  262. if u is None:
  263. print(f"[FAIL] user {args.user!r} not in MySQL (run scaffold first)")
  264. return 1
  265. UserStore(conn).update_token(user_id=u.id, token=token)
  266. print(f"\n[OK] A1 password login via real browser + jfbym succeeded")
  267. print(f" user = {args.user}")
  268. print(f" token = {token[:30]}...")
  269. print(f" saved to MySQL users.id={u.id}")
  270. return 0
  271. if __name__ == "__main__":
  272. sys.exit(asyncio.run(main_async()))