diff --git a/README.md b/README.md index 7791ebc..f0a62d5 100644 --- a/README.md +++ b/README.md @@ -330,12 +330,13 @@ that the target page expects in `cf-turnstile-response`. | `action` | no | Value of the widget's `data-action` attribute, if set. | | `data` | no | Custom payload from the widget's `data-cdata` attribute, if set. | | `pagedata` | no | Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. | -| `userAgent` | no | User-Agent to solve with -- the returned token is tied to it, submit with the same one. | +| `userAgent` | no | Current browser User-Agent. Pass it for Cloudflare Challenge pages; it is not needed for a standalone widget. | **Response:** `token` -- submit as `cf-turnstile-response`. For Cloudflare -Challenge pages, the solution also includes `userAgent`; use that User-Agent -when submitting the token. Pass `action`, `data`, and `pagedata` when the page -provides them. The API field is spelled `pagedata`, all lowercase. +Challenge pages, pass `action`, `data`, `pagedata`, and the current browser +`userAgent` in the task. The solution also contains `userAgent`; switch the +browser or HTTP client to this returned value before invoking the callback with +the token. The API field is spelled `pagedata`, all lowercase. ```python from captcha_solver_api import CaptchaClient @@ -343,10 +344,15 @@ from captcha_solver_api.tasks import TurnstileTaskProxyless client = CaptchaClient("your_api_key") task = TurnstileTaskProxyless( websiteURL="https://example.com/login", - websiteKey="YOUR_WEBSITE_KEY" + websiteKey="YOUR_WEBSITE_KEY", + action="managed", + data="INTERCEPTED_CDATA", + pagedata="INTERCEPTED_CHL_PAGE_DATA", + userAgent="CURRENT_BROWSER_USER_AGENT", ) result = client.solve(task) print(result["token"]) +print(result.get("userAgent")) ``` With proxy, use `TurnstileTask` (same proxy fields as reCAPTCHA v2). @@ -712,6 +718,7 @@ Full API reference: https://captcha-solver.com/en/docs/captcha-types ## Useful Links +- [Cloudflare Turnstile Puppeteer Demo](https://github.com/captcha-solver-api/cloudflare-turnstile-puppeteer-demo) — a working browser automation example for Cloudflare Turnstile. - [How to Automate Tencent CAPTCHA](https://captcha-solver.com/en/blog/how-to-automate-tencent-captcha) — a step-by-step guide with Python and JavaScript SDK examples. ## License diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index cec0f27..c9dc260 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -303,11 +303,14 @@ class TurnstileTaskProxyless(BaseTask): challenge pages beyond the basic widget. Named lowercase (not `pageData`) to match the API field name exactly -- Cloudflare-specific fields are the one place this API doesn't camelCase. - userAgent: User-Agent to solve with. The returned token is tied to it -- - submit it with the same User-Agent. + userAgent: User-Agent of the browser that loaded the challenge. Pass it + together with `action`, `data`, and `pagedata` for Cloudflare + Challenge pages. It is not needed for a standalone widget. Returns (`solution` from `solve()`): `token` -- the value to submit as `cf-turnstile-response`. + `userAgent` -- for Cloudflare Challenge pages, switch the browser or HTTP + client to this returned User-Agent before invoking the callback. """ type = "TurnstileTaskProxyless" @@ -345,10 +348,14 @@ class TurnstileTask(BaseTask, ProxyMixin): pagedata: Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. Named lowercase (not `pageData`) to match the API field name exactly. - userAgent: User-Agent to solve with. The returned token is tied to it. + userAgent: User-Agent of the browser that loaded the challenge. Pass it + for Cloudflare Challenge pages; it is not needed for a standalone + widget. Returns (`solution` from `solve()`): `token` -- the value to submit as `cf-turnstile-response`. + `userAgent` -- for Cloudflare Challenge pages, switch the browser or HTTP + client to this returned User-Agent before invoking the callback. """ type = "TurnstileTask" diff --git a/tests/sync/test_turnstile.py b/tests/sync/test_turnstile.py index 035b53a..cd39e7b 100644 --- a/tests/sync/test_turnstile.py +++ b/tests/sync/test_turnstile.py @@ -9,6 +9,18 @@ class TestTurnstile: + def test_proxyless_includes_challenge_user_agent(self): + task = TurnstileTaskProxyless( + websiteURL="https://example.com", + websiteKey="test_key", + action="managed", + data="cdata", + pagedata="page-data", + userAgent="Mozilla/5.0", + ) + + assert task.to_dict()["userAgent"] == "Mozilla/5.0" + def test_proxyless_to_dict(self): task = TurnstileTaskProxyless( websiteURL="https://example.com", @@ -25,12 +37,14 @@ def test_task_with_proxy_to_dict(self): proxyType="http", proxyAddress="1.2.3.4", proxyPort=8080, + userAgent="Mozilla/5.0", ) result = task.to_dict() assert result["type"] == "TurnstileTask" assert result["proxyType"] == "http" assert result["proxyAddress"] == "1.2.3.4" + assert result["userAgent"] == "Mozilla/5.0" def test_solve(self): client = CaptchaClient("test_key", polling_interval=0.1) @@ -39,8 +53,15 @@ def test_solve(self): with patch.object(client, "_request") as mock_request: mock_request.side_effect = [ {"errorId": 0, "taskId": 103}, - {"errorId": 0, "status": "ready", "solution": {"token": "turnstile_token"}}, + { + "errorId": 0, + "status": "ready", + "solution": { + "token": "turnstile_token", + "userAgent": "returned-agent", + }, + }, ] result = client.solve(task) - assert result == {"token": "turnstile_token"} + assert result == {"token": "turnstile_token", "userAgent": "returned-agent"}