131131
132132_CLOUDFLARE_CHALLENGE_DOMAIN = 'challenges.cloudflare.com'
133133_CLOUDFLARE_IFRAME_SELECTOR = f'iframe[src*="{ _CLOUDFLARE_CHALLENGE_DOMAIN } "]'
134- _CLOUDFLARE_CHECKBOX_SELECTOR = 'span.cb-i '
134+ _CLOUDFLARE_CHECKBOX_SELECTOR = 'input[type="checkbox"] '
135135
136136
137137class Tab (FindElementsMixin ):
@@ -1966,36 +1966,36 @@ def on_loaded(_: dict):
19661966 with contextlib .suppress (Exception ):
19671967 await self .disable_page_events ()
19681968
1969- async def _find_cloudflare_shadow_root (self , timeout : float ) -> ShadowRoot :
1970- """Poll for the Cloudflare Turnstile shadow root.
1969+ async def _find_cloudflare_shadow_root (self ) -> Optional [ ShadowRoot ] :
1970+ """Return the Cloudflare Turnstile shadow root if currently present .
19711971
1972- Repeatedly calls ``find_shadow_roots(deep=False)`` and checks each
1973- shadow root's ``inner_html`` for the Cloudflare challenge domain.
1974-
1975- Args:
1976- timeout: Maximum seconds to wait for the shadow root.
1977-
1978- Returns:
1979- The first ShadowRoot whose inner HTML contains
1980- ``challenges.cloudflare.com``.
1981-
1982- Raises:
1983- WaitElementTimeout: If no matching shadow root is found within
1984- *timeout* seconds.
1972+ Performs a single scan of the page's shadow roots and returns the first
1973+ one whose ``inner_html`` references ``challenges.cloudflare.com``, or
1974+ ``None`` when the challenge widget has not been injected yet.
19851975 """
1986- start_time = asyncio .get_event_loop ().time ()
1987- while True :
1988- shadow_roots = await self .find_shadow_roots (deep = False )
1989- for sr in shadow_roots :
1990- html = await sr .inner_html
1991- if _CLOUDFLARE_CHALLENGE_DOMAIN in html :
1992- return sr
1976+ for shadow_root in await self .find_shadow_roots (deep = False ):
1977+ with contextlib .suppress (Exception ):
1978+ if _CLOUDFLARE_CHALLENGE_DOMAIN in await shadow_root .inner_html :
1979+ return shadow_root
1980+ return None
19931981
1994- if asyncio .get_event_loop ().time () - start_time > timeout :
1995- raise WaitElementTimeout (
1996- f'Timed out after { timeout } s waiting for Cloudflare Turnstile shadow root'
1997- )
1998- await asyncio .sleep (0.5 )
1982+ @staticmethod
1983+ async def _click_cloudflare_checkbox (shadow_root : ShadowRoot ) -> None :
1984+ """Traverse the Turnstile widget and click its verification checkbox.
1985+
1986+ Navigates shadow root -> challenge iframe -> body -> inner shadow root
1987+ and clicks the ``input[type="checkbox"]`` element. Every step fails
1988+ fast (``timeout=0``): any node captured here can go stale while
1989+ Cloudflare re-renders the iframe, and polling locally on a stale node
1990+ both wastes time and can let four sequential waits overrun the caller's
1991+ deadline. Failing fast lets ``_bypass_cloudflare`` restart the whole
1992+ traversal from the top on its next poll.
1993+ """
1994+ iframe = await shadow_root .query (_CLOUDFLARE_IFRAME_SELECTOR , timeout = 0 )
1995+ body = await iframe .find (tag_name = 'body' , timeout = 0 )
1996+ inner_shadow = await body .get_shadow_root (timeout = 0 )
1997+ checkbox = await inner_shadow .query (_CLOUDFLARE_CHECKBOX_SELECTOR , timeout = 0 )
1998+ await checkbox .click ()
19991999
20002000 async def _bypass_cloudflare (
20012001 self ,
@@ -2004,21 +2004,31 @@ async def _bypass_cloudflare(
20042004 ) -> None :
20052005 """Attempt to bypass Cloudflare Turnstile captcha via shadow root traversal.
20062006
2007- Traverses shadow roots to locate the Cloudflare iframe, navigates into
2008- it, and clicks the actual checkbox element (``span.cb-i``).
2007+ Polls for the challenge widget and clicks its checkbox, retrying the
2008+ whole traversal until *time_to_wait_captcha* elapses. Retrying is
2009+ required because Cloudflare injects the widget after the load event and
2010+ re-renders the challenge iframe during its proof-of-work, which
2011+ invalidates any node captured mid-traversal.
20092012 """
2010- try :
2011- timeout_int = int (time_to_wait_captcha )
2012- shadow_root = await self ._find_cloudflare_shadow_root (
2013- timeout = time_to_wait_captcha ,
2014- )
2015- iframe = await shadow_root .query (_CLOUDFLARE_IFRAME_SELECTOR , timeout = timeout_int )
2016- body = await iframe .find (tag_name = 'body' , timeout = timeout_int )
2017- inner_shadow = await body .get_shadow_root (timeout = time_to_wait_captcha )
2018- checkbox = await inner_shadow .query (_CLOUDFLARE_CHECKBOX_SELECTOR , timeout = timeout_int )
2019- await checkbox .click ()
2020- except Exception as exc :
2021- logger .error (f'Error in cloudflare bypass: { exc } ' )
2013+ loop = asyncio .get_event_loop ()
2014+ deadline = loop .time () + time_to_wait_captcha
2015+ last_error : Optional [Exception ] = None
2016+ while True :
2017+ try :
2018+ shadow_root = await self ._find_cloudflare_shadow_root ()
2019+ if shadow_root is not None :
2020+ await self ._click_cloudflare_checkbox (shadow_root )
2021+ return
2022+ except Exception as exc :
2023+ last_error = exc
2024+ logger .debug (f'Cloudflare bypass attempt failed, retrying: { exc } ' )
2025+
2026+ if loop .time () >= deadline :
2027+ break
2028+ await asyncio .sleep (0.5 )
2029+
2030+ if last_error is not None :
2031+ logger .error (f'Error in cloudflare bypass: { last_error } ' )
20222032
20232033
20242034class _DownloadHandle :
0 commit comments