botasaurus-turnstile 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- botasaurus_turnstile-0.1.0/.gitignore +5 -0
- botasaurus_turnstile-0.1.0/LICENSE +21 -0
- botasaurus_turnstile-0.1.0/PKG-INFO +109 -0
- botasaurus_turnstile-0.1.0/README.md +85 -0
- botasaurus_turnstile-0.1.0/assets/peak-banner.png +0 -0
- botasaurus_turnstile-0.1.0/botasaurus_turnstile/__init__.py +34 -0
- botasaurus_turnstile-0.1.0/botasaurus_turnstile/peak.py +110 -0
- botasaurus_turnstile-0.1.0/botasaurus_turnstile/solver.py +123 -0
- botasaurus_turnstile-0.1.0/examples/solve_login.py +18 -0
- botasaurus_turnstile-0.1.0/pyproject.toml +45 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: botasaurus-turnstile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Solve Cloudflare Turnstile in Botasaurus via the Peak API — read the sitekey, get a token, inject it. For when the anti-detect driver still stalls on the widget.
|
|
5
|
+
Project-URL: Homepage, https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile
|
|
6
|
+
Project-URL: Documentation, https://peak.fo/docs/turnstile?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile
|
|
7
|
+
Project-URL: Pricing, https://peak.fo/pricing?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile
|
|
8
|
+
Project-URL: Source, https://github.com/CircuitSavage/botasaurus-turnstile
|
|
9
|
+
Author: Peak
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: anti-detect,botasaurus,bypass,captcha,cf-clearance,cloudflare,scraping,solver,turnstile
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Provides-Extra: botasaurus
|
|
22
|
+
Requires-Dist: botasaurus-driver>=4.0; extra == 'botasaurus'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<a href="https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/CircuitSavage/botasaurus-turnstile/main/assets/peak-banner.png" alt="Peak — solve Cloudflare Turnstile & the 5s challenge in ~1s" width="100%">
|
|
27
|
+
</a>
|
|
28
|
+
|
|
29
|
+
# botasaurus-turnstile
|
|
30
|
+
|
|
31
|
+
**botasaurus-turnstile** solves Cloudflare Turnstile on a [Botasaurus](https://github.com/omkarcloud/botasaurus) driver by injecting a Peak-solved token, so a protected form or navigation proceeds.
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
Botasaurus drives an anti-detect Chrome and ships a lot of Cloudflare handling, which is why people run it against protected sites. The interstitial often clears. The Turnstile *widget* on a datacenter IP is the part that doesn't — it renders, waits for a click or a token that never comes, and the run stalls.
|
|
36
|
+
|
|
37
|
+
This reads the sitekey from the DOM, gets a token from Peak, and injects it as the `cf-turnstile-response` value plus the widget's success callback. When the driver gets you to the page but the widget won't clear, drop in a Peak API key and it goes through.
|
|
38
|
+
|
|
39
|
+
## Powered by Peak
|
|
40
|
+
|
|
41
|
+
This package uses [Peak](https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile) to solve Turnstile.
|
|
42
|
+
- Solve Cloudflare Turnstile & the 5s challenge in about a second
|
|
43
|
+
- $0.90 per 1,000 successful Turnstile solves, down to $0.35 at volume
|
|
44
|
+
- 1,000 free solves to start, no card.
|
|
45
|
+
|
|
46
|
+
→ [Get your free API key](https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile) · [Docs](https://peak.fo/docs/turnstile?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile) · [Pricing](https://peak.fo/pricing?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile)
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install botasaurus-turnstile botasaurus-driver
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import os
|
|
58
|
+
from botasaurus.browser import browser, Driver
|
|
59
|
+
from botasaurus_turnstile import solve_turnstile
|
|
60
|
+
|
|
61
|
+
os.environ["PEAK_API_KEY"] = "pk_your_api_key"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@browser
|
|
65
|
+
def scrape(driver: Driver, data):
|
|
66
|
+
driver.get("https://protected.example/login")
|
|
67
|
+
|
|
68
|
+
# Reads the sitekey from the page, solves via Peak, injects the token.
|
|
69
|
+
solve_turnstile(driver)
|
|
70
|
+
|
|
71
|
+
# the form / navigation can now proceed
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
scrape()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Works the same with a standalone `botasaurus_driver.Driver()` — anything that exposes `run_js` and `current_url`.
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
1. **Read the sitekey.** A `run_js` DOM scan finds the `data-sitekey` on the `.cf-turnstile` widget, or the sitekey set through a JS `render()` call.
|
|
82
|
+
2. **Solve via Peak.** `POST https://api.peak.fo/solve` with `task_type: "turnstiletask"`, the sitekey, and the driver's current URL. Pass `proxy=` to mint the token from your browser's egress IP.
|
|
83
|
+
3. **Inject the token.** `run_js` sets every `cf-turnstile-response` field, fires `input`/`change`, and calls the widget's `data-callback` so the form or navigation continues.
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
solve_turnstile(
|
|
89
|
+
driver, # a Botasaurus Driver
|
|
90
|
+
api_key=None, # defaults to env PEAK_API_KEY
|
|
91
|
+
proxy=None, # optional http://user:pass@ip:port, forwarded to Peak
|
|
92
|
+
sitekey=None, # override auto-detection (e.g. widget in a frame)
|
|
93
|
+
url=None, # defaults to driver.current_url
|
|
94
|
+
task_type="turnstiletask",
|
|
95
|
+
timeout=180.0,
|
|
96
|
+
) -> str # the injected token
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`read_sitekey(driver)` and `inject_token(driver, token)` are exposed for finer control. Peak also supports `task_type="cloudflare5stask"` for the 5s interstitial.
|
|
100
|
+
|
|
101
|
+
The API key is read from `PEAK_API_KEY` by default; you can also pass it explicitly. Never hardcode a real key.
|
|
102
|
+
|
|
103
|
+
## Legitimate use
|
|
104
|
+
|
|
105
|
+
For automation, QA, and scraping public data you are allowed to access. Respect each target's Terms of Service and robots directives, and do not use this for credential-stuffing or other abuse.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<a href="https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/CircuitSavage/botasaurus-turnstile/main/assets/peak-banner.png" alt="Peak — solve Cloudflare Turnstile & the 5s challenge in ~1s" width="100%">
|
|
3
|
+
</a>
|
|
4
|
+
|
|
5
|
+
# botasaurus-turnstile
|
|
6
|
+
|
|
7
|
+
**botasaurus-turnstile** solves Cloudflare Turnstile on a [Botasaurus](https://github.com/omkarcloud/botasaurus) driver by injecting a Peak-solved token, so a protected form or navigation proceeds.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Botasaurus drives an anti-detect Chrome and ships a lot of Cloudflare handling, which is why people run it against protected sites. The interstitial often clears. The Turnstile *widget* on a datacenter IP is the part that doesn't — it renders, waits for a click or a token that never comes, and the run stalls.
|
|
12
|
+
|
|
13
|
+
This reads the sitekey from the DOM, gets a token from Peak, and injects it as the `cf-turnstile-response` value plus the widget's success callback. When the driver gets you to the page but the widget won't clear, drop in a Peak API key and it goes through.
|
|
14
|
+
|
|
15
|
+
## Powered by Peak
|
|
16
|
+
|
|
17
|
+
This package uses [Peak](https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile) to solve Turnstile.
|
|
18
|
+
- Solve Cloudflare Turnstile & the 5s challenge in about a second
|
|
19
|
+
- $0.90 per 1,000 successful Turnstile solves, down to $0.35 at volume
|
|
20
|
+
- 1,000 free solves to start, no card.
|
|
21
|
+
|
|
22
|
+
→ [Get your free API key](https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile) · [Docs](https://peak.fo/docs/turnstile?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile) · [Pricing](https://peak.fo/pricing?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile)
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install botasaurus-turnstile botasaurus-driver
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import os
|
|
34
|
+
from botasaurus.browser import browser, Driver
|
|
35
|
+
from botasaurus_turnstile import solve_turnstile
|
|
36
|
+
|
|
37
|
+
os.environ["PEAK_API_KEY"] = "pk_your_api_key"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@browser
|
|
41
|
+
def scrape(driver: Driver, data):
|
|
42
|
+
driver.get("https://protected.example/login")
|
|
43
|
+
|
|
44
|
+
# Reads the sitekey from the page, solves via Peak, injects the token.
|
|
45
|
+
solve_turnstile(driver)
|
|
46
|
+
|
|
47
|
+
# the form / navigation can now proceed
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
scrape()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Works the same with a standalone `botasaurus_driver.Driver()` — anything that exposes `run_js` and `current_url`.
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
1. **Read the sitekey.** A `run_js` DOM scan finds the `data-sitekey` on the `.cf-turnstile` widget, or the sitekey set through a JS `render()` call.
|
|
58
|
+
2. **Solve via Peak.** `POST https://api.peak.fo/solve` with `task_type: "turnstiletask"`, the sitekey, and the driver's current URL. Pass `proxy=` to mint the token from your browser's egress IP.
|
|
59
|
+
3. **Inject the token.** `run_js` sets every `cf-turnstile-response` field, fires `input`/`change`, and calls the widget's `data-callback` so the form or navigation continues.
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
solve_turnstile(
|
|
65
|
+
driver, # a Botasaurus Driver
|
|
66
|
+
api_key=None, # defaults to env PEAK_API_KEY
|
|
67
|
+
proxy=None, # optional http://user:pass@ip:port, forwarded to Peak
|
|
68
|
+
sitekey=None, # override auto-detection (e.g. widget in a frame)
|
|
69
|
+
url=None, # defaults to driver.current_url
|
|
70
|
+
task_type="turnstiletask",
|
|
71
|
+
timeout=180.0,
|
|
72
|
+
) -> str # the injected token
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`read_sitekey(driver)` and `inject_token(driver, token)` are exposed for finer control. Peak also supports `task_type="cloudflare5stask"` for the 5s interstitial.
|
|
76
|
+
|
|
77
|
+
The API key is read from `PEAK_API_KEY` by default; you can also pass it explicitly. Never hardcode a real key.
|
|
78
|
+
|
|
79
|
+
## Legitimate use
|
|
80
|
+
|
|
81
|
+
For automation, QA, and scraping public data you are allowed to access. Respect each target's Terms of Service and robots directives, and do not use this for credential-stuffing or other abuse.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT — see [LICENSE](./LICENSE).
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""botasaurus-turnstile: solve Cloudflare Turnstile in Botasaurus via Peak.
|
|
2
|
+
|
|
3
|
+
Public API::
|
|
4
|
+
|
|
5
|
+
from botasaurus_turnstile import solve_turnstile
|
|
6
|
+
token = solve_turnstile(driver, api_key="pk_...")
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .peak import (
|
|
12
|
+
DEFAULT_API_URL,
|
|
13
|
+
TASK_CLOUDFLARE_5S,
|
|
14
|
+
TASK_TURNSTILE,
|
|
15
|
+
PeakClient,
|
|
16
|
+
PeakError,
|
|
17
|
+
build_solve_payload,
|
|
18
|
+
)
|
|
19
|
+
from .solver import inject_token, read_sitekey, solve_turnstile
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"solve_turnstile",
|
|
25
|
+
"read_sitekey",
|
|
26
|
+
"inject_token",
|
|
27
|
+
"PeakClient",
|
|
28
|
+
"PeakError",
|
|
29
|
+
"build_solve_payload",
|
|
30
|
+
"DEFAULT_API_URL",
|
|
31
|
+
"TASK_TURNSTILE",
|
|
32
|
+
"TASK_CLOUDFLARE_5S",
|
|
33
|
+
"__version__",
|
|
34
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Peak API client for solving Cloudflare Turnstile.
|
|
2
|
+
|
|
3
|
+
Kept free of any Botasaurus dependency so it can be unit tested and reused
|
|
4
|
+
on its own. The HTTP call uses the standard library only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
DEFAULT_API_URL = "https://api.peak.fo/solve"
|
|
15
|
+
|
|
16
|
+
# task_type sent to Peak for the Turnstile widget challenge.
|
|
17
|
+
TASK_TURNSTILE = "turnstiletask"
|
|
18
|
+
# Peak also supports the 5s interstitial via this task_type.
|
|
19
|
+
TASK_CLOUDFLARE_5S = "cloudflare5stask"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PeakError(RuntimeError):
|
|
23
|
+
"""Raised when Peak cannot solve the challenge or the call fails."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_solve_payload(
|
|
27
|
+
sitekey: str,
|
|
28
|
+
url: str,
|
|
29
|
+
proxy: Optional[str] = None,
|
|
30
|
+
task_type: str = TASK_TURNSTILE,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Build the JSON body for POST https://api.peak.fo/solve.
|
|
33
|
+
|
|
34
|
+
``proxy`` is omitted entirely when not provided, per the Peak contract.
|
|
35
|
+
"""
|
|
36
|
+
payload = {
|
|
37
|
+
"task_type": task_type,
|
|
38
|
+
"sitekey": sitekey,
|
|
39
|
+
"url": url,
|
|
40
|
+
}
|
|
41
|
+
if proxy:
|
|
42
|
+
payload["proxy"] = proxy
|
|
43
|
+
return payload
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PeakClient:
|
|
47
|
+
"""Thin client around the Peak solve endpoint."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
api_key: str,
|
|
52
|
+
api_url: str = DEFAULT_API_URL,
|
|
53
|
+
proxy: Optional[str] = None,
|
|
54
|
+
timeout: float = 180.0,
|
|
55
|
+
) -> None:
|
|
56
|
+
if not api_key:
|
|
57
|
+
raise PeakError(
|
|
58
|
+
"No Peak API key. Set PEAK_API_KEY (get a free key at "
|
|
59
|
+
"https://peak.fo)."
|
|
60
|
+
)
|
|
61
|
+
self.api_key = api_key
|
|
62
|
+
self.api_url = api_url
|
|
63
|
+
self.proxy = proxy
|
|
64
|
+
self.timeout = timeout
|
|
65
|
+
|
|
66
|
+
def _post(self, payload: dict) -> dict:
|
|
67
|
+
data = json.dumps(payload).encode("utf-8")
|
|
68
|
+
req = urllib.request.Request(
|
|
69
|
+
self.api_url,
|
|
70
|
+
data=data,
|
|
71
|
+
method="POST",
|
|
72
|
+
headers={
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"X-API-Key": self.api_key,
|
|
75
|
+
"User-Agent": "botasaurus-turnstile/0.1.0 (+https://github.com/CircuitSavage/botasaurus-turnstile)",
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
80
|
+
body = resp.read().decode("utf-8")
|
|
81
|
+
except urllib.error.HTTPError as exc: # pragma: no cover - network path
|
|
82
|
+
body = exc.read().decode("utf-8", "replace")
|
|
83
|
+
except urllib.error.URLError as exc: # pragma: no cover - network path
|
|
84
|
+
raise PeakError(f"Peak request failed: {exc}") from exc
|
|
85
|
+
try:
|
|
86
|
+
return json.loads(body)
|
|
87
|
+
except ValueError as exc: # pragma: no cover - defensive
|
|
88
|
+
raise PeakError(f"Peak returned non-JSON response: {body!r}") from exc
|
|
89
|
+
|
|
90
|
+
def solve(
|
|
91
|
+
self,
|
|
92
|
+
sitekey: str,
|
|
93
|
+
url: str,
|
|
94
|
+
proxy: Optional[str] = None,
|
|
95
|
+
task_type: str = TASK_TURNSTILE,
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Solve a Turnstile challenge and return the token string.
|
|
98
|
+
|
|
99
|
+
Raises :class:`PeakError` on failure.
|
|
100
|
+
"""
|
|
101
|
+
payload = build_solve_payload(
|
|
102
|
+
sitekey, url, proxy=proxy or self.proxy, task_type=task_type
|
|
103
|
+
)
|
|
104
|
+
result = self._post(payload)
|
|
105
|
+
if not result.get("success"):
|
|
106
|
+
raise PeakError(result.get("error") or "Peak solve failed")
|
|
107
|
+
token = (result.get("data") or {}).get("token")
|
|
108
|
+
if not token:
|
|
109
|
+
raise PeakError("Peak response missing data.token")
|
|
110
|
+
return token
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Solve Cloudflare Turnstile on a live Botasaurus driver via the Peak API.
|
|
2
|
+
|
|
3
|
+
Botasaurus drives an anti-detect Chrome and is a common pick against
|
|
4
|
+
Cloudflare. When the Turnstile widget still stalls, this reads the sitekey,
|
|
5
|
+
gets a token from Peak, and injects it so the form or navigation proceeds.
|
|
6
|
+
|
|
7
|
+
Works with the Botasaurus ``Driver`` (uses ``run_js`` and ``current_url``).
|
|
8
|
+
Botasaurus itself is an optional import.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from .peak import DEFAULT_API_URL, TASK_TURNSTILE, PeakClient, PeakError
|
|
18
|
+
|
|
19
|
+
_READ_SITEKEY_JS = r"""
|
|
20
|
+
var el = document.querySelector('[data-sitekey]');
|
|
21
|
+
if (el) { return el.getAttribute('data-sitekey'); }
|
|
22
|
+
var m = (document.documentElement.outerHTML || '').match(
|
|
23
|
+
/(?:sitekey|render)\s*[:=]\s*["']((?:0x|1x)[^"']+)["']/i);
|
|
24
|
+
return m ? m[1] : null;
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Injects a solved token: sets every cf-turnstile-response field, fires
|
|
28
|
+
# input/change so listeners react, and calls the widget's success callback.
|
|
29
|
+
# The token is embedded as a JSON literal in inject_token.
|
|
30
|
+
_INJECT_TOKEN_BODY = r"""
|
|
31
|
+
var fields = document.querySelectorAll(
|
|
32
|
+
'input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"], ' +
|
|
33
|
+
'input#cf-turnstile-response, [name="cf-turnstile-response"]');
|
|
34
|
+
if (!fields.length) {
|
|
35
|
+
var holder = document.querySelector('.cf-turnstile') || document.body;
|
|
36
|
+
var inp = document.createElement('input');
|
|
37
|
+
inp.type = 'hidden';
|
|
38
|
+
inp.name = 'cf-turnstile-response';
|
|
39
|
+
holder.appendChild(inp);
|
|
40
|
+
fields = [inp];
|
|
41
|
+
}
|
|
42
|
+
fields.forEach(function (f) {
|
|
43
|
+
f.value = token;
|
|
44
|
+
f.dispatchEvent(new Event('input', { bubbles: true }));
|
|
45
|
+
f.dispatchEvent(new Event('change', { bubbles: true }));
|
|
46
|
+
});
|
|
47
|
+
var called = false;
|
|
48
|
+
document.querySelectorAll('[data-callback]').forEach(function (w) {
|
|
49
|
+
var name = w.getAttribute('data-callback');
|
|
50
|
+
if (name && typeof window[name] === 'function') {
|
|
51
|
+
window[name](token);
|
|
52
|
+
called = true;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return called;
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_sitekey(driver) -> Optional[str]:
|
|
60
|
+
"""Return the Turnstile sitekey on the current page, or None."""
|
|
61
|
+
sitekey = driver.run_js(_READ_SITEKEY_JS)
|
|
62
|
+
if sitekey:
|
|
63
|
+
return str(sitekey).strip()
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def inject_token(driver, token: str) -> None:
|
|
68
|
+
"""Inject a solved token into the page's Turnstile widget."""
|
|
69
|
+
driver.run_js("var token = " + json.dumps(token) + ";\n" + _INJECT_TOKEN_BODY)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _current_url(driver) -> Optional[str]:
|
|
73
|
+
try:
|
|
74
|
+
url = getattr(driver, "current_url", None)
|
|
75
|
+
if url:
|
|
76
|
+
return url
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
try:
|
|
80
|
+
return driver.run_js("return window.location.href")
|
|
81
|
+
except Exception:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def solve_turnstile(
|
|
86
|
+
driver,
|
|
87
|
+
api_key: Optional[str] = None,
|
|
88
|
+
proxy: Optional[str] = None,
|
|
89
|
+
sitekey: Optional[str] = None,
|
|
90
|
+
url: Optional[str] = None,
|
|
91
|
+
task_type: str = TASK_TURNSTILE,
|
|
92
|
+
api_url: Optional[str] = None,
|
|
93
|
+
timeout: float = 180.0,
|
|
94
|
+
) -> str:
|
|
95
|
+
"""Solve the Turnstile challenge on ``driver``'s current page.
|
|
96
|
+
|
|
97
|
+
Reads the sitekey from the DOM (unless ``sitekey`` is given), asks Peak
|
|
98
|
+
for a token, injects it, and returns the token. ``api_key`` defaults to
|
|
99
|
+
the ``PEAK_API_KEY`` environment variable; ``url`` defaults to the
|
|
100
|
+
driver's current URL. Pass ``proxy`` to mint the token from the browser's
|
|
101
|
+
egress IP; omit it and Peak solves without one.
|
|
102
|
+
|
|
103
|
+
Raises :class:`~botasaurus_turnstile.peak.PeakError` on failure.
|
|
104
|
+
"""
|
|
105
|
+
api_key = api_key or os.environ.get("PEAK_API_KEY")
|
|
106
|
+
|
|
107
|
+
if sitekey is None:
|
|
108
|
+
sitekey = read_sitekey(driver)
|
|
109
|
+
if not sitekey:
|
|
110
|
+
raise PeakError(
|
|
111
|
+
"No Turnstile sitekey found on the page. Pass sitekey= "
|
|
112
|
+
"explicitly if the widget renders in a frame or after load."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if url is None:
|
|
116
|
+
url = _current_url(driver)
|
|
117
|
+
|
|
118
|
+
client = PeakClient(
|
|
119
|
+
api_key, api_url=api_url or DEFAULT_API_URL, proxy=proxy, timeout=timeout
|
|
120
|
+
)
|
|
121
|
+
token = client.solve(sitekey, url, proxy=proxy, task_type=task_type)
|
|
122
|
+
inject_token(driver, token)
|
|
123
|
+
return token
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from botasaurus.browser import browser, Driver
|
|
4
|
+
from botasaurus_turnstile import solve_turnstile
|
|
5
|
+
|
|
6
|
+
# Get a free key at https://peak.fo
|
|
7
|
+
os.environ.setdefault("PEAK_API_KEY", "pk_your_api_key")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@browser
|
|
11
|
+
def scrape(driver: Driver, data):
|
|
12
|
+
driver.get("https://protected.example/login")
|
|
13
|
+
token = solve_turnstile(driver)
|
|
14
|
+
print("solved, token length:", len(token))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
scrape()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "botasaurus-turnstile"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Solve Cloudflare Turnstile in Botasaurus via the Peak API — read the sitekey, get a token, inject it. For when the anti-detect driver still stalls on the widget."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{ name = "Peak" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"botasaurus",
|
|
15
|
+
"cloudflare",
|
|
16
|
+
"turnstile",
|
|
17
|
+
"captcha",
|
|
18
|
+
"solver",
|
|
19
|
+
"bypass",
|
|
20
|
+
"cf-clearance",
|
|
21
|
+
"scraping",
|
|
22
|
+
"anti-detect",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
30
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
31
|
+
"Topic :: Software Development :: Testing",
|
|
32
|
+
]
|
|
33
|
+
dependencies = []
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
botasaurus = ["botasaurus-driver>=4.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://peak.fo/?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile"
|
|
40
|
+
Documentation = "https://peak.fo/docs/turnstile?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile"
|
|
41
|
+
Pricing = "https://peak.fo/pricing?utm_source=github&utm_medium=readme&utm_campaign=packages&utm_content=botasaurus-turnstile"
|
|
42
|
+
Source = "https://github.com/CircuitSavage/botasaurus-turnstile"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["botasaurus_turnstile"]
|