193 lines
7.4 KiB
Python
193 lines
7.4 KiB
Python
"""
|
|
<input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-kda53_response">
|
|
|
|
<div class="footer" role="contentinfo">
|
|
<div class="footer-inner">
|
|
<div class="footer-wrapper">
|
|
<div>
|
|
<div class="ray-id">Ray ID: <code>a3ea73c2a817983b</code>
|
|
</div>
|
|
</div>
|
|
<div class="footer-link-wrapper">
|
|
<span class="footer-text">Производительность и безопасность с <a rel="noopener noreferrer" href="https://www.cloudflare.com?utm_source=challenge&utm_campaign=m" target="_blank" aria-label="Cloudflare, открывается в новой вкладке">Cloudflare</a>
|
|
</span>
|
|
<span class="footer-divider"></span>
|
|
<a target="_blank" rel="noopener noreferrer" href="https://www.cloudflare.com/privacypolicy/" aria-label="Конфиденциальность, открывается в новой вкладке" class="footer-text">Конфиденциальность</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
//input[@name="cf-turnstile-response"]
|
|
//footer/footer-inner/footer-wrapper/div[@class="ray-id"]
|
|
|
|
iFrame with checkbox
|
|
|
|
<div class="main-wrapper RUWFy0" role="main">
|
|
<div class="main-content">
|
|
<div class="lpdkQ3">
|
|
<img src="/favicon.ico" class="uMaz2" alt="Значок zkillboard.com">
|
|
<h1>zkillboard.com</h1>
|
|
</div>
|
|
<h2 id="jddkS1" class="YNaX0 MmgY4">Выполнение проверки безопасности</h2>
|
|
<p id="QeINV1" class="nnhvx2 ndZkd3">Этот веб-сайт использует сервис безопасности для защиты от вредоносных ботов. Эта страница отображается, пока веб-сайт проверяет, что вы не бот.</p>
|
|
<div id="lVJB5" style="display: grid;">
|
|
<div>
|
|
<div>
|
|
<div></div>
|
|
<input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-2t1qg_response">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="CJPCL0" style="display: none;">
|
|
<h2 id="zfpQF7" class="ndZkd3">Проверка выполнена успешно. Ожидание ответа zkillboard.com</h2>
|
|
</div>
|
|
<div id="EaAgR8" class="Wivzc8 aLvno9" style="display: none; visibility: hidden;">
|
|
<div class="TighH9">
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
</div>
|
|
</div>
|
|
<noscript>
|
|
<div class="h2">
|
|
<span id="challenge-error-text">Enable JavaScript and cookies to continue</span>
|
|
</div>
|
|
</noscript>
|
|
</div>
|
|
</div>
|
|
"""
|
|
import logging
|
|
import re
|
|
import time
|
|
from random import randint
|
|
|
|
from playwright.async_api import Page
|
|
|
|
from ..base import Detector, ChallengeInfo, AntiBotProvider, Solver
|
|
from ..base import Stage
|
|
from ..registry import register
|
|
from ..humanize import wait_for_page_stability, wait_for_networkidle
|
|
|
|
log = logging.getLogger(__name__)
|
|
log.setLevel(logging.DEBUG)
|
|
log.addHandler(logging.StreamHandler())
|
|
|
|
|
|
class CloudflareDetector(Detector):
|
|
|
|
# степени риска, которые Cloudflare сам проставляет в конфиге своего
|
|
# челлендж-скрипта на странице (см. `cType: '...'` в html/js) —
|
|
# по нему и определяем стадию, а не по факту наличия виджета на глаз
|
|
_CTYPES = ("non-interactive", "managed", "interactive")
|
|
|
|
async def detect(self, page: Page, info: ChallengeInfo):
|
|
log.debug("CloudflareDetector run")
|
|
await wait_for_page_stability(page, True, True)
|
|
|
|
info.meta = {}
|
|
widget_selector = "div.cf-turnstile[data-sitekey], div[data-sitekey].cf-turnstile"
|
|
marker_selector = info.detect_locator or "xpath=//input[@name='cf-turnstile-response']"
|
|
info.detect_locator = marker_selector
|
|
|
|
log.debug(f"ищем {marker_selector} успех если есть {info.success_locator}")
|
|
|
|
if not await page.query_selector(marker_selector):
|
|
log.debug("на странице нет маркера преграды")
|
|
if info.success_locator:
|
|
info.success = bool(await page.query_selector(info.success_locator))
|
|
info.meta["reason"] = [
|
|
"Нет маркера блокировки и нет маркера успеха :-(",
|
|
"Нет маркера блокировки зато есть маркер успеха :-)"
|
|
][info.success]
|
|
else:
|
|
info.success = True
|
|
info.meta["reason"] = "нет маркера блокировки"
|
|
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}")
|
|
|
|
widget = await page.query_selector(widget_selector)
|
|
|
|
if cf_type == "non-interactive":
|
|
# фулл-пейдж интерстишл без чекбокса — просто ждать, пока пропадёт
|
|
# или эскалирует в managed/interactive (тогда cf_type сменится на
|
|
# следующем detect())
|
|
info.stage = Stage.INTERSTITIAL
|
|
elif cf_type in ("managed", "interactive"):
|
|
# CF уже решил, что нужен клик по виджету внутри своего iframe
|
|
# (challenges.cloudflare.com) — сам клик этот проект не решает
|
|
info.stage = Stage.CHECKBOX
|
|
info.meta["requires_click"] = True
|
|
elif widget:
|
|
# обычный встроенный виджет на странице (без cType, просто
|
|
# <div class="cf-turnstile" data-sitekey="...">) — решаем через
|
|
# captcha_solver по токену, см. CloudflareSolver
|
|
info.stage = Stage.CHECKBOX
|
|
info.meta["container_selector"] = widget_selector
|
|
else:
|
|
info.stage = Stage.INTERSTITIAL
|
|
|
|
|
|
__CF_PATTERN__ = re.compile(r"^https://challenges\.cloudflare\.com/cdn-cgi/challenge-platform/.*")
|
|
|
|
|
|
class CloudflareSolver(Solver):
|
|
|
|
async def solve(self, page: Page, info: ChallengeInfo):
|
|
if info.stage == Stage.CHECKBOX:
|
|
# # Calculate the Captcha coordinates for any viewport
|
|
box = await page.locator("xpath=//input[@name='cf-turnstile-response']/../div").bounding_box()
|
|
captcha_x, captcha_y = box["x"] + randint(26, 28), box["y"] + randint(25, 27)
|
|
|
|
# # TODO: не кликает!
|
|
await page.mouse.click(captcha_x, captcha_y, delay=randint(100, 200), button="left")
|
|
|
|
await wait_for_networkidle(page)
|
|
|
|
if info.success_locator:
|
|
try:
|
|
await page.wait_for_selector(info.success_locator)
|
|
except:
|
|
pass
|
|
finally:
|
|
return
|
|
else:
|
|
deadline = time.time() + 10
|
|
while time.time() < deadline:
|
|
if not await page.query_selector(info.detect_locator):
|
|
return
|
|
return
|
|
elif info.stage == Stage.INTERSTITIAL:
|
|
if info.success_locator:
|
|
try:
|
|
await page.wait_for_selector(info.success_locator)
|
|
except Exception:
|
|
return
|
|
else:
|
|
return
|
|
else:
|
|
deadline = time.time() + 10
|
|
while time.time() < deadline:
|
|
if not await page.query_selector(info.detect_locator):
|
|
return
|
|
return
|
|
else:
|
|
captcha_solver = self._opt.get("captcha_solver")
|
|
if not captcha_solver:
|
|
raise RuntimeError("Нужен captcha_solver (2captcha) чтобы решить Turnstile")
|
|
await captcha_solver.solve(info.meta["container_selector"], page)
|
|
# TODO: возбудить js
|
|
|
|
|
|
@register
|
|
class ReCaptchaProvider(AntiBotProvider):
|
|
provider = 'cloudflare'
|
|
_solver_cls = CloudflareSolver
|
|
_detector_cls = CloudflareDetector
|