fix: слоты, безопасность, единый формат ответа и валидация

- слоты браузеров: asyncio.Semaphore, размер из BROWSER_SLOTS, при нехватке — 503 + Retry-After;
  устранена утечка счётчика при отказе (#12, #49)
- session_id принимается только как UUID, путь профиля проверяется (#5)
- секреты клиента в логах маскируются, полный дамп — только на DEBUG (#6)
- ответ: status всегда "ok"/"err", поле code (no_slots / request_failed / challenge_failed /
  action_failed), response_model (#7)
- непройденный антибот → challenge_failed, действия не выполняются (#8)
- таймауты провайдеров в мс вынесены в константы; detect переживает навигацию посреди
  проверки; пауза между неудачными solve вместо холостого цикла (#9)
- обязательные поля действий и формат прокси проверяются схемой → 422 до запуска браузера (#13)
- прокси необязателен (#15)
- user_agent сохраняется в профиле сессии и реально передаётся в браузер (#16)
- duration_ms у действий и у запроса (#18), опция screenshot (#19)
- общий конфиг логирования (LOG_LEVEL), print → логгер, CloudflareProvider,
  удалён cached_persistent, GET /health (#22)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-13 14:52:12 +04:00
1 parent f58306cbb4
commit 437ea27be7
14 files changed
+360 -201

No files matched your search

+9 -8
View File
@@ -72,8 +72,6 @@ from ..registry import register
from ..humanize import wait_for_page_stability, wait_for_networkidle, click_human
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
log.addHandler(logging.StreamHandler())
class CloudflareDetector(Detector):
@@ -185,9 +183,12 @@ __CF_PATTERN__ = re.compile(r"^https://challenges\.cloudflare\.com/cdn-cgi/chall
class CloudflareSolver(Solver):
# опрос исчезновения маркера челленджа после клика / на interstitial
POLL_INTERVAL_MS = 250
WAIT_CLEARED_S = 10
async def solve(self, page: Page, info: ChallengeInfo):
print("start solving")
log.debug("start solving")
iframe = page.frame(url=__CF_PATTERN__)
if iframe:
await page.wait_for_timeout(randint(500, 1000))
@@ -212,11 +213,11 @@ class CloudflareSolver(Solver):
finally:
return
else:
deadline = time.time() + 10
deadline = time.time() + self.WAIT_CLEARED_S
while time.time() < deadline:
if not await page.query_selector(info.detect_locator):
return
await page.wait_for_timeout(1)
await page.wait_for_timeout(self.POLL_INTERVAL_MS)
return
elif info.stage == Stage.INTERSTITIAL:
if info.success_locator:
@@ -227,11 +228,11 @@ class CloudflareSolver(Solver):
else:
return
else:
deadline = time.time() + 10
deadline = time.time() + self.WAIT_CLEARED_S
while time.time() < deadline:
if not await page.query_selector(info.detect_locator):
return
await page.wait_for_timeout(1)
await page.wait_for_timeout(self.POLL_INTERVAL_MS)
return
else:
@@ -257,7 +258,7 @@ class CloudflareSolver(Solver):
@register
class ReCaptchaProvider(AntiBotProvider):
class CloudflareProvider(AntiBotProvider):
provider = 'cloudflare'
_solver_cls = CloudflareSolver
_detector_cls = CloudflareDetector
+3 -4
View File
@@ -11,8 +11,6 @@ from ..registry import register
from ..exceptions import IframeLoadError
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
log.addHandler(logging.StreamHandler())
JS_CHALLENGE_SUBMIT = """(token) => {
@@ -36,6 +34,8 @@ JS_CHALLENGE_SUBMIT = """(token) => {
class ReCaptchaSolver(Solver):
page: Page
# пауза на стадии INTERSTITIAL перед повторным detect
INTERSTITIAL_WAIT_MS = 15_000
def __init__(self, captcha_solver: Captcha, **kwargs) -> None:
super().__init__(captcha_solver=captcha_solver, **kwargs)
@@ -49,8 +49,7 @@ class ReCaptchaSolver(Solver):
self.page = page
log.debug("Начинаю решение recaptcha антибот преграды")
if info.stage == Stage.INTERSTITIAL:
# TODO: вытащить
await page.wait_for_timeout(15)
await page.wait_for_timeout(self.INTERSTITIAL_WAIT_MS)
return
elif info.stage == Stage.CHECKBOX:
log.debug("Нужно тыкнуть чекбокс")