mirror of
https://github.com/ThePhaseless/Byparr.git
synced 2026-09-24 22:03:30 +01:00
The widget is reachable but the click leaves it unchecked, so compare locator.click, force, check, label, and a humanized page.mouse press on fresh pages and report which ticks the box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
"""Find a click Cloudflare's checkbox actually accepts.
|
|
|
|
The widget is reachable now, but `Checkbox clicked successfully` leaves it
|
|
`checked=False`, so the click is not registering as a user gesture. This tries
|
|
each candidate in turn against a fresh page and reports which one ticks the box
|
|
and which one clears the challenge.
|
|
|
|
docker run --rm -e PYTHONPATH=/app -e TRACE_URL=https://extratorrent.st/ \\
|
|
-v "$PWD/out:/out" byparr-test uv run python debug/click_modes.py
|
|
|
|
MODES defaults to every strategy; set MODES=force,label to narrow it.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import pathlib
|
|
import time
|
|
|
|
from src.utils import get_browser
|
|
|
|
URL = os.environ.get("TRACE_URL", "https://extratorrent.st/")
|
|
MODES = os.environ.get(
|
|
"MODES", "locator,force,check,label,mouse,widget_centre"
|
|
).split(",")
|
|
SETTLE = int(os.environ.get("SETTLE", "45"))
|
|
OUT = pathlib.Path(os.environ.get("DIAG_OUT", "/out"))
|
|
|
|
|
|
def log(*a: object) -> None:
|
|
"""Print immediately."""
|
|
print(*a, flush=True)
|
|
|
|
|
|
def cf_frame(page):
|
|
"""The turnstile widget frame."""
|
|
for frame in page.frames:
|
|
if "challenges.cloudflare.com" in frame.url and not frame.is_detached():
|
|
return frame
|
|
return None
|
|
|
|
|
|
async def wait_for_widget(page, seconds: int = 30):
|
|
"""Wait until the checkbox is visible, returning (frame, locator)."""
|
|
for _ in range(seconds * 2):
|
|
frame = cf_frame(page)
|
|
if frame is not None:
|
|
try:
|
|
box = frame.locator('input[type="checkbox"]')
|
|
if await box.count() and await box.first.is_visible():
|
|
return frame, box.first
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
await asyncio.sleep(0.5)
|
|
return None, None
|
|
|
|
|
|
async def do_click(page, frame, box, mode: str) -> str:
|
|
"""Perform one click strategy."""
|
|
if mode == "locator":
|
|
await box.click(timeout=10_000)
|
|
return "locator.click()"
|
|
if mode == "force":
|
|
await box.click(timeout=10_000, force=True)
|
|
return "locator.click(force=True)"
|
|
if mode == "check":
|
|
await box.check(timeout=10_000)
|
|
return "locator.check()"
|
|
if mode == "label":
|
|
label = frame.locator("label")
|
|
if await label.count():
|
|
await label.first.click(timeout=10_000)
|
|
return "label.click()"
|
|
return "no label present"
|
|
if mode in {"mouse", "widget_centre"}:
|
|
rect = await box.bounding_box()
|
|
if rect is None:
|
|
return "no bounding box"
|
|
x = rect["x"] + rect["width"] / 2
|
|
y = rect["y"] + rect["height"] / 2
|
|
# Approach first: a cursor that teleports is itself a signal.
|
|
await page.mouse.move(x - 180, y - 120)
|
|
await asyncio.sleep(0.3)
|
|
await page.mouse.move(x - 40, y - 20, steps=18)
|
|
await asyncio.sleep(0.2)
|
|
await page.mouse.move(x, y, steps=10)
|
|
await asyncio.sleep(0.35)
|
|
await page.mouse.down()
|
|
await asyncio.sleep(0.07)
|
|
await page.mouse.up()
|
|
return f"page.mouse at ({x:.0f}, {y:.0f})"
|
|
return f"unknown mode {mode}"
|
|
|
|
|
|
async def try_mode(mode: str) -> None:
|
|
"""One fresh browser, one strategy, one verdict."""
|
|
log(f"\n===== mode={mode} =====")
|
|
async for dep in get_browser():
|
|
page = dep.page
|
|
await page.goto(URL, timeout=60_000)
|
|
await page.wait_for_load_state("domcontentloaded", timeout=30_000)
|
|
|
|
# Is our shadow-root patch even surviving the page's CSP?
|
|
try:
|
|
src = await page.evaluate("() => Element.prototype.attachShadow.toString()")
|
|
flag = await page.evaluate("() => '_shadowRootPatched' in window")
|
|
log(f" on challenge page: native={'[native code]' in src} flag={flag}")
|
|
except Exception as exc: # noqa: BLE001
|
|
log(f" tamper probe blocked: {str(exc)[:70]}")
|
|
|
|
frame, box = await wait_for_widget(page)
|
|
if box is None:
|
|
log(" checkbox never became visible")
|
|
return
|
|
|
|
log(f" before: checked={await box.is_checked()}")
|
|
try:
|
|
what = await do_click(page, frame, box, mode)
|
|
log(f" clicked via {what}")
|
|
except Exception as exc: # noqa: BLE001
|
|
log(f" click raised: {str(exc)[:120]}")
|
|
return
|
|
|
|
start = time.perf_counter()
|
|
for _ in range(SETTLE // 3):
|
|
await asyncio.sleep(3)
|
|
elapsed = time.perf_counter() - start
|
|
title = await page.title()
|
|
checked = None
|
|
frame_now = cf_frame(page)
|
|
if frame_now is not None:
|
|
try:
|
|
b = frame_now.locator('input[type="checkbox"]')
|
|
checked = await b.first.is_checked() if await b.count() else None
|
|
except Exception: # noqa: BLE001
|
|
checked = "unreadable"
|
|
log(f" +{elapsed:3.0f}s checked={checked} title={title!r}")
|
|
if "oment" not in title and "ierpliwo" not in title:
|
|
log(f" >>> CLEARED by {mode}")
|
|
await page.screenshot(path=str(OUT / f"cleared-{mode}.png"))
|
|
return
|
|
await page.screenshot(path=str(OUT / f"stuck-{mode}.png"))
|
|
log(f" {mode}: still challenged")
|
|
|
|
|
|
async def main() -> None:
|
|
"""Try each strategy on its own fresh browser."""
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
log(f"### {URL} modes={MODES}")
|
|
for mode in MODES:
|
|
try:
|
|
await try_mode(mode.strip())
|
|
except Exception as exc: # noqa: BLE001
|
|
log(f" mode {mode} blew up: {str(exc)[:150]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|