cdp_puzzle_solve.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. """Solve the geetest v4 PUZZLE slider properly:
  2. 1. Screenshot the captcha widget region
  3. 2. Pixel-analyze to find the puzzle GAP (the notch where the piece fits)
  4. 3. Drag the slider to EXACTLY that x-offset (not a fixed distance)
  5. 4. Human-like trajectory
  6. Then click login and monitor for success + game launch.
  7. """
  8. import asyncio
  9. import base64
  10. import io
  11. import json
  12. import math
  13. import subprocess
  14. import sys
  15. import httpx
  16. from PIL import Image
  17. import websockets
  18. USERNAME = "longwu988"
  19. PASSWORD = "Longwu1988"
  20. CDP = "http://127.0.0.1:9222"
  21. async def get_targets():
  22. async with httpx.AsyncClient(timeout=5) as c:
  23. r = await c.get(f"{CDP}/json/list")
  24. return r.json()
  25. class Page:
  26. def __init__(self, ws):
  27. self.ws = ws
  28. self.mid = 0
  29. self.events = []
  30. async def call(self, method, params=None):
  31. self.mid += 1
  32. await self.ws.send(json.dumps({"id": self.mid, "method": method, "params": params or {}}))
  33. while True:
  34. msg = json.loads(await self.ws.recv())
  35. if msg.get("id") == self.mid:
  36. return msg.get("result", {})
  37. elif "method" in msg:
  38. self.events.append(msg)
  39. async def eval(self, expr):
  40. r = await self.call("Runtime.evaluate", {"expression": expr, "returnByValue": True})
  41. return r.get("result", {}).get("value")
  42. async def mouse(self, typ, x, y):
  43. params = {"type": typ, "x": x, "y": y, "button": "left"}
  44. if typ == "mousePressed":
  45. params["clickCount"] = 1
  46. params["buttons"] = 1
  47. if typ == "mouseReleased":
  48. params["clickCount"] = 1
  49. params["buttons"] = 0
  50. await self.call("Input.dispatchMouseEvent", params)
  51. async def screenshot(self) -> Image.Image | None:
  52. r = await self.call("Page.captureScreenshot", {"format": "png"})
  53. if r.get("data"):
  54. return Image.open(io.BytesIO(base64.b64decode(r["data"])))
  55. return None
  56. def find_gap_x(img: Image.Image, widget: dict) -> int | None:
  57. """Find the puzzle gap x-position in the widget screenshot.
  58. The gap is a rectangular notch with distinctly different brightness
  59. from the surrounding image. Scan the image area (top 60% of widget)
  60. column-by-column for a vertical brightness discontinuity.
  61. """
  62. # widget coords are in page space; screenshot is full page
  63. x0, y0 = int(widget["x"]), int(widget["y"])
  64. w, h = int(widget["w"]), int(widget["h"])
  65. # the puzzle image is typically the upper portion
  66. img_y0 = y0 + 40 # skip header
  67. img_y1 = y0 + int(h * 0.55)
  68. img_x0 = x0 + 5
  69. img_x1 = x0 + w - 5
  70. if img_x1 <= img_x0 or img_y1 <= img_y0:
  71. return None
  72. crop = img.crop((img_x0, img_y0, img_x1, img_y1)).convert("RGB")
  73. cw, ch = crop.size
  74. px = crop.load()
  75. # Column brightness signature
  76. col_avg = []
  77. for x in range(cw):
  78. s = 0
  79. for y in range(0, ch, 2):
  80. r, g, b = px[x, y]
  81. s += (r + g + b) / 3
  82. col_avg.append(s / max(1, ch // 2))
  83. # The gap: several consecutive columns much darker (or brighter) than neighbors.
  84. # Find the strongest left-edge discontinuity in the middle 80% of the image
  85. best_x = None
  86. best_score = 0
  87. for x in range(int(cw * 0.15), int(cw * 0.9)):
  88. left_avg = sum(col_avg[max(0, x - 6) : x]) / 6
  89. right_avg = sum(col_avg[x : x + 6]) / 6
  90. diff = abs(left_avg - right_avg)
  91. if diff > best_score:
  92. best_score = diff
  93. best_x = x
  94. if best_x is None or best_score < 8: # too subtle
  95. return None
  96. # gap position in PAGE coordinates
  97. return img_x0 + best_x
  98. def cq2_pid():
  99. try:
  100. out = subprocess.run(
  101. ["powershell", "-NoProfile", "-Command",
  102. "Get-Process cq2 -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Id"],
  103. capture_output=True, text=True, timeout=5)
  104. return out.stdout.strip() or None
  105. except Exception:
  106. return None
  107. def gateway_conns():
  108. try:
  109. out = subprocess.run(
  110. ["powershell", "-NoProfile", "-Command",
  111. "(netstat -ano | Select-String '47.118.|:12000' | Measure-Object).Count"],
  112. capture_output=True, text=True, timeout=5)
  113. return int(out.stdout.strip() or 0)
  114. except Exception:
  115. return 0
  116. async def main():
  117. targets = await get_targets()
  118. login = next((t for t in targets if "login" in t.get("url", "") and t["type"] == "page"), None)
  119. if not login:
  120. print("no login page")
  121. for t in targets:
  122. print(f" [{t['type']}] {t['url'][:70]}")
  123. return 1
  124. async with websockets.connect(login["webSocketDebuggerUrl"], max_size=10**7) as ws:
  125. page = Page(ws)
  126. await page.call("Network.enable")
  127. # 1. ensure credentials
  128. await page.eval(f"""
  129. (function() {{
  130. const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
  131. const u = document.querySelector('#username');
  132. const p = document.querySelector('#password');
  133. if (u && !(u.value||'').length) {{ set.call(u, '{USERNAME}'); u.dispatchEvent(new Event('input', {{bubbles:true}})); }}
  134. if (p && !(p.value||'').length) {{ set.call(p, '{PASSWORD}'); p.dispatchEvent(new Event('input', {{bubbles:true}})); }}
  135. return 'ok';
  136. }})()
  137. """)
  138. # 2. click login
  139. print("clicking login...")
  140. await page.eval("""
  141. (function() {
  142. const btn = document.querySelector('button.ant-btn.login-btn, button.login-btn, button.ant-btn');
  143. if (btn) btn.click();
  144. })()
  145. """)
  146. await asyncio.sleep(2.5)
  147. # 3. captcha loop (up to 5 attempts)
  148. for attempt in range(5):
  149. # is captcha visible?
  150. gt = await page.eval("""
  151. (function() {
  152. const g = document.querySelector('.geetest_box, [class*=geetest_panel]');
  153. if (!g || g.offsetParent === null) return null;
  154. const r = g.getBoundingClientRect();
  155. const slider = g.querySelector('[class*=slider_button], .geetest_btn_drag, [class*=btn_drag], .geetest_btn');
  156. const sr = slider ? slider.getBoundingClientRect() : null;
  157. const state = (g.innerText||'').slice(0,100);
  158. return JSON.stringify({x:r.x, y:r.y, w:r.width, h:r.height,
  159. sx: sr?sr.x+sr.width/2:null, sy: sr?sr.y+sr.height/2:null,
  160. state});
  161. })()
  162. """)
  163. if not gt:
  164. print(f"attempt {attempt+1}: captcha not visible")
  165. # maybe logged in already?
  166. url = await page.eval("location.href")
  167. if "login" not in (url or ""):
  168. print(f"*** NAVIGATED AWAY: {url} — LOGIN SUCCESS ***")
  169. break
  170. break
  171. g = json.loads(gt)
  172. print(f"attempt {attempt+1}: captcha visible {g['w']:.0f}x{g['h']:.0f}")
  173. print(f" state: {g.get('state','')[:60]}")
  174. if "验证失败" in g.get("state", "") or "请重新" in g.get("state", ""):
  175. print(" previous attempt failed; refreshing captcha...")
  176. await page.eval("""
  177. (function() {
  178. const r = document.querySelector('[class*=refresh], .geetest_refresh, [class*=reset]');
  179. if (r) r.click();
  180. })()
  181. """)
  182. await asyncio.sleep(1.5)
  183. # 4. screenshot & find the gap
  184. shot = await page.screenshot()
  185. if shot is None:
  186. print(" screenshot failed")
  187. continue
  188. shot.save(f"captcha_attempt{attempt+1}.png")
  189. gap_x = find_gap_x(shot, g)
  190. if gap_x is None:
  191. print(" gap not found — trying center heuristic")
  192. gap_x = int(g["x"] + g["w"] * 0.6)
  193. print(f" gap at page-x={gap_x}")
  194. # 5. slider position
  195. if g.get("sx") is None:
  196. print(" slider handle not found")
  197. continue
  198. sx, sy = g["sx"], g["sy"]
  199. dist = gap_x - sx + 12 # small overshoot correction
  200. print(f" slider at ({sx:.0f},{sy:.0f}), drag dist={dist:.0f}px")
  201. # 6. human-like drag
  202. await page.mouse("mouseMoved", sx, sy)
  203. await asyncio.sleep(0.12)
  204. await page.mouse("mousePressed", sx, sy)
  205. await asyncio.sleep(0.08)
  206. steps = 45
  207. for i in range(1, steps + 1):
  208. progress = i / steps
  209. eased = 1 - (1 - progress) ** 2.4
  210. cx = sx + dist * eased
  211. cy = sy + math.sin(i * 0.6) * 2.0
  212. await page.mouse("mouseMoved", cx, cy)
  213. await asyncio.sleep(0.024)
  214. await asyncio.sleep(0.10)
  215. # tiny settle-back (real hands overshoot then correct)
  216. back = 3
  217. await page.mouse("mouseMoved", sx + dist - back, sy + 1)
  218. await asyncio.sleep(0.08)
  219. await page.mouse("mouseReleased", sx + dist - back, sy + 1)
  220. print(" drag complete")
  221. await asyncio.sleep(3.5)
  222. # 7. check outcome
  223. url = await page.eval("location.href")
  224. if "login" not in (url or ""):
  225. print(f"\n*** LOGIN SUCCESS! url={url[:60]} ***")
  226. break
  227. state2 = await page.eval("""
  228. (document.querySelector('.geetest_box')||{innerText:''}).innerText.slice(0,80)
  229. """)
  230. print(f" post-drag captcha state: {state2}")
  231. if "验证成功" in (state2 or "") or "成功" in (state2 or ""):
  232. print(" captcha SOLVED, waiting for redirect...")
  233. await asyncio.sleep(4)
  234. url2 = await page.eval("location.href")
  235. if "login" not in (url2 or ""):
  236. print(f"*** LOGIN SUCCESS! url={url2[:60]} ***")
  237. break
  238. # 8. if logged in: monitor for game launch
  239. url = await page.eval("location.href")
  240. if "login" not in (url or ""):
  241. print("\n=== monitoring 90s for game launch + traffic ===")
  242. for i in range(30):
  243. await asyncio.sleep(3)
  244. pid = cq2_pid()
  245. conns = gateway_conns()
  246. t = (i + 1) * 3
  247. flags = []
  248. if pid:
  249. flags.append(f"CQ2={pid}")
  250. if conns:
  251. flags.append(f"conns={conns}")
  252. print(f" t+{t:>3}s {' '.join(flags)}")
  253. if pid and conns:
  254. print("\n*** REAL TRAFFIC — capture is recording ***")
  255. await asyncio.sleep(25)
  256. return 0
  257. else:
  258. print("\nlogin did not complete")
  259. return 0
  260. sys.exit(asyncio.run(main()))