dragg2 commited on
Commit
5124452
·
verified ·
1 Parent(s): 1753a74

Upload 16 files

Browse files
src/api/routes.py CHANGED
@@ -62,8 +62,12 @@ def _check_client_key(client_key: str) -> CreateTaskResponse | None:
62
  return None
63
 
64
 
65
- @router.post("/createTask", response_model=CreateTaskResponse)
66
- async def create_task(request: CreateTaskRequest) -> CreateTaskResponse:
 
 
 
 
67
  err = _check_client_key(request.clientKey)
68
  if err:
69
  return err
@@ -117,8 +121,12 @@ async def create_task(request: CreateTaskRequest) -> CreateTaskResponse:
117
  return CreateTaskResponse(errorId=0, taskId=task_id)
118
 
119
 
120
- @router.post("/getTaskResult", response_model=GetTaskResultResponse)
121
- async def get_task_result(
 
 
 
 
122
  request: GetTaskResultRequest,
123
  ) -> GetTaskResultResponse:
124
  if config.client_key and request.clientKey != config.client_key:
@@ -153,19 +161,24 @@ async def get_task_result(
153
  )
154
 
155
 
156
- @router.post("/getBalance", response_model=GetBalanceResponse)
157
- async def get_balance(request: GetBalanceRequest) -> GetBalanceResponse:
 
 
 
 
158
  if config.client_key and request.clientKey != config.client_key:
159
  return GetBalanceResponse(errorId=1, balance=0)
160
  return GetBalanceResponse(errorId=0, balance=99999.0)
161
 
162
 
163
  @router.get("/api/v1/health")
164
- async def health() -> dict[str, object]:
165
- return {
166
- "status": "ok",
167
- "supported_task_types": task_manager.supported_types(),
168
- "browser_headless": config.browser_headless,
169
- "cloud_model": config.cloud_model,
170
- "local_model": config.local_model,
171
- }
 
 
62
  return None
63
 
64
 
65
+ @router.post(
66
+ "/createTask",
67
+ response_model=CreateTaskResponse,
68
+ response_model_exclude_none=True,
69
+ )
70
+ async def create_task(request: CreateTaskRequest) -> CreateTaskResponse:
71
  err = _check_client_key(request.clientKey)
72
  if err:
73
  return err
 
121
  return CreateTaskResponse(errorId=0, taskId=task_id)
122
 
123
 
124
+ @router.post(
125
+ "/getTaskResult",
126
+ response_model=GetTaskResultResponse,
127
+ response_model_exclude_none=True,
128
+ )
129
+ async def get_task_result(
130
  request: GetTaskResultRequest,
131
  ) -> GetTaskResultResponse:
132
  if config.client_key and request.clientKey != config.client_key:
 
161
  )
162
 
163
 
164
+ @router.post(
165
+ "/getBalance",
166
+ response_model=GetBalanceResponse,
167
+ response_model_exclude_none=True,
168
+ )
169
+ async def get_balance(request: GetBalanceRequest) -> GetBalanceResponse:
170
  if config.client_key and request.clientKey != config.client_key:
171
  return GetBalanceResponse(errorId=1, balance=0)
172
  return GetBalanceResponse(errorId=0, balance=99999.0)
173
 
174
 
175
  @router.get("/api/v1/health")
176
+ async def health() -> dict[str, object]:
177
+ return {
178
+ "status": "ok",
179
+ "supported_task_types": task_manager.supported_types(),
180
+ "browser_headless": config.browser_headless,
181
+ "browser_proxy_configured": bool(config.browser_proxy_url),
182
+ "cloud_model": config.cloud_model,
183
+ "local_model": config.local_model,
184
+ }
src/core/config.py CHANGED
@@ -20,8 +20,8 @@ import os
20
  from dataclasses import dataclass
21
 
22
 
23
- @dataclass(frozen=True)
24
- class Config:
25
  server_host: str
26
  server_port: int
27
 
@@ -41,9 +41,10 @@ class Config:
41
  captcha_retries: int
42
  captcha_timeout: int
43
 
44
- # Playwright browser
45
- browser_headless: bool
46
- browser_timeout: int # seconds
 
47
 
48
  # ── Convenience aliases (backward-compat) ──
49
 
@@ -95,12 +96,13 @@ def load_config() -> Config:
95
  "LOCAL_MODEL",
96
  os.environ.get("CAPTCHA_MULTIMODAL_MODEL", "Qwen/Qwen3.5-2B"),
97
  ),
98
- captcha_retries=int(os.environ.get("CAPTCHA_RETRIES", "3")),
99
- captcha_timeout=int(os.environ.get("CAPTCHA_TIMEOUT", "30")),
100
- browser_headless=os.environ.get("BROWSER_HEADLESS", "true").strip().lower()
101
- in {"1", "true", "yes"},
102
- browser_timeout=int(os.environ.get("BROWSER_TIMEOUT", "30")),
103
- )
 
104
 
105
 
106
  config = load_config()
 
20
  from dataclasses import dataclass
21
 
22
 
23
+ @dataclass(frozen=True)
24
+ class Config:
25
  server_host: str
26
  server_port: int
27
 
 
41
  captcha_retries: int
42
  captcha_timeout: int
43
 
44
+ # Playwright browser
45
+ browser_headless: bool
46
+ browser_timeout: int # seconds
47
+ browser_proxy_url: str | None = None
48
 
49
  # ── Convenience aliases (backward-compat) ──
50
 
 
96
  "LOCAL_MODEL",
97
  os.environ.get("CAPTCHA_MULTIMODAL_MODEL", "Qwen/Qwen3.5-2B"),
98
  ),
99
+ captcha_retries=int(os.environ.get("CAPTCHA_RETRIES", "3")),
100
+ captcha_timeout=int(os.environ.get("CAPTCHA_TIMEOUT", "30")),
101
+ browser_headless=os.environ.get("BROWSER_HEADLESS", "true").strip().lower()
102
+ in {"1", "true", "yes"},
103
+ browser_timeout=int(os.environ.get("BROWSER_TIMEOUT", "30")),
104
+ browser_proxy_url=os.environ.get("BROWSER_PROXY_URL", "").strip() or None,
105
+ )
106
 
107
 
108
  config = load_config()
src/models/task.py CHANGED
@@ -4,18 +4,22 @@ from __future__ import annotations
4
 
5
  from typing import Any
6
 
7
- from pydantic import BaseModel, Field
8
 
9
 
10
  # ── createTask ──────────────────────────────────────────────
11
 
12
  class TaskObject(BaseModel):
 
 
13
  type: str
14
  websiteURL: str | None = None
15
  websiteKey: str | None = None
16
  pageAction: str | None = None
17
  minScore: float | None = None
18
  isInvisible: bool | None = None
 
 
19
  enterprisePayload: dict[str, Any] | None = None
20
  # Image captcha / classification fields
21
  body: str | None = None
@@ -47,13 +51,23 @@ class GetTaskResultRequest(BaseModel):
47
 
48
 
49
  class SolutionObject(BaseModel):
 
 
50
  gRecaptchaResponse: str | None = None
51
  respKey: str | None = None
52
  text: str | None = None
53
  token: str | None = None
54
  objects: list[int] | None = None
55
- answer: bool | list[int] | None = None
56
- userAgent: str | None = None
 
 
 
 
 
 
 
 
57
 
58
 
59
  class GetTaskResultResponse(BaseModel):
 
4
 
5
  from typing import Any
6
 
7
+ from pydantic import BaseModel, ConfigDict, Field
8
 
9
 
10
  # ── createTask ──────────────────────────────────────────────
11
 
12
  class TaskObject(BaseModel):
13
+ model_config = ConfigDict(populate_by_name=True)
14
+
15
  type: str
16
  websiteURL: str | None = None
17
  websiteKey: str | None = None
18
  pageAction: str | None = None
19
  minScore: float | None = None
20
  isInvisible: bool | None = None
21
+ isSession: bool | None = None
22
+ apiDomain: str | None = None
23
  enterprisePayload: dict[str, Any] | None = None
24
  # Image captcha / classification fields
25
  body: str | None = None
 
51
 
52
 
53
  class SolutionObject(BaseModel):
54
+ model_config = ConfigDict(populate_by_name=True)
55
+
56
  gRecaptchaResponse: str | None = None
57
  respKey: str | None = None
58
  text: str | None = None
59
  token: str | None = None
60
  objects: list[int] | None = None
61
+ answer: bool | list[int] | None = None
62
+ userAgent: str | None = None
63
+ acceptLanguage: str | None = None
64
+ secChUa: str | None = None
65
+ secChUaMobile: str | None = None
66
+ secChUaPlatform: str | None = None
67
+ recaptchaCaT: str | None = Field(default=None, alias="recaptcha-ca-t")
68
+ recaptchaCaE: str | None = Field(default=None, alias="recaptcha-ca-e")
69
+ createTime: int | None = None
70
+ runtimeKind: str | None = None
71
 
72
 
73
  class GetTaskResultResponse(BaseModel):
src/services/recaptcha_v3.py CHANGED
@@ -1,160 +1,804 @@
1
- """reCAPTCHA v3 solver using Playwright browser automation."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import logging
7
- from typing import Any
8
-
9
- from playwright.async_api import Browser, Playwright, async_playwright
10
-
11
- from ..core.config import Config
12
-
13
- log = logging.getLogger(__name__)
14
-
15
- # JS executed inside the browser to obtain a reCAPTCHA v3 token.
16
- # Handles both standard and enterprise reCAPTCHA libraries.
17
- _EXECUTE_JS = """
18
- ([key, action]) => new Promise((resolve, reject) => {
19
- const gr = window.grecaptcha?.enterprise || window.grecaptcha;
20
- if (gr && typeof gr.execute === 'function') {
21
- gr.ready(() => {
22
- gr.execute(key, {action}).then(resolve).catch(reject);
23
- });
24
- return;
25
- }
26
- // grecaptcha not loaded yet — inject the script ourselves
27
- const script = document.createElement('script');
28
- script.src = 'https://www.google.com/recaptcha/api.js?render=' + key;
29
- script.onerror = () => reject(new Error('Failed to load reCAPTCHA script'));
30
- script.onload = () => {
31
- const g = window.grecaptcha;
32
- if (!g) { reject(new Error('grecaptcha still undefined after script load')); return; }
33
- g.ready(() => {
34
- g.execute(key, {action}).then(resolve).catch(reject);
35
- });
36
- };
37
- document.head.appendChild(script);
38
- })
39
- """
40
-
41
- # Basic anti-detection init script
42
- _STEALTH_JS = """
43
- Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
44
- Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
45
- Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
46
- window.chrome = {runtime: {}, loadTimes: () => {}, csi: () => {}};
47
- """
48
-
49
-
50
- class RecaptchaV3Solver:
51
- """Solves RecaptchaV3TaskProxyless tasks via headless Chromium."""
52
-
53
- def __init__(self, config: Config) -> None:
54
- self._config = config
55
- self._playwright: Playwright | None = None
56
- self._browser: Browser | None = None
57
-
58
- async def start(self) -> None:
59
- self._playwright = await async_playwright().start()
60
- self._browser = await self._playwright.chromium.launch(
61
- headless=self._config.browser_headless,
62
- args=[
63
- "--disable-blink-features=AutomationControlled",
64
- "--no-sandbox",
65
- "--disable-dev-shm-usage",
66
- "--disable-gpu",
67
- ],
68
- )
69
- log.info(
70
- "Playwright browser started (headless=%s)", self._config.browser_headless
71
- )
72
-
73
- async def stop(self) -> None:
74
- if self._browser:
75
- await self._browser.close()
76
- if self._playwright:
77
- await self._playwright.stop()
78
- log.info("Playwright browser stopped")
79
-
80
- async def solve(self, params: dict[str, Any]) -> dict[str, Any]:
81
- website_url = params["websiteURL"]
82
- website_key = params["websiteKey"]
83
- page_action = params.get("pageAction", "verify")
84
-
85
- last_error: Exception | None = None
86
- for attempt in range(self._config.captcha_retries):
87
- try:
88
- token = await self._solve_once(
89
- website_url, website_key, page_action
90
- )
91
- return {"gRecaptchaResponse": token}
92
- except Exception as exc:
93
- last_error = exc
94
- log.warning(
95
- "Attempt %d/%d failed for %s: %s",
96
- attempt + 1,
97
- self._config.captcha_retries,
98
- website_url,
99
- exc,
100
- )
101
- if attempt < self._config.captcha_retries - 1:
102
- await asyncio.sleep(2)
103
-
104
- raise RuntimeError(
105
- f"Failed after {self._config.captcha_retries} attempts: {last_error}"
106
- )
107
-
108
- async def _solve_once(
109
- self, website_url: str, website_key: str, page_action: str
110
- ) -> str:
111
- assert self._browser is not None
112
-
113
- context = await self._browser.new_context(
114
- user_agent=(
115
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
116
- "AppleWebKit/537.36 (KHTML, like Gecko) "
117
- "Chrome/131.0.0.0 Safari/537.36"
118
- ),
119
- viewport={"width": 1920, "height": 1080},
120
- locale="en-US",
121
- )
122
-
123
- page = await context.new_page()
124
- await page.add_init_script(_STEALTH_JS)
125
-
126
- try:
127
- timeout_ms = self._config.browser_timeout * 1000
128
- await page.goto(
129
- website_url, wait_until="networkidle", timeout=timeout_ms
130
- )
131
-
132
- # Simulate minimal human-like behaviour to improve score
133
- await page.mouse.move(400, 300)
134
- await asyncio.sleep(1)
135
- await page.mouse.move(600, 400)
136
- await asyncio.sleep(0.5)
137
-
138
- # Wait for reCAPTCHA to become available (may already be on page)
139
- try:
140
- await page.wait_for_function(
141
- "(typeof grecaptcha !== 'undefined' && typeof grecaptcha.execute === 'function') "
142
- "|| (typeof grecaptcha !== 'undefined' && typeof grecaptcha?.enterprise?.execute === 'function')",
143
- timeout=10_000,
144
- )
145
- except Exception:
146
- log.info(
147
- "grecaptcha not detected on page, will attempt script injection"
148
- )
149
-
150
- token = await page.evaluate(_EXECUTE_JS, [website_key, page_action])
151
-
152
- if not isinstance(token, str) or len(token) < 20:
153
- raise RuntimeError(f"Invalid token received: {token!r}")
154
-
155
- log.info(
156
- "Got reCAPTCHA token for %s (len=%d)", website_url, len(token)
157
- )
158
- return token
159
- finally:
160
- await context.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime-aware reCAPTCHA v3 solver using Playwright browser automation.
2
+
3
+ This module intentionally separates:
4
+
5
+ 1. task normalization
6
+ 2. runtime probing (standard v3 vs enterprise)
7
+ 3. execution
8
+ 4. result/artifact collection
9
+
10
+ That structure mirrors mainstream captcha providers more closely than a
11
+ single "get token if any string is returned" flow.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import logging
18
+ import re
19
+ import time
20
+ from dataclasses import dataclass, field
21
+ from typing import Any
22
+ from urllib.parse import unquote, urlparse
23
+
24
+ from playwright.async_api import Browser, Page, Request, Response, Playwright, async_playwright
25
+
26
+ from ..core.config import Config
27
+
28
+ log = logging.getLogger(__name__)
29
+
30
+ _STANDARD_RUNTIME = "standard"
31
+ _ENTERPRISE_RUNTIME = "enterprise"
32
+ _DEFAULT_API_DOMAIN = "www.google.com"
33
+ _BROWSER_ACCEPT_LANGUAGE = "en-US,en;q=0.9"
34
+ _TOKEN_RESPONSE_MARKERS = (
35
+ "/recaptcha/api2/reload",
36
+ "/recaptcha/api2/clr",
37
+ "/recaptcha/enterprise/reload",
38
+ "/recaptcha/enterprise/clr",
39
+ )
40
+ _TOKEN_RESPONSE_PATTERNS = (
41
+ re.compile(r'"rresp","([^"]+)"'),
42
+ re.compile(r"'rresp','([^']+)'"),
43
+ )
44
+
45
+ _WAIT_FOR_STANDARD_RUNTIME_JS = """
46
+ () => (
47
+ typeof window.grecaptcha !== 'undefined'
48
+ && typeof window.grecaptcha.execute === 'function'
49
+ )
50
+ """
51
+
52
+ _WAIT_FOR_ENTERPRISE_RUNTIME_JS = """
53
+ () => (
54
+ typeof window.grecaptcha !== 'undefined'
55
+ && typeof window.grecaptcha.enterprise !== 'undefined'
56
+ && typeof window.grecaptcha.enterprise.execute === 'function'
57
+ )
58
+ """
59
+
60
+ _RUNTIME_INSPECTION_JS = """
61
+ () => {
62
+ const scripts = [...document.scripts]
63
+ .map((script) => script.src)
64
+ .filter((src) => typeof src === 'string' && src.includes('/recaptcha/'));
65
+ const cfg = window.___grecaptcha_cfg || {};
66
+ return {
67
+ scripts,
68
+ hasStandardExecute: typeof window.grecaptcha?.execute === 'function',
69
+ hasEnterpriseExecute: typeof window.grecaptcha?.enterprise?.execute === 'function',
70
+ enterpriseCfg: cfg.enterprise === true,
71
+ };
72
+ }
73
+ """
74
+
75
+ _ENSURE_SCRIPT_LOADED_JS = """
76
+ ([scriptUrl]) => new Promise((resolve, reject) => {
77
+ const existing = [...document.scripts].find((script) => script.src === scriptUrl);
78
+ if (existing) {
79
+ resolve(scriptUrl);
80
+ return;
81
+ }
82
+ const script = document.createElement('script');
83
+ script.src = scriptUrl;
84
+ script.async = true;
85
+ script.defer = true;
86
+ script.onerror = () => reject(new Error(`Failed to load script: ${scriptUrl}`));
87
+ script.onload = () => resolve(scriptUrl);
88
+ document.head.appendChild(script);
89
+ })
90
+ """
91
+
92
+ _EXECUTE_STANDARD_JS = """
93
+ ([key, action]) => new Promise((resolve, reject) => {
94
+ const gr = window.grecaptcha;
95
+ if (!gr || typeof gr.execute !== 'function') {
96
+ reject(new Error('grecaptcha.execute is not available'));
97
+ return;
98
+ }
99
+ gr.ready(() => {
100
+ const options = {};
101
+ if (action) {
102
+ options.action = action;
103
+ }
104
+ gr.execute(key, options).then(resolve).catch(reject);
105
+ });
106
+ })
107
+ """
108
+
109
+ _EXECUTE_ENTERPRISE_JS = """
110
+ ([key, action, sToken]) => new Promise((resolve, reject) => {
111
+ const gr = window.grecaptcha?.enterprise;
112
+ if (!gr || typeof gr.execute !== 'function') {
113
+ reject(new Error('grecaptcha.enterprise.execute is not available'));
114
+ return;
115
+ }
116
+ gr.ready(() => {
117
+ const options = {};
118
+ if (action) {
119
+ options.action = action;
120
+ }
121
+ if (sToken) {
122
+ options.s = sToken;
123
+ }
124
+ gr.execute(key, options).then(resolve).catch(reject);
125
+ });
126
+ })
127
+ """
128
+
129
+ _FALLBACK_FINGERPRINT_JS = """
130
+ () => {
131
+ const uaData = navigator.userAgentData || null;
132
+ let secChUa = '';
133
+ let secChUaMobile = '';
134
+ let secChUaPlatform = '';
135
+
136
+ if (uaData) {
137
+ if (Array.isArray(uaData.brands) && uaData.brands.length > 0) {
138
+ secChUa = uaData.brands
139
+ .map((item) => `"${item.brand}";v="${item.version}"`)
140
+ .join(', ');
141
+ }
142
+ secChUaMobile = uaData.mobile ? '?1' : '?0';
143
+ if (uaData.platform) {
144
+ secChUaPlatform = `"${uaData.platform}"`;
145
+ }
146
+ }
147
+
148
+ return {
149
+ userAgent: navigator.userAgent || '',
150
+ acceptLanguage: Array.isArray(navigator.languages) && navigator.languages.length > 0
151
+ ? navigator.languages.join(',')
152
+ : (navigator.language || ''),
153
+ secChUa,
154
+ secChUaMobile,
155
+ secChUaPlatform,
156
+ };
157
+ }
158
+ """
159
+
160
+ # Basic anti-detection init script
161
+ _STEALTH_JS = """
162
+ Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
163
+ Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
164
+ Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
165
+ window.chrome = {runtime: {}, loadTimes: () => {}, csi: () => {}};
166
+ """
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class BrowserFingerprint:
171
+ """Actual browser fingerprint observed during token generation."""
172
+
173
+ user_agent: str = ""
174
+ accept_language: str = ""
175
+ sec_ch_ua: str = ""
176
+ sec_ch_ua_mobile: str = ""
177
+ sec_ch_ua_platform: str = ""
178
+
179
+ def to_solution_fields(self) -> dict[str, Any]:
180
+ payload: dict[str, Any] = {}
181
+ if self.user_agent:
182
+ payload["userAgent"] = self.user_agent
183
+ if self.accept_language:
184
+ payload["acceptLanguage"] = self.accept_language
185
+ if self.sec_ch_ua:
186
+ payload["secChUa"] = self.sec_ch_ua
187
+ if self.sec_ch_ua_mobile:
188
+ payload["secChUaMobile"] = self.sec_ch_ua_mobile
189
+ if self.sec_ch_ua_platform:
190
+ payload["secChUaPlatform"] = self.sec_ch_ua_platform
191
+ return payload
192
+
193
+
194
+ @dataclass(frozen=True)
195
+ class RecaptchaSessionArtifacts:
196
+ """Session cookies that some mature vendors return for reCAPTCHA v3."""
197
+
198
+ recaptcha_ca_t: str | None = None
199
+ recaptcha_ca_e: str | None = None
200
+
201
+ def to_solution_fields(self) -> dict[str, Any]:
202
+ payload: dict[str, Any] = {}
203
+ if self.recaptcha_ca_t:
204
+ payload["recaptcha-ca-t"] = self.recaptcha_ca_t
205
+ if self.recaptcha_ca_e:
206
+ payload["recaptcha-ca-e"] = self.recaptcha_ca_e
207
+ return payload
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class RecaptchaTaskProfile:
212
+ """Normalized internal task view."""
213
+
214
+ task_type: str
215
+ website_url: str
216
+ website_key: str
217
+ page_action: str
218
+ requested_runtime: str
219
+ api_domain: str
220
+ enterprise_s_token: str
221
+ wants_session_artifacts: bool
222
+
223
+
224
+ @dataclass
225
+ class RecaptchaRuntimeEvidence:
226
+ """Runtime signals collected before/after execute."""
227
+
228
+ runtime_kind: str
229
+ detection_reason: str
230
+ scripts: list[str] = field(default_factory=list)
231
+ request_urls: list[str] = field(default_factory=list)
232
+ response_statuses: dict[str, int] = field(default_factory=dict)
233
+ has_standard_execute: bool = False
234
+ has_enterprise_execute: bool = False
235
+ enterprise_cfg: bool = False
236
+ api_domain: str = _DEFAULT_API_DOMAIN
237
+
238
+ def all_urls(self) -> list[str]:
239
+ return [*self.scripts, *self.request_urls, *self.response_statuses.keys()]
240
+
241
+
242
+ @dataclass(frozen=True)
243
+ class RecaptchaV3SolveResult:
244
+ """Final execution artifacts returned by `_solve_once()`."""
245
+
246
+ token: str
247
+ runtime_kind: str
248
+ fingerprint: BrowserFingerprint
249
+ session_artifacts: RecaptchaSessionArtifacts
250
+ create_time_ms: int
251
+
252
+
253
+ class RecaptchaNetworkObserver:
254
+ """Collect reCAPTCHA request evidence and the actual network fingerprint."""
255
+
256
+ def __init__(self) -> None:
257
+ self.request_urls: list[str] = []
258
+ self.response_statuses: dict[str, int] = {}
259
+ self._fingerprint = BrowserFingerprint()
260
+ self._network_token = ""
261
+ self._token_source_url = ""
262
+ self._response_tasks: set[asyncio.Task[None]] = set()
263
+
264
+ @staticmethod
265
+ def _is_relevant_url(url: str) -> bool:
266
+ return "/recaptcha/" in url
267
+
268
+ @staticmethod
269
+ def _is_token_response_url(url: str) -> bool:
270
+ return any(marker in url for marker in _TOKEN_RESPONSE_MARKERS)
271
+
272
+ @staticmethod
273
+ def extract_token_from_body(body: str) -> str:
274
+ for pattern in _TOKEN_RESPONSE_PATTERNS:
275
+ match = pattern.search(body)
276
+ if match:
277
+ return match.group(1)
278
+ return ""
279
+
280
+ def bind(self, page: Page) -> None:
281
+ page.on("request", self._capture_request)
282
+ page.on("response", self._capture_response)
283
+
284
+ def _capture_request(self, request: Request) -> None:
285
+ url = request.url
286
+ if not self._is_relevant_url(url):
287
+ return
288
+ self.request_urls.append(url)
289
+ headers = {
290
+ str(key).lower(): str(value)
291
+ for key, value in (request.headers or {}).items()
292
+ }
293
+ self._fingerprint = BrowserFingerprint(
294
+ user_agent=headers.get("user-agent", self._fingerprint.user_agent),
295
+ accept_language=headers.get(
296
+ "accept-language",
297
+ self._fingerprint.accept_language,
298
+ ),
299
+ sec_ch_ua=headers.get("sec-ch-ua", self._fingerprint.sec_ch_ua),
300
+ sec_ch_ua_mobile=headers.get(
301
+ "sec-ch-ua-mobile",
302
+ self._fingerprint.sec_ch_ua_mobile,
303
+ ),
304
+ sec_ch_ua_platform=headers.get(
305
+ "sec-ch-ua-platform",
306
+ self._fingerprint.sec_ch_ua_platform,
307
+ ),
308
+ )
309
+
310
+ def _capture_response(self, response: Response) -> None:
311
+ url = response.url
312
+ if not self._is_relevant_url(url):
313
+ return
314
+ self.response_statuses[url] = response.status
315
+ if self._is_token_response_url(url):
316
+ task = asyncio.create_task(self._capture_response_body(response))
317
+ self._response_tasks.add(task)
318
+ task.add_done_callback(self._response_tasks.discard)
319
+
320
+ def snapshot_fingerprint(self) -> BrowserFingerprint:
321
+ return self._fingerprint
322
+
323
+ @property
324
+ def network_token(self) -> str:
325
+ return self._network_token
326
+
327
+ @property
328
+ def token_source_url(self) -> str:
329
+ return self._token_source_url
330
+
331
+ async def flush(self) -> None:
332
+ if not self._response_tasks:
333
+ return
334
+ await asyncio.gather(*tuple(self._response_tasks), return_exceptions=True)
335
+
336
+ async def _capture_response_body(self, response: Response) -> None:
337
+ try:
338
+ body = await response.text()
339
+ except Exception as exc:
340
+ log.debug(
341
+ "Failed to read reCAPTCHA response body from %s: %s",
342
+ response.url,
343
+ exc,
344
+ )
345
+ return
346
+
347
+ token = self.extract_token_from_body(body)
348
+ if not token:
349
+ return
350
+
351
+ self._network_token = token
352
+ self._token_source_url = response.url
353
+
354
+
355
+ class RecaptchaV3Solver:
356
+ """Solves reCAPTCHA v3 tasks via runtime-aware Playwright automation."""
357
+
358
+ def __init__(self, config: Config) -> None:
359
+ self._config = config
360
+ self._playwright: Playwright | None = None
361
+ self._browser: Browser | None = None
362
+
363
+ @staticmethod
364
+ def _build_proxy_settings(raw_proxy_url: str) -> dict[str, str]:
365
+ parsed = urlparse(raw_proxy_url.strip())
366
+ if not parsed.scheme or not parsed.hostname or not parsed.port:
367
+ raise ValueError(
368
+ "BROWSER_PROXY_URL must use a full URL such as socks5://user:pass@host:port"
369
+ )
370
+
371
+ payload = {"server": f"{parsed.scheme}://{parsed.hostname}:{parsed.port}"}
372
+ if parsed.username:
373
+ payload["username"] = unquote(parsed.username)
374
+ if parsed.password:
375
+ payload["password"] = unquote(parsed.password)
376
+ return payload
377
+
378
+ @staticmethod
379
+ def _extract_browser_major_version(browser_version: str) -> str:
380
+ match = re.search(r"(\d+)", browser_version)
381
+ return match.group(1) if match else "131"
382
+
383
+ @staticmethod
384
+ def _build_chromium_user_agent(browser_version: str) -> str:
385
+ major = RecaptchaV3Solver._extract_browser_major_version(browser_version)
386
+ return (
387
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
388
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
389
+ f"Chrome/{major}.0.0.0 Safari/537.36"
390
+ )
391
+
392
+ def _resolve_browser_version(self) -> str:
393
+ if self._browser is None:
394
+ return ""
395
+
396
+ version_attr = getattr(self._browser, "version", "")
397
+ if callable(version_attr):
398
+ try:
399
+ return str(version_attr())
400
+ except Exception:
401
+ return ""
402
+ return str(version_attr or "")
403
+
404
+ def _build_browser_context_options(self) -> dict[str, Any]:
405
+ return {
406
+ "user_agent": self._build_chromium_user_agent(
407
+ self._resolve_browser_version()
408
+ ),
409
+ "viewport": {"width": 1920, "height": 1080},
410
+ "locale": "en-US",
411
+ "extra_http_headers": {
412
+ "Accept-Language": _BROWSER_ACCEPT_LANGUAGE,
413
+ },
414
+ }
415
+
416
+ async def start(self) -> None:
417
+ self._playwright = await async_playwright().start()
418
+ launch_options: dict[str, Any] = {
419
+ "headless": self._config.browser_headless,
420
+ "args": [
421
+ "--disable-blink-features=AutomationControlled",
422
+ "--no-sandbox",
423
+ "--disable-dev-shm-usage",
424
+ "--disable-gpu",
425
+ ],
426
+ }
427
+ if self._config.browser_proxy_url:
428
+ launch_options["proxy"] = self._build_proxy_settings(
429
+ self._config.browser_proxy_url
430
+ )
431
+
432
+ self._browser = await self._playwright.chromium.launch(**launch_options)
433
+ log.info(
434
+ "Playwright browser started (headless=%s proxy=%s)",
435
+ self._config.browser_headless,
436
+ "configured" if self._config.browser_proxy_url else "none",
437
+ )
438
+
439
+ async def stop(self) -> None:
440
+ if self._browser:
441
+ await self._browser.close()
442
+ if self._playwright:
443
+ await self._playwright.stop()
444
+ log.info("Playwright browser stopped")
445
+
446
+ async def solve(self, params: dict[str, Any]) -> dict[str, Any]:
447
+ profile = self._build_task_profile(params)
448
+ last_error: Exception | None = None
449
+ for attempt in range(self._config.captcha_retries):
450
+ try:
451
+ result = await self._solve_once(profile)
452
+ return self._build_solution_payload(result)
453
+ except Exception as exc:
454
+ last_error = exc
455
+ log.warning(
456
+ "reCAPTCHA v3 attempt %d/%d failed for %s (%s): %s",
457
+ attempt + 1,
458
+ self._config.captcha_retries,
459
+ profile.website_url,
460
+ profile.task_type,
461
+ exc,
462
+ )
463
+ if attempt < self._config.captcha_retries - 1:
464
+ await asyncio.sleep(2)
465
+
466
+ raise RuntimeError(
467
+ f"reCAPTCHA v3 failed after {self._config.captcha_retries} attempts: {last_error}"
468
+ )
469
+
470
+ @staticmethod
471
+ def _build_task_profile(params: dict[str, Any]) -> RecaptchaTaskProfile:
472
+ task_type = str(params.get("type") or "RecaptchaV3TaskProxyless")
473
+ website_url = str(params["websiteURL"])
474
+ website_key = str(params["websiteKey"])
475
+ page_action = str(params.get("pageAction") or "").strip()
476
+ requested_runtime = (
477
+ _ENTERPRISE_RUNTIME
478
+ if "enterprise" in task_type.lower()
479
+ else _STANDARD_RUNTIME
480
+ )
481
+ enterprise_payload = params.get("enterprisePayload") or {}
482
+ enterprise_s_token = ""
483
+ if isinstance(enterprise_payload, dict):
484
+ enterprise_s_token = str(enterprise_payload.get("s") or "").strip()
485
+
486
+ return RecaptchaTaskProfile(
487
+ task_type=task_type,
488
+ website_url=website_url,
489
+ website_key=website_key,
490
+ page_action=page_action,
491
+ requested_runtime=requested_runtime,
492
+ api_domain=RecaptchaV3Solver._normalize_api_domain(
493
+ str(params.get("apiDomain") or "")
494
+ ),
495
+ enterprise_s_token=enterprise_s_token,
496
+ wants_session_artifacts=bool(params.get("isSession")),
497
+ )
498
+
499
+ @staticmethod
500
+ def _normalize_api_domain(raw_domain: str) -> str:
501
+ value = raw_domain.strip()
502
+ if not value:
503
+ return _DEFAULT_API_DOMAIN
504
+ parsed = urlparse(value if "://" in value else f"https://{value}")
505
+ return parsed.netloc or parsed.path or _DEFAULT_API_DOMAIN
506
+
507
+ @staticmethod
508
+ def _build_loader_url(
509
+ runtime_kind: str,
510
+ api_domain: str,
511
+ website_key: str,
512
+ ) -> str:
513
+ domain = RecaptchaV3Solver._normalize_api_domain(api_domain)
514
+ if runtime_kind == _ENTERPRISE_RUNTIME:
515
+ return f"https://{domain}/recaptcha/enterprise.js?render={website_key}"
516
+ return f"https://{domain}/recaptcha/api.js?render={website_key}"
517
+
518
+ @staticmethod
519
+ def _determine_runtime_kind(
520
+ *,
521
+ requested_runtime: str,
522
+ scripts: list[str],
523
+ request_urls: list[str],
524
+ has_standard_execute: bool,
525
+ has_enterprise_execute: bool,
526
+ enterprise_cfg: bool,
527
+ ) -> tuple[str, str]:
528
+ evidence_urls = [*scripts, *request_urls]
529
+ if has_enterprise_execute or enterprise_cfg:
530
+ return _ENTERPRISE_RUNTIME, "page exposes grecaptcha.enterprise.execute"
531
+ if any("/recaptcha/enterprise" in url for url in evidence_urls):
532
+ return _ENTERPRISE_RUNTIME, "enterprise script or network evidence detected"
533
+ if has_standard_execute:
534
+ return _STANDARD_RUNTIME, "page exposes grecaptcha.execute"
535
+ if any(
536
+ "/recaptcha/api.js" in url or "/recaptcha/api2/" in url
537
+ for url in evidence_urls
538
+ ):
539
+ return _STANDARD_RUNTIME, "standard script or network evidence detected"
540
+ return requested_runtime, "no runtime evidence; falling back to task hint"
541
+
542
+ @staticmethod
543
+ def _has_runtime_network_evidence(
544
+ runtime_kind: str,
545
+ urls: list[str],
546
+ ) -> bool:
547
+ if runtime_kind == _ENTERPRISE_RUNTIME:
548
+ return any("/recaptcha/enterprise" in url for url in urls)
549
+ return any(
550
+ "/recaptcha/api.js" in url or "/recaptcha/api2/" in url
551
+ for url in urls
552
+ )
553
+
554
+ @staticmethod
555
+ def _extract_api_domain(urls: list[str], fallback: str) -> str:
556
+ for url in urls:
557
+ if "/recaptcha/" not in url:
558
+ continue
559
+ parsed = urlparse(url)
560
+ if parsed.netloc:
561
+ return parsed.netloc
562
+ return RecaptchaV3Solver._normalize_api_domain(fallback)
563
+
564
+ @staticmethod
565
+ def _build_solution_payload(result: RecaptchaV3SolveResult) -> dict[str, Any]:
566
+ payload: dict[str, Any] = {
567
+ "gRecaptchaResponse": result.token,
568
+ "createTime": result.create_time_ms,
569
+ "runtimeKind": result.runtime_kind,
570
+ }
571
+ payload.update(result.fingerprint.to_solution_fields())
572
+ payload.update(result.session_artifacts.to_solution_fields())
573
+ return payload
574
+
575
+ @staticmethod
576
+ def _select_best_token(
577
+ execute_token: str,
578
+ network_token: str,
579
+ network_token_source: str,
580
+ ) -> str:
581
+ execute_value = execute_token.strip()
582
+ observed_value = network_token.strip()
583
+ if not observed_value:
584
+ return execute_value
585
+ if execute_value and execute_value != observed_value:
586
+ log.warning(
587
+ "reCAPTCHA execute token differed from network token; using network token from %s",
588
+ network_token_source or "unknown-source",
589
+ )
590
+ return observed_value
591
+
592
+ async def _solve_once(self, profile: RecaptchaTaskProfile) -> RecaptchaV3SolveResult:
593
+ assert self._browser is not None
594
+
595
+ context = await self._browser.new_context(
596
+ **self._build_browser_context_options()
597
+ )
598
+ page = await context.new_page()
599
+ observer = RecaptchaNetworkObserver()
600
+ observer.bind(page)
601
+ await page.add_init_script(_STEALTH_JS)
602
+
603
+ try:
604
+ timeout_ms = self._config.browser_timeout * 1000
605
+ await page.goto(
606
+ profile.website_url,
607
+ wait_until="networkidle",
608
+ timeout=timeout_ms,
609
+ )
610
+ await self._simulate_human_activity(page)
611
+
612
+ initial_runtime = await self._probe_runtime(page, profile, observer)
613
+ if initial_runtime.runtime_kind != profile.requested_runtime:
614
+ log.info(
615
+ "Runtime probe selected %s for %s (requested=%s, reason=%s)",
616
+ initial_runtime.runtime_kind,
617
+ profile.website_url,
618
+ profile.requested_runtime,
619
+ initial_runtime.detection_reason,
620
+ )
621
+
622
+ execute_token = await self._execute_for_runtime(
623
+ page,
624
+ profile,
625
+ initial_runtime,
626
+ )
627
+ await observer.flush()
628
+ token = self._select_best_token(
629
+ execute_token=execute_token,
630
+ network_token=observer.network_token,
631
+ network_token_source=observer.token_source_url,
632
+ )
633
+ if not isinstance(token, str) or len(token) < 20:
634
+ raise RuntimeError(f"Invalid token received: {token!r}")
635
+
636
+ await asyncio.sleep(0.35)
637
+ await observer.flush()
638
+ final_runtime = await self._probe_runtime(page, profile, observer)
639
+ if not self._has_runtime_network_evidence(
640
+ final_runtime.runtime_kind,
641
+ final_runtime.all_urls(),
642
+ ):
643
+ raise RuntimeError(
644
+ f"No {final_runtime.runtime_kind} runtime evidence observed after execute"
645
+ )
646
+
647
+ fingerprint = await self._capture_fingerprint(page, observer)
648
+ session_artifacts = (
649
+ await self._capture_session_artifacts(context)
650
+ if profile.wants_session_artifacts
651
+ else RecaptchaSessionArtifacts()
652
+ )
653
+ result = RecaptchaV3SolveResult(
654
+ token=token,
655
+ runtime_kind=final_runtime.runtime_kind,
656
+ fingerprint=fingerprint,
657
+ session_artifacts=session_artifacts,
658
+ create_time_ms=int(time.time() * 1000),
659
+ )
660
+ log.info(
661
+ "Got reCAPTCHA v3 token for %s (runtime=%s len=%d ua=%s ca_t=%s ca_e=%s)",
662
+ profile.website_url,
663
+ result.runtime_kind,
664
+ len(result.token),
665
+ "yes" if result.fingerprint.user_agent else "no",
666
+ "yes" if result.session_artifacts.recaptcha_ca_t else "no",
667
+ "yes" if result.session_artifacts.recaptcha_ca_e else "no",
668
+ )
669
+ return result
670
+ finally:
671
+ await context.close()
672
+
673
+ async def _simulate_human_activity(self, page: Page) -> None:
674
+ await page.mouse.move(400, 300)
675
+ await asyncio.sleep(1)
676
+ await page.mouse.move(600, 400)
677
+ await asyncio.sleep(0.5)
678
+
679
+ async def _probe_runtime(
680
+ self,
681
+ page: Page,
682
+ profile: RecaptchaTaskProfile,
683
+ observer: RecaptchaNetworkObserver,
684
+ ) -> RecaptchaRuntimeEvidence:
685
+ raw = await page.evaluate(_RUNTIME_INSPECTION_JS)
686
+ scripts = list(raw.get("scripts") or [])
687
+ request_urls = list(observer.request_urls)
688
+ runtime_kind, detection_reason = self._determine_runtime_kind(
689
+ requested_runtime=profile.requested_runtime,
690
+ scripts=scripts,
691
+ request_urls=request_urls,
692
+ has_standard_execute=bool(raw.get("hasStandardExecute")),
693
+ has_enterprise_execute=bool(raw.get("hasEnterpriseExecute")),
694
+ enterprise_cfg=bool(raw.get("enterpriseCfg")),
695
+ )
696
+ return RecaptchaRuntimeEvidence(
697
+ runtime_kind=runtime_kind,
698
+ detection_reason=detection_reason,
699
+ scripts=scripts,
700
+ request_urls=request_urls,
701
+ response_statuses=dict(observer.response_statuses),
702
+ has_standard_execute=bool(raw.get("hasStandardExecute")),
703
+ has_enterprise_execute=bool(raw.get("hasEnterpriseExecute")),
704
+ enterprise_cfg=bool(raw.get("enterpriseCfg")),
705
+ api_domain=self._extract_api_domain(
706
+ [*scripts, *request_urls],
707
+ fallback=profile.api_domain,
708
+ ),
709
+ )
710
+
711
+ async def _execute_for_runtime(
712
+ self,
713
+ page: Page,
714
+ profile: RecaptchaTaskProfile,
715
+ runtime: RecaptchaRuntimeEvidence,
716
+ ) -> str:
717
+ loader_domain = runtime.api_domain or profile.api_domain
718
+ if runtime.runtime_kind == _ENTERPRISE_RUNTIME:
719
+ await self._ensure_runtime_loaded(
720
+ page=page,
721
+ ready_expression=_WAIT_FOR_ENTERPRISE_RUNTIME_JS,
722
+ loader_url=self._build_loader_url(
723
+ _ENTERPRISE_RUNTIME,
724
+ loader_domain,
725
+ profile.website_key,
726
+ ),
727
+ )
728
+ return await page.evaluate(
729
+ _EXECUTE_ENTERPRISE_JS,
730
+ [
731
+ profile.website_key,
732
+ profile.page_action,
733
+ profile.enterprise_s_token,
734
+ ],
735
+ )
736
+
737
+ await self._ensure_runtime_loaded(
738
+ page=page,
739
+ ready_expression=_WAIT_FOR_STANDARD_RUNTIME_JS,
740
+ loader_url=self._build_loader_url(
741
+ _STANDARD_RUNTIME,
742
+ loader_domain,
743
+ profile.website_key,
744
+ ),
745
+ )
746
+ return await page.evaluate(
747
+ _EXECUTE_STANDARD_JS,
748
+ [profile.website_key, profile.page_action],
749
+ )
750
+
751
+ async def _ensure_runtime_loaded(
752
+ self,
753
+ *,
754
+ page: Page,
755
+ ready_expression: str,
756
+ loader_url: str,
757
+ ) -> None:
758
+ try:
759
+ await page.wait_for_function(ready_expression, timeout=5_000)
760
+ return
761
+ except Exception:
762
+ log.info("reCAPTCHA runtime not ready, injecting %s", loader_url)
763
+
764
+ await page.evaluate(_ENSURE_SCRIPT_LOADED_JS, [loader_url])
765
+ await page.wait_for_function(ready_expression, timeout=10_000)
766
+
767
+ async def _capture_fingerprint(
768
+ self,
769
+ page: Page,
770
+ observer: RecaptchaNetworkObserver,
771
+ ) -> BrowserFingerprint:
772
+ network_fp = observer.snapshot_fingerprint()
773
+ if network_fp.user_agent or network_fp.sec_ch_ua or network_fp.accept_language:
774
+ return network_fp
775
+
776
+ fallback = await page.evaluate(_FALLBACK_FINGERPRINT_JS)
777
+ return BrowserFingerprint(
778
+ user_agent=str(fallback.get("userAgent") or ""),
779
+ accept_language=str(fallback.get("acceptLanguage") or ""),
780
+ sec_ch_ua=str(fallback.get("secChUa") or ""),
781
+ sec_ch_ua_mobile=str(fallback.get("secChUaMobile") or ""),
782
+ sec_ch_ua_platform=str(fallback.get("secChUaPlatform") or ""),
783
+ )
784
+
785
+ async def _capture_session_artifacts(
786
+ self,
787
+ context: Any,
788
+ ) -> RecaptchaSessionArtifacts:
789
+ cookies = await context.cookies()
790
+ recaptcha_ca_t = None
791
+ recaptcha_ca_e = None
792
+ for cookie in cookies:
793
+ name = str(cookie.get("name") or "")
794
+ value = str(cookie.get("value") or "")
795
+ if not value:
796
+ continue
797
+ if name == "recaptcha-ca-t":
798
+ recaptcha_ca_t = value
799
+ elif name == "recaptcha-ca-e":
800
+ recaptcha_ca_e = value
801
+ return RecaptchaSessionArtifacts(
802
+ recaptcha_ca_t=recaptcha_ca_t,
803
+ recaptcha_ca_e=recaptcha_ca_e,
804
+ )