333 lines
15 KiB
Markdown
333 lines
15 KiB
Markdown
---
|
||
name: anti-detection-browser
|
||
description: Browser automation with Cloudflare Turnstile & CDP-detection bypass. DrissionPage + Turnstile patch extension + proxy auth forwarder. Use when headless browsers are blocked by Cloudflare or anti-bot detection.
|
||
category: automation
|
||
---
|
||
|
||
# Anti-Detection Browser Automation
|
||
|
||
Обход Cloudflare Turnstile и CDP-детекции при автоматизации браузера.
|
||
|
||
## Когда использовать
|
||
|
||
- Cloudflare блокирует headless-браузеры (Playwright, Puppeteer, Selenium)
|
||
- Страница показывает "Attention Required! | Cloudflare" или "Sorry, you have been blocked"
|
||
- `MouseEvent.screenX/screenY = 0` детектится как CDP-браузер
|
||
- Нужна регистрация/логин на сайтах с Turnstile (x.ai, Clerk, TokenHarbor)
|
||
|
||
## Почему Playwright не работает
|
||
|
||
Playwright использует CDP (Chrome DevTools Protocol). Cloudflare Turnstile детектит CDP-браузеры через:
|
||
- `MouseEvent.screenX/screenY = 0` (в headless-режиме координаты мыши не эмулируются)
|
||
- `navigator.webdriver = true`
|
||
- Отсутствие реальных движений мыши перед кликом
|
||
|
||
## Решение: DrissionPage + Turnstile Patch
|
||
|
||
**DrissionPage** — библиотека управления Chromium, которая не использует WebDriver и не выставляет `navigator.webdriver`.
|
||
|
||
**Turnstile patch** — Chrome extension (Manifest V3), который инжектит скрипт в `MAIN` world на `document_start`, переопределяя `MouseEvent.prototype.screenX/Y` случайными реалистичными значениями.
|
||
|
||
### Структура расширения
|
||
|
||
```
|
||
turnstilePatch/
|
||
├── manifest.json
|
||
└── script.js
|
||
```
|
||
|
||
**manifest.json**:
|
||
```json
|
||
{
|
||
"manifest_version": 3,
|
||
"name": "Turnstile Patch",
|
||
"version": "1.0",
|
||
"content_scripts": [{
|
||
"matches": ["<all_urls>"],
|
||
"js": ["script.js"],
|
||
"run_at": "document_start",
|
||
"world": "MAIN"
|
||
}]
|
||
}
|
||
```
|
||
|
||
**script.js**:
|
||
```javascript
|
||
Object.defineProperty(MouseEvent.prototype, 'screenX', {
|
||
get: function() { return Math.floor(Math.random() * 400) + 800; }
|
||
});
|
||
Object.defineProperty(MouseEvent.prototype, 'screenY', {
|
||
get: function() { return Math.floor(Math.random() * 200) + 400; }
|
||
});
|
||
```
|
||
|
||
### Настройка DrissionPage
|
||
|
||
```python
|
||
from DrissionPage import Chromium, ChromiumOptions
|
||
|
||
co = ChromiumOptions()
|
||
co.auto_port()
|
||
co.set_argument("--no-sandbox")
|
||
co.set_argument("--disable-gpu")
|
||
co.set_argument("--disable-dev-shm-usage")
|
||
co.set_argument("--disable-software-rasterizer")
|
||
|
||
# Загрузка Turnstile patch
|
||
EXTENSION_PATH = os.path.abspath("turnstilePatch")
|
||
co.add_extension(EXTENSION_PATH)
|
||
|
||
# Прокси (если без аутентификации)
|
||
co.set_proxy("http://proxy:port")
|
||
|
||
browser = Chromium(co)
|
||
```
|
||
|
||
### Ожидание Turnstile
|
||
|
||
После навигации нужно ждать 15-30 секунд, пока Turnstile пройдёт в non-interactive режиме:
|
||
|
||
```python
|
||
page.get("https://target.com/sign-up")
|
||
run_logger.info("Waiting for Cloudflare Turnstile...")
|
||
deadline = time.time() + 30
|
||
while time.time() < deadline:
|
||
has_challenge = page.run_js(
|
||
"return document.title.includes('Attention Required') || "
|
||
"document.querySelector('#challenge-running') !== null || "
|
||
"document.body.innerText.includes('verifying you are human')"
|
||
)
|
||
if not has_challenge:
|
||
break
|
||
time.sleep(2)
|
||
```
|
||
|
||
### Поиск кнопок с fallback
|
||
|
||
На странице может быть разный язык. Использовать JS-поиск по тексту:
|
||
|
||
```javascript
|
||
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"]'));
|
||
const target = candidates.find(node => {
|
||
const text = (node.innerText || node.textContent || '').replace(/\s+/g, '').toLowerCase();
|
||
return text.includes('signupwithemail') ||
|
||
text.includes('continue with email') ||
|
||
text.includes('sign up with email') ||
|
||
text.includes('use email') ||
|
||
text.includes('email');
|
||
});
|
||
if (target) target.click();
|
||
```
|
||
|
||
## Proxy Auth Forwarder
|
||
|
||
DrissionPage и nodriver НЕ поддерживают прокси с аутентификацией. Ошибка: "你似乎在设置使用账号密码的代理,暂时不支持这种代理".
|
||
|
||
Решение: локальный forward proxy, который добавляет `Proxy-Authorization` header.
|
||
|
||
### ⚠️ HTTP-based forwarder НЕ работает с Chrome
|
||
|
||
`http.server.HTTPServer` + `BaseHTTPRequestHandler` ломает CONNECT-туннели для Chrome — браузер получает `chrome-error://chromewebdata/`. Curl работает, Chrome — нет.
|
||
|
||
**Использовать RAW TCP forwarder** (см. `scripts/tcp_forwarder.py`):
|
||
|
||
```python
|
||
import socket, threading, select, base64
|
||
|
||
UPSTREAM_HOST = 'residential-x.bpproxy.at'
|
||
UPSTREAM_PORT = 1000
|
||
AUTH = base64.b64encode(b'user:pass').decode()
|
||
LOCAL_PORT = 9129
|
||
|
||
def handle_client(client_sock):
|
||
data = b''
|
||
while b'\r\n\r\n' not in data:
|
||
chunk = client_sock.recv(4096)
|
||
if not chunk: return
|
||
data += chunk
|
||
|
||
request = data.decode('latin-1', errors='replace')
|
||
first_line = request.split('\r\n')[0]
|
||
method, target, _ = first_line.split(' ', 2)
|
||
|
||
if method == 'CONNECT':
|
||
host, port_str = target.rsplit(':', 1)
|
||
port = int(port_str)
|
||
remote = socket.create_connection((UPSTREAM_HOST, UPSTREAM_PORT), timeout=10)
|
||
connect_req = f'CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\nProxy-Authorization: Basic {AUTH}\r\n\r\n'
|
||
remote.send(connect_req.encode())
|
||
resp = b''
|
||
while b'\r\n\r\n' not in resp:
|
||
resp += remote.recv(4096)
|
||
if b'200' not in resp.split(b'\r\n')[0]:
|
||
client_sock.send(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
||
remote.close()
|
||
return
|
||
client_sock.send(b'HTTP/1.1 200 Connection Established\r\n\r\n')
|
||
client_sock.settimeout(30)
|
||
remote.settimeout(30)
|
||
socks = [client_sock, remote]
|
||
while True:
|
||
r, _, _ = select.select(socks, [], [], 30)
|
||
if not r: break
|
||
for s in r:
|
||
d = s.recv(8192)
|
||
if not d: return
|
||
dst = remote if s is client_sock else client_sock
|
||
dst.sendall(d)
|
||
|
||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
server.bind(('127.0.0.1', LOCAL_PORT))
|
||
server.listen(50)
|
||
while True:
|
||
client, addr = server.accept()
|
||
threading.Thread(target=handle_client, args=(client,), daemon=True).start()
|
||
```
|
||
|
||
Запуск:
|
||
```bash
|
||
python tcp_forwarder.py &
|
||
# Затем использовать http://127.0.0.1:9129 как browser_proxy
|
||
```
|
||
|
||
## Geo-Blocking
|
||
|
||
Сервисы типа x.ai гео-блокируют по стране выхода прокси. Проверять перед использованием:
|
||
|
||
```bash
|
||
curl -x "http://user:pass@proxy:port" -s http://ip-api.com/json
|
||
# Проверить поле "country" — x.ai доступен из US, CA, UK, EU, но НЕ из IQ, RU, CN
|
||
```
|
||
|
||
## Nodriver: Fallback When DrissionPage Fails
|
||
|
||
DrissionPage может быть детектирован Cloudflare даже с Turnstile patch. Симптом: `PageDisconnectedError: 与页面的连接已断开` — Cloudflare активно рвёт CDP-соединение.
|
||
|
||
**Nodriver** (`pip install nodriver`) — альтернатива с встроенной антидетект-защитой. Не требует отдельного Turnstile patch.
|
||
|
||
### Настройка nodriver
|
||
|
||
```python
|
||
import nodriver as uc
|
||
|
||
browser = await uc.start(
|
||
headless=False,
|
||
browser_args=[
|
||
"--window-size=1280,900",
|
||
"--no-first-run",
|
||
"--no-default-browser-check",
|
||
"--disable-blink-features=AutomationControlled",
|
||
"--proxy-server=http://127.0.0.1:9129", # через proxy forwarder
|
||
"--ignore-certificate-errors",
|
||
],
|
||
)
|
||
tab = await browser.get("https://accounts.x.ai/sign-up?redirect=grok-com")
|
||
```
|
||
|
||
### API ключевые отличия от DrissionPage
|
||
|
||
- **Async**: все методы `await`
|
||
- `tab.find(text)` — поиск элемента по тексту (аналог `page.run_js(...find text...)`)
|
||
- `tab.select(selector)` — CSS-селектор
|
||
- `tab.evaluate(js)` — выполнить JS
|
||
- `el.click()`, `el.send_keys(text)` — взаимодействие
|
||
- Обернуть в `asyncio.wait_for()` для таймаутов
|
||
|
||
### Google OAuth Bypass для accounts.x.ai
|
||
|
||
**Проблема**: `accounts.x.ai` блокирует ВСЕ bpproxy residential IP (403) напрямую, но Google OAuth (`accounts.google.com`) принимает те же прокси (302).
|
||
|
||
**Решение**: использовать Google OAuth flow вместо прямой email-регистрации:
|
||
|
||
1. Зайти на `accounts.x.ai/sign-up?redirect=grok-com`
|
||
2. Нажать «Continue with Google» (не «Use email»)
|
||
3. Google OAuth-страница загружается через прокси (Google принимает bpproxy)
|
||
4. Ввести Gmail + пароль + TOTP (если 2FA)
|
||
5. Пройти consent screen
|
||
6. Редирект обратно на Grok
|
||
|
||
**Требования**: Gmail аккаунты с 2FA-секретами (формат как в `tusk_gmail_accounts.json`).
|
||
|
||
```python
|
||
# Пример: поиск кнопки Google
|
||
clicked = await click_text(tab, [
|
||
"Continue with Google", "Sign in with Google",
|
||
"Sign up with Google", "Google",
|
||
], timeout=10)
|
||
```
|
||
|
||
### Port Conflicts: Multiple Proxy Forwarders
|
||
|
||
Никогда не запускать несколько экземпляров proxy forwarder на одном порту — они конфликтуют и порт становится недоступным. При смене upstream-прокси:
|
||
|
||
1. Убить старый forwarder: `taskkill /F /PID <pid>`
|
||
2. Проверить что порт свободен: `netstat -ano | grep <port>`
|
||
3. Запустить новый forwarder на **уникальном** порту
|
||
|
||
**Рекомендация**: использовать разные порты для разных upstream-прокси (9128, 9129, 9130...).
|
||
|
||
### Nodriver `browser.stop()` не awaitable
|
||
|
||
`browser.stop()` в nodriver возвращает `None` — не использовать `await`:
|
||
|
||
```python
|
||
# ❌ ОШИБКА: TypeError: object NoneType can't be used in 'await' expression
|
||
await browser.stop()
|
||
|
||
# ✅ ПРАВИЛЬНО
|
||
browser.stop()
|
||
```
|
||
|
||
### TCP Forwarder: всегда вместо HTTP-based
|
||
|
||
HTTP-based forwarder (`http.server.HTTPServer`) не работает с Chrome — CONNECT-туннели ломаются. Всегда использовать RAW TCP forwarder (`scripts/tcp_forwarder.py`).
|
||
|
||
## Pitfalls
|
||
|
||
### Playwright Headless Blocked by Cloudflare (General)
|
||
Playwright's headless Chromium is detected by Cloudflare on many sites, not just those with Turnstile. Symptoms: `page.goto()` times out, empty page content, or Cloudflare challenge page. Use `headless=False` with a persistent Chrome user profile (`launch_persistent_context`), or connect via CDP to an existing Chrome instance (`connect_over_cdp`).
|
||
|
||
### Playwright CDP Works, Headless Doesn't
|
||
`connect_over_cdp("http://127.0.0.1:9222")` to an existing Chrome with remote debugging works reliably because it uses the real Chrome profile. Headless Playwright (`launch()`) is detected. For automation that needs Cloudflare bypass, always prefer CDP connection over headless launch.
|
||
|
||
### Nodriver Windows Asyncio Crashes (2026-08-21)
|
||
Nodriver (`uc.start()`) crashes on Windows with `ValueError: I/O operation on closed pipe` in `asyncio.proactor_events`. This is a Windows-specific asyncio issue. Fall back to Playwright with `headless=False` or CDP connection.
|
||
|
||
### MCP Browser as Reliable Fallback
|
||
When Playwright and nodriver both fail, the Chrome DevTools MCP (`mcp__chrome_devtools__*` tools) works reliably. It uses the existing Chrome instance with the user's profile, avoiding Cloudflare detection entirely. Use `evaluate_script` with nativeInputSetter for fastest form interactions.
|
||
|
||
### Standalone Chrome + CDP: самопроходящий Turnstile (2026-09-10)
|
||
Когда все MCP/headless-пути висят на SPA-бандле (белая страница, бандл pending, хотя curl качает 200), а firecrawl_interact рвёт сессии — поднять отдельный Chrome с `--remote-debugging-port=9333 --user-data-dir=<fresh>` (background=true), подключиться Playwright `connect_over_cdp`, заполнить форму и **поллить скрытое поле `cf-turnstile-response` до length>10** — в реальном Chrome non-interactive Turnstile решается сам за 10-20 сек. Полный разбор (RainProxy): `references/rainproxy-cdp-turnstile.md` — там же pitfalls: `has-text("Login")` ловит "Login with Google", Gmail-алиасы с `+` отклоняются, UTF-7 имена папок Gmail при русской локали.
|
||
|
||
### accounts.x.ai blocks ALL bpproxy residential IPs (2026-08-14)
|
||
|
||
Все 25 bpproxy residential прокси возвращают `403` при прямом обращении к `accounts.x.ai`. Google (`accounts.google.com`) принимает те же прокси. Использовать Google OAuth flow вместо прямого email signup.
|
||
|
||
### DrissionPage CDP disconnection
|
||
|
||
Cloudflare может детектировать и разрывать CDP-соединение DrissionPage (`PageDisconnectedError`). Переключиться на nodriver.
|
||
|
||
### DrissionPage proxy auth
|
||
DrissionPage не поддерживает `user:pass@host:port` в URL прокси. Использовать proxy forwarder.
|
||
|
||
### Turnstile timing
|
||
Non-interactive Turnstile требует 5-30 секунд. Недостаточное ожидание → кнопки не найдены (страница ещё не загрузилась после challenge).
|
||
|
||
### Geo-blocking bpproxy
|
||
bpproxy residential выходят из разных стран (Ирак, Канада, etc.). Всегда проверять `ip-api.com/json` перед использованием с гео-зависимыми сервисами.
|
||
|
||
### bpproxy hardsession истекает
|
||
Hardsession-прокси могут умереть через несколько часов. Периодически перегенерировать пул через `bpproxy` skill.
|
||
|
||
## Связанные скиллы
|
||
|
||
- `bpproxy` — генерация и управление прокси
|
||
- `bulk-account-registration` — пайплайн массовой регистрации
|
||
|
||
## Связанные файлы
|
||
|
||
- `references/grok-registration-pipeline.md` — конкретный пайплайн регистрации Grok (x.ai) с DrissionPage
|
||
- `scripts/proxy_forwarder.py` — HTTP forward proxy (DEPRECATED — не работает с Chrome, используй tcp_forwarder.py)
|
||
- `scripts/tcp_forwarder.py` — RAW TCP forward proxy (работает с Chrome, nodriver, DrissionPage)
|
||
- `templates/turnstile-patch/` — шаблон Chrome extension для обхода CDP-детекции (manifest.json + script.js) |