fix: reCAPTCHA — окно с картинками определяется по положению (#10)
- Маркер интерактивного челленджа: iframe bframe, контейнер которого не стоит за экраном (top: -10000px); click_and_race ждёт видимости, а не появления в DOM. - Маркер наличия капчи берётся из исходного HTML (api.js / enterprise.js, .g-recaptcha, iframe чекбокса), проверка мгновенная, без ожидания iframe. - SolverReCaptcha ждёт detect_locator в DOM, а не видимости (script невидим). - test_main.http: секреты вынесены в http-client.private.env.json, запрос на стенд zennolab, исправлены XPath с @class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fdee12e2d8
commit
4d90dc4a21
7 files changed
+67
-19
No files matched your search
+1
-1
@@ -12,5 +12,5 @@ tab_width = 4
|
|||||||
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}]
|
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[{*.yaml,*.yml,environment.yaml,environment.yml}]
|
[{*.yaml,*.yml,*.http,environment.yaml,environment.yml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
@@ -9,3 +9,4 @@ build/
|
|||||||
client/dist/
|
client/dist/
|
||||||
client/build/
|
client/build/
|
||||||
client/src/*.egg-info/
|
client/src/*.egg-info/
|
||||||
|
http-client.private.env.json
|
||||||
+3
-1
@@ -23,8 +23,10 @@ async def click_and_race(page, element, challenge_locator, timeout=30_000):
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# challenge_locator должен совпадать только с открытым окном челленджа: элементы челленджа могут быть в DOM
|
||||||
|
# заранее (reCAPTCHA держит их за экраном), поэтому ждём видимости, а не появления
|
||||||
challenge_task = asyncio.create_task(
|
challenge_task = asyncio.create_task(
|
||||||
challenge_locator.wait_for(state="attached", timeout=timeout)
|
challenge_locator.first.wait_for(state="visible", timeout=timeout)
|
||||||
)
|
)
|
||||||
|
|
||||||
await element.click()
|
await element.click()
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ from ..exceptions import IframeLoadError
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# iframe окна с картинками (bframe) вставляется в DOM вместе с чекбоксом, но его контейнер стоит за экраном:
|
||||||
|
# style="visibility: hidden; ... top: -10000px". Когда выпадает челлендж, контейнер переезжает на экран и
|
||||||
|
# становится видимым — локатор совпадает только с открытым окном
|
||||||
|
INTERACTIVE_MARKER_SELECTOR = (
|
||||||
|
"xpath=//iframe[contains(@src, '/bframe')][not(ancestor::div[contains(@style, 'top: -10000px')])]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# признак reCAPTCHA, который есть уже в исходном HTML, до загрузки iframe с чекбоксом (он грузится долго):
|
||||||
|
# скрипт api.js / enterprise.js, контейнер .g-recaptcha (у render=explicit его может не быть) или сам iframe.
|
||||||
|
# #recaptcha-token не подходит — он внутри iframe чекбокса, с основной страницы его не видно
|
||||||
|
DETECT_MARKER_SELECTOR = (
|
||||||
|
"script[src*='/recaptcha/api.js'], script[src*='/recaptcha/enterprise.js'], "
|
||||||
|
".g-recaptcha, iframe[title='reCAPTCHA']"
|
||||||
|
)
|
||||||
|
|
||||||
JS_CHALLENGE_SUBMIT = """(token) => {
|
JS_CHALLENGE_SUBMIT = """(token) => {
|
||||||
const clients = window.___grecaptcha_cfg && window.___grecaptcha_cfg.clients;
|
const clients = window.___grecaptcha_cfg && window.___grecaptcha_cfg.clients;
|
||||||
@@ -55,9 +69,7 @@ class ReCaptchaSolver(Solver):
|
|||||||
log.debug("Нужно тыкнуть чекбокс")
|
log.debug("Нужно тыкнуть чекбокс")
|
||||||
checkbox_selector = "#recaptcha-anchor"
|
checkbox_selector = "#recaptcha-anchor"
|
||||||
element = page.frame_locator("iframe[title='reCAPTCHA']").locator(checkbox_selector)
|
element = page.frame_locator("iframe[title='reCAPTCHA']").locator(checkbox_selector)
|
||||||
# TODO: маркер всегда присутствует
|
challenge_locator = page.locator(INTERACTIVE_MARKER_SELECTOR)
|
||||||
interactive_marker_selector = "//div[@class='g-recaptcha-bubble-arrow']/..//iframe"
|
|
||||||
challenge_locator = page.locator(interactive_marker_selector)
|
|
||||||
result = await click_and_race(page, element, challenge_locator)
|
result = await click_and_race(page, element, challenge_locator)
|
||||||
log.debug(f"Ожидание завершено: {result}")
|
log.debug(f"Ожидание завершено: {result}")
|
||||||
log.debug("Возврат к детекту")
|
log.debug("Возврат к детекту")
|
||||||
@@ -87,10 +99,9 @@ class ReCaptchaDetector(Detector):
|
|||||||
log.debug("ReCaptchaDetector: попытка определить есть ли преграда")
|
log.debug("ReCaptchaDetector: попытка определить есть ли преграда")
|
||||||
info.meta = {}
|
info.meta = {}
|
||||||
checkbox_selector = "#recaptcha-anchor"
|
checkbox_selector = "#recaptcha-anchor"
|
||||||
interactive_marker_selector = "//div[@class='g-recaptcha-bubble-arrow']/..//iframe"
|
|
||||||
|
|
||||||
# попытка определить что мы вообще на странице блокировки
|
# попытка определить что мы вообще на странице блокировки
|
||||||
marker_selector = info.detect_locator or "#captcha-form"
|
marker_selector = info.detect_locator or DETECT_MARKER_SELECTOR
|
||||||
info.detect_locator = marker_selector
|
info.detect_locator = marker_selector
|
||||||
|
|
||||||
log.debug(f"ищем по detect: {marker_selector} success: {info.success_locator}")
|
log.debug(f"ищем по detect: {marker_selector} success: {info.success_locator}")
|
||||||
@@ -111,7 +122,7 @@ class ReCaptchaDetector(Detector):
|
|||||||
|
|
||||||
log.debug("Вижу что это страница с проверкой. Определяю состояние.")
|
log.debug("Вижу что это страница с проверкой. Определяю состояние.")
|
||||||
iframe = page.frame_locator("iframe[title='reCAPTCHA']")
|
iframe = page.frame_locator("iframe[title='reCAPTCHA']")
|
||||||
# ожидаем когда будет загружен чекбокс
|
# маркер есть — значит капча будет, ждём пока догрузится iframe с чекбоксом
|
||||||
|
|
||||||
info.stage = Stage.INTERSTITIAL
|
info.stage = Stage.INTERSTITIAL
|
||||||
checkbox = iframe.locator(checkbox_selector)
|
checkbox = iframe.locator(checkbox_selector)
|
||||||
@@ -126,7 +137,7 @@ class ReCaptchaDetector(Detector):
|
|||||||
log.debug("Чек бокс найден")
|
log.debug("Чек бокс найден")
|
||||||
info.stage = Stage.CHECKBOX
|
info.stage = Stage.CHECKBOX
|
||||||
|
|
||||||
if await page.locator(interactive_marker_selector).count() > 0:
|
if await page.locator(INTERACTIVE_MARKER_SELECTOR).first.is_visible():
|
||||||
log.debug("Нужно решать капчу")
|
log.debug("Нужно решать капчу")
|
||||||
# нужно решение
|
# нужно решение
|
||||||
info.stage = Stage.INTERACTIVE
|
info.stage = Stage.INTERACTIVE
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ class SolverReCaptcha(Captcha):
|
|||||||
:param page:
|
:param page:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
div_container = page.locator(selector)
|
# selector может указывать на невидимый элемент (script) и совпадать с несколькими
|
||||||
await div_container.wait_for()
|
await page.locator(selector).first.wait_for(state="attached")
|
||||||
|
|
||||||
site_key_input_element = page.locator("iframe[title='reCAPTCHA']")
|
site_key_input_element = page.locator("iframe[title='reCAPTCHA']")
|
||||||
src_value = await site_key_input_element.get_attribute("src")
|
src_value = await site_key_input_element.get_attribute("src")
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"dev": {
|
||||||
|
"api_key": "rbx_...",
|
||||||
|
"proxy": "http://user:pass@host:port",
|
||||||
|
"captcha_key": "<ключ 2captcha>"
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-8
@@ -1,4 +1,6 @@
|
|||||||
# Test your FastAPI endpoints
|
# Test your FastAPI endpoints
|
||||||
|
# Секреты (api_key, proxy, captcha_key) — в tests/http-client.private.env.json (не в git, шаблон —
|
||||||
|
# http-client.private.env.example.json); окружение dev выбирается в PyCharm над запросом.
|
||||||
|
|
||||||
GET http://127.0.0.1:8000/
|
GET http://127.0.0.1:8000/
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
@@ -13,7 +15,7 @@ Accept: application/json
|
|||||||
POST http://127.0.0.1:8000/v1/solve
|
POST http://127.0.0.1:8000/v1/solve
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: <key>
|
X-API-Key: {{api_key}}
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -36,7 +38,7 @@ X-API-Key: <key>
|
|||||||
POST http://127.0.0.1:8000/v1/solve
|
POST http://127.0.0.1:8000/v1/solve
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: <key>
|
X-API-Key: {{api_key}}
|
||||||
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
|||||||
POST http://127.0.0.1:8000/v1/solve
|
POST http://127.0.0.1:8000/v1/solve
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: <key>
|
X-API-Key: {{api_key}}
|
||||||
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
||||||
|
|
||||||
|
|
||||||
@@ -72,7 +74,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
|||||||
POST http://127.0.0.1:8000/v1/solve
|
POST http://127.0.0.1:8000/v1/solve
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: <key>
|
X-API-Key: {{api_key}}
|
||||||
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
||||||
|
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
|
|||||||
POST http://127.0.0.1:8000/v1/solve
|
POST http://127.0.0.1:8000/v1/solve
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: <key>
|
X-API-Key: {{api_key}}
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -112,11 +114,11 @@ X-API-Key: <key>
|
|||||||
POST http://localhost:8002/v2/tasks
|
POST http://localhost:8002/v2/tasks
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: <key>
|
X-API-Key: {{api_key}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"url": "https://freelancehunt.com/",
|
"url": "https://freelancehunt.com/",
|
||||||
"proxy": "http://BQLo3X:poqAWX@168.81.43.154:8000",
|
"proxy": "{{proxy}}",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"type": "inner_text",
|
"type": "inner_text",
|
||||||
@@ -132,6 +134,31 @@ X-API-Key: <key>
|
|||||||
GET http://localhost:8002/v2/tasks/4d8105f8-93a8-43ee-aa6e-db917451363b
|
GET http://localhost:8002/v2/tasks/4d8105f8-93a8-43ee-aa6e-db917451363b
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
X-API-Key: <key>
|
X-API-Key: {{api_key}}
|
||||||
|
|
||||||
|
###
|
||||||
|
# @timeout 600
|
||||||
|
POST http://localhost:8010/v1/solve
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-API-Key: {{api_key}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "https://lessons.zennolab.com/captchas/recaptcha/v2_nosubmit.php?level=low",
|
||||||
|
"proxy": "{{proxy}}",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "pass_challenge",
|
||||||
|
"value": "recaptcha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inner_text",
|
||||||
|
"selector": "//button[contains(@class, 'actionItem_151') and @type='submit']"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"captcha": "{{captcha_key}}",
|
||||||
|
"captcha_type": "recaptcha",
|
||||||
|
"captcha_success_locator": "//code[contains(@class, 'hljs _successMessageCode')]"
|
||||||
|
}
|
||||||
|
|
||||||
###
|
###
|
||||||
Reference in new issue
Block a user