diff --git a/src/antibot/known_providers/cloudflare.py b/src/antibot/known_providers/cloudflare.py index 02b8c64..8ebe65c 100644 --- a/src/antibot/known_providers/cloudflare.py +++ b/src/antibot/known_providers/cloudflare.py @@ -84,7 +84,7 @@ class CloudflareDetector(Detector): async def detect(self, page: Page, info: ChallengeInfo): log.debug("CloudflareDetector run") - await wait_for_networkidle(page) + await wait_for_page_stability(page, True, False) log.debug("Страница загружена полностью") info.meta = {} @@ -94,7 +94,9 @@ class CloudflareDetector(Detector): log.debug(f"ищем {marker_selector} успех если есть {info.success_locator}") - if not await page.query_selector(marker_selector): + cf_type = await self._detect_cloudflare(page) + + if not cf_type: log.debug("на странице нет маркера преграды") if info.success_locator: info.success = bool(await page.query_selector(info.success_locator)) @@ -108,8 +110,6 @@ class CloudflareDetector(Detector): log.debug("Решение не требуется") return - content = await page.content() - cf_type = next((t for t in self._CTYPES if f"cType: '{t}'" in content), None) info.meta["cf_type"] = cf_type log.debug(f"cType со страницы: {cf_type!r}") @@ -134,6 +134,41 @@ class CloudflareDetector(Detector): else: info.stage = Stage.INTERSTITIAL + @staticmethod + async def _detect_cloudflare(page: Page) -> str | None: + """ + Detect the type of Cloudflare challenge present in the provided page content. + + This function analyzes the given page content to identify whether a specific + type of Cloudflare challenge is present. It checks for three predefined + challenge types: non-interactive, managed, and interactive. If a challenge + type is detected, it returns the corresponding type as a string. If no + challenge type is detected, it returns None. + + Args: + page_content (str): The content of the page to analyze for Cloudflare + challenge types. + + Returns: + str: A string representing the detected Cloudflare challenge type, if + found. Returns None if no challenge matches. + """ + challenge_types = ( + "non-interactive", + "managed", + "interactive", + ) + page_content = await page.content() + for ctype in challenge_types: + if f"cType: '{ctype}'" in page_content: + return ctype + + # Check if turnstile captcha is embedded inside the page (Usually inside a closed Shadow iframe) + if await page.query_selector('script[src*="challenges.cloudflare.com/turnstile/v"]'): + return "embedded" + + return None + __CF_PATTERN__ = re.compile(r"^https://challenges\.cloudflare\.com/cdn-cgi/challenge-platform/.*") @@ -170,6 +205,7 @@ class CloudflareSolver(Solver): while time.time() < deadline: if not await page.query_selector(info.detect_locator): return + await page.wait_for_timeout(1) return elif info.stage == Stage.INTERSTITIAL: if info.success_locator: @@ -184,6 +220,7 @@ class CloudflareSolver(Solver): while time.time() < deadline: if not await page.query_selector(info.detect_locator): return + await page.wait_for_timeout(1) return else: @@ -193,6 +230,20 @@ class CloudflareSolver(Solver): await captcha_solver.solve(info.meta["container_selector"], page) # TODO: возбудить js + @classmethod + def _challenge_cleared(cls, page_content: str, challenge_type: str) -> bool: + """ + Check whether the Cloudflare challenge is no longer present in the page content. + + Args: + page_content (str): The content of the page to analyze. + challenge_type (str): The challenge type returned by `_detect_cloudflare`. + + Returns: + bool: True if the challenge is gone from the page, False otherwise. + """ + return challenge_type == "embedded" or cls._detect_cloudflare(page_content) is None + @register class ReCaptchaProvider(AntiBotProvider):