mycaptcha / src /services /turnstile.py
dragg2's picture
Upload 33 files
c00180e verified
"""Cloudflare Turnstile solver using Playwright browser automation.
Supports TurnstileTaskProxyless and TurnstileTaskProxylessM1 task types.
Visits the target page, interacts with the Turnstile widget, and extracts the token.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any
from playwright.async_api import Browser, Playwright, async_playwright
from ..core.config import Config
log = logging.getLogger(__name__)
_STEALTH_JS = """
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
window.chrome = {runtime: {}, loadTimes: () => {}, csi: () => {}};
"""
_EXTRACT_TURNSTILE_TOKEN_JS = """
() => {
// Check for Turnstile response input
const input = document.querySelector('[name="cf-turnstile-response"]')
|| document.querySelector('input[name*="turnstile"]');
if (input && input.value && input.value.length > 20) {
return input.value;
}
// Try the turnstile API
if (window.turnstile && typeof window.turnstile.getResponse === 'function') {
const resp = window.turnstile.getResponse();
if (resp && resp.length > 20) return resp;
}
return null;
}
"""
class TurnstileSolver:
"""Solves Cloudflare Turnstile tasks via headless Chromium."""
def __init__(self, config: Config, browser: Browser | None = None) -> None:
self._config = config
self._playwright: Playwright | None = None
self._browser: Browser | None = browser
self._owns_browser = browser is None
self._start_lock = asyncio.Lock()
async def start(self) -> None:
if self._browser is not None:
return
async with self._start_lock:
if self._browser is not None:
return
playwright = await async_playwright().start()
try:
browser = await playwright.chromium.launch(
headless=self._config.browser_headless,
args=[
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
],
)
except Exception:
await playwright.stop()
raise
self._playwright = playwright
self._browser = browser
log.info("TurnstileSolver browser started lazily")
async def stop(self) -> None:
async with self._start_lock:
if self._owns_browser:
if self._browser:
await self._browser.close()
self._browser = None
if self._playwright:
await self._playwright.stop()
self._playwright = None
log.info("TurnstileSolver stopped")
async def solve(self, params: dict[str, Any]) -> dict[str, Any]:
await self.start()
website_url = params["websiteURL"]
website_key = params["websiteKey"]
last_error: Exception | None = None
for attempt in range(self._config.captcha_retries):
try:
token = await self._solve_once(website_url, website_key)
return {"token": token}
except Exception as exc:
last_error = exc
log.warning(
"Turnstile attempt %d/%d failed: %s",
attempt + 1,
self._config.captcha_retries,
exc,
)
if attempt < self._config.captcha_retries - 1:
await asyncio.sleep(2)
raise RuntimeError(
f"Turnstile failed after {self._config.captcha_retries} attempts: {last_error}"
)
async def _solve_once(self, website_url: str, website_key: str) -> str:
assert self._browser is not None
context = await self._browser.new_context(
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
),
viewport={"width": 1920, "height": 1080},
locale="en-US",
)
page = await context.new_page()
await page.add_init_script(_STEALTH_JS)
try:
timeout_ms = self._config.browser_timeout * 1000
await page.goto(website_url, wait_until="networkidle", timeout=timeout_ms)
await page.mouse.move(400, 300)
await asyncio.sleep(1)
# Try clicking the Turnstile checkbox
try:
iframe_element = page.frame_locator(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
)
checkbox = iframe_element.locator(
'input[type="checkbox"], .ctp-checkbox-label, label'
)
await checkbox.click(timeout=8_000)
except Exception:
log.info("No Turnstile checkbox found, waiting for auto-solve")
# Wait for the token to appear
for _ in range(15):
await asyncio.sleep(2)
token = await page.evaluate(_EXTRACT_TURNSTILE_TOKEN_JS)
if token:
log.info("Got Turnstile token (len=%d)", len(token))
return token
raise RuntimeError("Turnstile token not obtained within timeout")
finally:
await context.close()