flarebreak 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.
- flarebreak-0.1.0/PKG-INFO +215 -0
- flarebreak-0.1.0/README.md +192 -0
- flarebreak-0.1.0/flarebreak/__init__.py +35 -0
- flarebreak-0.1.0/flarebreak/async_client.py +225 -0
- flarebreak-0.1.0/flarebreak/client.py +249 -0
- flarebreak-0.1.0/flarebreak/models.py +116 -0
- flarebreak-0.1.0/flarebreak.egg-info/PKG-INFO +215 -0
- flarebreak-0.1.0/flarebreak.egg-info/SOURCES.txt +12 -0
- flarebreak-0.1.0/flarebreak.egg-info/dependency_links.txt +1 -0
- flarebreak-0.1.0/flarebreak.egg-info/requires.txt +10 -0
- flarebreak-0.1.0/flarebreak.egg-info/top_level.txt +1 -0
- flarebreak-0.1.0/pyproject.toml +33 -0
- flarebreak-0.1.0/setup.cfg +4 -0
- flarebreak-0.1.0/setup.py +26 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flarebreak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python wrapper for cloudflare-solver — bypass Turnstile & IUAM with ease
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/B00H0O/cloudflare-solver
|
|
7
|
+
Keywords: cloudflare,turnstile,captcha,iuam,cf-clearance,scraping,bypass
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests>=2.28
|
|
15
|
+
Provides-Extra: async
|
|
16
|
+
Requires-Dist: aiohttp>=3.9; extra == "async"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
20
|
+
Requires-Dist: aiohttp>=3.9; extra == "dev"
|
|
21
|
+
Requires-Dist: responses; extra == "dev"
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
|
|
24
|
+
# flarebreak
|
|
25
|
+
|
|
26
|
+
Python wrapper for the [cloudflare-solver](https://github.com/B00H0O/cloudflare-solver) Rust service.
|
|
27
|
+
Supports **sync** (`requests`) and **async** (`aiohttp`) usage out of the box.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
The Rust service must be running before you use this library:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
docker build -t turnstile-solver .
|
|
37
|
+
docker run -d -p 407:407 turnstile-solver
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Sync only
|
|
46
|
+
pip install flarebreak
|
|
47
|
+
|
|
48
|
+
# Sync + Async
|
|
49
|
+
pip install "flarebreak[async]"
|
|
50
|
+
|
|
51
|
+
# From source
|
|
52
|
+
pip install -e ".[async]"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Turnstile (sync)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from flarebreak import CloudflareSolver
|
|
63
|
+
|
|
64
|
+
with CloudflareSolver() as solver:
|
|
65
|
+
result = solver.turnstile(
|
|
66
|
+
url="https://bypass.city",
|
|
67
|
+
sitekey="0x4AAAAAAAGzw6rXeQWJ_y2P",
|
|
68
|
+
)
|
|
69
|
+
print(result.token) # 1.kMLfH4VM8kCMPXvMy-QcrmU…
|
|
70
|
+
print(result.elapsed) # 2.94s
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### IUAM / cf_clearance (sync)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import requests
|
|
77
|
+
from flarebreak import CloudflareSolver
|
|
78
|
+
|
|
79
|
+
with CloudflareSolver() as solver:
|
|
80
|
+
result = solver.iuam(url="https://nowsecure.nl")
|
|
81
|
+
|
|
82
|
+
print(result.cf_clearance) # 155jEz2BCC8oFRCOu0x8…
|
|
83
|
+
print(result.user_agent)
|
|
84
|
+
print(result.ip) # egress IP used during solve
|
|
85
|
+
|
|
86
|
+
# Replay the session — same IP + UA is mandatory
|
|
87
|
+
session = requests.Session()
|
|
88
|
+
session.headers.update(result.headers) # sets Cookie + User-Agent
|
|
89
|
+
resp = session.get("https://nowsecure.nl")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Async / parallel
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import asyncio
|
|
96
|
+
from flarebreak import AsyncCloudflareSolver
|
|
97
|
+
|
|
98
|
+
async def main():
|
|
99
|
+
async with AsyncCloudflareSolver() as solver:
|
|
100
|
+
results = await asyncio.gather(
|
|
101
|
+
solver.turnstile(url="https://a.com", sitekey="0x..."),
|
|
102
|
+
solver.turnstile(url="https://b.com", sitekey="0x..."),
|
|
103
|
+
solver.iuam(url="https://c.com"),
|
|
104
|
+
)
|
|
105
|
+
for r in results:
|
|
106
|
+
print(r)
|
|
107
|
+
|
|
108
|
+
asyncio.run(main())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
### `CloudflareSolver(host, port, timeout, session)`
|
|
116
|
+
|
|
117
|
+
| Param | Default | Description |
|
|
118
|
+
|-------|---------|-------------|
|
|
119
|
+
| `host` | `"localhost"` | Solver service host |
|
|
120
|
+
| `port` | `407` | Solver service port |
|
|
121
|
+
| `timeout` | `60` | Seconds before giving up |
|
|
122
|
+
| `session` | `None` | Custom `requests.Session` |
|
|
123
|
+
|
|
124
|
+
#### `.health() → HealthResult`
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
h = solver.health()
|
|
128
|
+
print(h.available, h.capacity) # 18 / 20
|
|
129
|
+
print(h.healthy) # True
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `.turnstile(url, sitekey, *, cdata, action, proxy) → TurnstileResult`
|
|
133
|
+
|
|
134
|
+
| Param | Required | Description |
|
|
135
|
+
|-------|----------|-------------|
|
|
136
|
+
| `url` | ✅ | Page hosting the widget |
|
|
137
|
+
| `sitekey` | ✅ | `"0x4AAA…"` or `["0x…", "0x…"]` for multiple widgets |
|
|
138
|
+
| `cdata` | ❌ | Turnstile cData field |
|
|
139
|
+
| `action` | ❌ | Turnstile action field |
|
|
140
|
+
| `proxy` | ❌ | `"http://user:pass@host:port"` / `"socks5://…"` |
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
result.token # str or list[str]
|
|
144
|
+
result.elapsed # "2.94s"
|
|
145
|
+
result.success # True / False
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `.iuam(url, *, proxy) → IUAMResult`
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
result.cf_clearance # extracted cookie value
|
|
152
|
+
result.user_agent # must be replayed verbatim
|
|
153
|
+
result.ip # egress IP (important for proxy users)
|
|
154
|
+
result.headers # {"Cookie": "…", "User-Agent": "…"}
|
|
155
|
+
result.elapsed # "2.87s"
|
|
156
|
+
result.success # True / False
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `AsyncCloudflareSolver`
|
|
160
|
+
|
|
161
|
+
Same API as `CloudflareSolver`, but every method is `async`. Use as an async context manager.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Exceptions
|
|
166
|
+
|
|
167
|
+
| Exception | When |
|
|
168
|
+
|-----------|------|
|
|
169
|
+
| `SolverError` | Base class for all errors |
|
|
170
|
+
| `TimeoutError` | Solver gave up on the challenge |
|
|
171
|
+
| `SolverUnavailableError` | Service is unreachable |
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from flarebreak import SolverError, TimeoutError, SolverUnavailableError
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
result = solver.turnstile(url=..., sitekey=...)
|
|
178
|
+
except TimeoutError:
|
|
179
|
+
# retry or scale up
|
|
180
|
+
pass
|
|
181
|
+
except SolverUnavailableError:
|
|
182
|
+
# docker container not running
|
|
183
|
+
pass
|
|
184
|
+
except SolverError as e:
|
|
185
|
+
print(e)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Custom host / scaling
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# Point to a different host or port
|
|
194
|
+
solver = CloudflareSolver(host="192.168.1.10", port=408)
|
|
195
|
+
|
|
196
|
+
# Or load-balance across scaled containers manually
|
|
197
|
+
import itertools
|
|
198
|
+
ports = itertools.cycle([407, 408, 409, 410])
|
|
199
|
+
solver = CloudflareSolver(port=next(ports))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Running tests
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
pytest tests/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# flarebreak
|
|
2
|
+
|
|
3
|
+
Python wrapper for the [cloudflare-solver](https://github.com/B00H0O/cloudflare-solver) Rust service.
|
|
4
|
+
Supports **sync** (`requests`) and **async** (`aiohttp`) usage out of the box.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
The Rust service must be running before you use this library:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
docker build -t turnstile-solver .
|
|
14
|
+
docker run -d -p 407:407 turnstile-solver
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Sync only
|
|
23
|
+
pip install flarebreak
|
|
24
|
+
|
|
25
|
+
# Sync + Async
|
|
26
|
+
pip install "flarebreak[async]"
|
|
27
|
+
|
|
28
|
+
# From source
|
|
29
|
+
pip install -e ".[async]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Turnstile (sync)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from flarebreak import CloudflareSolver
|
|
40
|
+
|
|
41
|
+
with CloudflareSolver() as solver:
|
|
42
|
+
result = solver.turnstile(
|
|
43
|
+
url="https://bypass.city",
|
|
44
|
+
sitekey="0x4AAAAAAAGzw6rXeQWJ_y2P",
|
|
45
|
+
)
|
|
46
|
+
print(result.token) # 1.kMLfH4VM8kCMPXvMy-QcrmU…
|
|
47
|
+
print(result.elapsed) # 2.94s
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### IUAM / cf_clearance (sync)
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import requests
|
|
54
|
+
from flarebreak import CloudflareSolver
|
|
55
|
+
|
|
56
|
+
with CloudflareSolver() as solver:
|
|
57
|
+
result = solver.iuam(url="https://nowsecure.nl")
|
|
58
|
+
|
|
59
|
+
print(result.cf_clearance) # 155jEz2BCC8oFRCOu0x8…
|
|
60
|
+
print(result.user_agent)
|
|
61
|
+
print(result.ip) # egress IP used during solve
|
|
62
|
+
|
|
63
|
+
# Replay the session — same IP + UA is mandatory
|
|
64
|
+
session = requests.Session()
|
|
65
|
+
session.headers.update(result.headers) # sets Cookie + User-Agent
|
|
66
|
+
resp = session.get("https://nowsecure.nl")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Async / parallel
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import asyncio
|
|
73
|
+
from flarebreak import AsyncCloudflareSolver
|
|
74
|
+
|
|
75
|
+
async def main():
|
|
76
|
+
async with AsyncCloudflareSolver() as solver:
|
|
77
|
+
results = await asyncio.gather(
|
|
78
|
+
solver.turnstile(url="https://a.com", sitekey="0x..."),
|
|
79
|
+
solver.turnstile(url="https://b.com", sitekey="0x..."),
|
|
80
|
+
solver.iuam(url="https://c.com"),
|
|
81
|
+
)
|
|
82
|
+
for r in results:
|
|
83
|
+
print(r)
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## API Reference
|
|
91
|
+
|
|
92
|
+
### `CloudflareSolver(host, port, timeout, session)`
|
|
93
|
+
|
|
94
|
+
| Param | Default | Description |
|
|
95
|
+
|-------|---------|-------------|
|
|
96
|
+
| `host` | `"localhost"` | Solver service host |
|
|
97
|
+
| `port` | `407` | Solver service port |
|
|
98
|
+
| `timeout` | `60` | Seconds before giving up |
|
|
99
|
+
| `session` | `None` | Custom `requests.Session` |
|
|
100
|
+
|
|
101
|
+
#### `.health() → HealthResult`
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
h = solver.health()
|
|
105
|
+
print(h.available, h.capacity) # 18 / 20
|
|
106
|
+
print(h.healthy) # True
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### `.turnstile(url, sitekey, *, cdata, action, proxy) → TurnstileResult`
|
|
110
|
+
|
|
111
|
+
| Param | Required | Description |
|
|
112
|
+
|-------|----------|-------------|
|
|
113
|
+
| `url` | ✅ | Page hosting the widget |
|
|
114
|
+
| `sitekey` | ✅ | `"0x4AAA…"` or `["0x…", "0x…"]` for multiple widgets |
|
|
115
|
+
| `cdata` | ❌ | Turnstile cData field |
|
|
116
|
+
| `action` | ❌ | Turnstile action field |
|
|
117
|
+
| `proxy` | ❌ | `"http://user:pass@host:port"` / `"socks5://…"` |
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
result.token # str or list[str]
|
|
121
|
+
result.elapsed # "2.94s"
|
|
122
|
+
result.success # True / False
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `.iuam(url, *, proxy) → IUAMResult`
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
result.cf_clearance # extracted cookie value
|
|
129
|
+
result.user_agent # must be replayed verbatim
|
|
130
|
+
result.ip # egress IP (important for proxy users)
|
|
131
|
+
result.headers # {"Cookie": "…", "User-Agent": "…"}
|
|
132
|
+
result.elapsed # "2.87s"
|
|
133
|
+
result.success # True / False
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `AsyncCloudflareSolver`
|
|
137
|
+
|
|
138
|
+
Same API as `CloudflareSolver`, but every method is `async`. Use as an async context manager.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Exceptions
|
|
143
|
+
|
|
144
|
+
| Exception | When |
|
|
145
|
+
|-----------|------|
|
|
146
|
+
| `SolverError` | Base class for all errors |
|
|
147
|
+
| `TimeoutError` | Solver gave up on the challenge |
|
|
148
|
+
| `SolverUnavailableError` | Service is unreachable |
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from flarebreak import SolverError, TimeoutError, SolverUnavailableError
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
result = solver.turnstile(url=..., sitekey=...)
|
|
155
|
+
except TimeoutError:
|
|
156
|
+
# retry or scale up
|
|
157
|
+
pass
|
|
158
|
+
except SolverUnavailableError:
|
|
159
|
+
# docker container not running
|
|
160
|
+
pass
|
|
161
|
+
except SolverError as e:
|
|
162
|
+
print(e)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Custom host / scaling
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# Point to a different host or port
|
|
171
|
+
solver = CloudflareSolver(host="192.168.1.10", port=408)
|
|
172
|
+
|
|
173
|
+
# Or load-balance across scaled containers manually
|
|
174
|
+
import itertools
|
|
175
|
+
ports = itertools.cycle([407, 408, 409, 410])
|
|
176
|
+
solver = CloudflareSolver(port=next(ports))
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Running tests
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install -e ".[dev]"
|
|
185
|
+
pytest tests/
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flarebreak-py
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Python wrapper for the flarebreak Rust service.
|
|
5
|
+
Supports both sync (requests) and async (aiohttp) usage.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import CloudflareSolver
|
|
9
|
+
from .models import (
|
|
10
|
+
TurnstileResult,
|
|
11
|
+
IUAMResult,
|
|
12
|
+
HealthResult,
|
|
13
|
+
SolverError,
|
|
14
|
+
TimeoutError,
|
|
15
|
+
SolverUnavailableError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def __getattr__(name):
|
|
19
|
+
if name == "AsyncCloudflareSolver":
|
|
20
|
+
from .async_client import AsyncCloudflareSolver # noqa: PLC0415
|
|
21
|
+
return AsyncCloudflareSolver
|
|
22
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CloudflareSolver",
|
|
28
|
+
"AsyncCloudflareSolver",
|
|
29
|
+
"TurnstileResult",
|
|
30
|
+
"IUAMResult",
|
|
31
|
+
"HealthResult",
|
|
32
|
+
"SolverError",
|
|
33
|
+
"TimeoutError",
|
|
34
|
+
"SolverUnavailableError",
|
|
35
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asynchronous client for the flarebreak service.
|
|
3
|
+
Requires: aiohttp
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Union, List
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import aiohttp
|
|
11
|
+
except ImportError as exc: # pragma: no cover
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"aiohttp is required for AsyncCloudflareSolver. "
|
|
14
|
+
"Install it with: pip install flarebreak-py[async]"
|
|
15
|
+
) from exc
|
|
16
|
+
|
|
17
|
+
from .models import (
|
|
18
|
+
TurnstileResult,
|
|
19
|
+
IUAMResult,
|
|
20
|
+
HealthResult,
|
|
21
|
+
SolverError,
|
|
22
|
+
TimeoutError,
|
|
23
|
+
SolverUnavailableError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AsyncCloudflareSolver:
|
|
28
|
+
"""
|
|
29
|
+
Async wrapper around the flarebreak HTTP service (uses aiohttp).
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
host:
|
|
34
|
+
Hostname / IP of the running solver container. Default ``"localhost"``.
|
|
35
|
+
port:
|
|
36
|
+
Port the solver listens on. Default ``407``.
|
|
37
|
+
timeout:
|
|
38
|
+
Seconds to wait for a solve to complete. Default ``60``.
|
|
39
|
+
session:
|
|
40
|
+
Optional pre-configured ``aiohttp.ClientSession`` to reuse.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
::
|
|
45
|
+
|
|
46
|
+
import asyncio
|
|
47
|
+
from flarebreak import AsyncCloudflareSolver
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
async with AsyncCloudflareSolver() as solver:
|
|
51
|
+
result = await solver.turnstile(
|
|
52
|
+
url="https://example.com",
|
|
53
|
+
sitekey="0x4AAAAAAAGzw6rXeQWJ_y2P",
|
|
54
|
+
)
|
|
55
|
+
print(result.token)
|
|
56
|
+
|
|
57
|
+
asyncio.run(main())
|
|
58
|
+
|
|
59
|
+
Parallel solves::
|
|
60
|
+
|
|
61
|
+
async def main():
|
|
62
|
+
async with AsyncCloudflareSolver() as solver:
|
|
63
|
+
results = await asyncio.gather(
|
|
64
|
+
solver.turnstile(url="https://a.com", sitekey="0x..."),
|
|
65
|
+
solver.turnstile(url="https://b.com", sitekey="0x..."),
|
|
66
|
+
)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
host: str = "localhost",
|
|
72
|
+
port: int = 407,
|
|
73
|
+
timeout: float = 60,
|
|
74
|
+
session: Optional[aiohttp.ClientSession] = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
self._base = f"http://{host}:{port}"
|
|
77
|
+
self._timeout = aiohttp.ClientTimeout(total=timeout)
|
|
78
|
+
self._session = session
|
|
79
|
+
self._owns_session = session is None
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# Public API
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
async def health(self) -> HealthResult:
|
|
86
|
+
"""Check solver service health and available capacity."""
|
|
87
|
+
data = await self._get("/health")
|
|
88
|
+
return HealthResult(
|
|
89
|
+
status=data.get("status", "unknown"),
|
|
90
|
+
dyno=data.get("dyno", ""),
|
|
91
|
+
capacity=data.get("capacity", 0),
|
|
92
|
+
available=data.get("available", 0),
|
|
93
|
+
active=data.get("active", 0),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def turnstile(
|
|
97
|
+
self,
|
|
98
|
+
url: str,
|
|
99
|
+
sitekey: Union[str, List[str]],
|
|
100
|
+
*,
|
|
101
|
+
cdata: Optional[str] = None,
|
|
102
|
+
action: Optional[str] = None,
|
|
103
|
+
proxy: Optional[str] = None,
|
|
104
|
+
) -> TurnstileResult:
|
|
105
|
+
"""
|
|
106
|
+
Solve a Cloudflare Turnstile challenge (async).
|
|
107
|
+
|
|
108
|
+
Parameters mirror :meth:`CloudflareSolver.turnstile`.
|
|
109
|
+
"""
|
|
110
|
+
payload: dict = {"url": url, "sitekey": sitekey}
|
|
111
|
+
if cdata is not None:
|
|
112
|
+
payload["cdata"] = cdata
|
|
113
|
+
if action is not None:
|
|
114
|
+
payload["action"] = action
|
|
115
|
+
if proxy is not None:
|
|
116
|
+
payload["proxy"] = proxy
|
|
117
|
+
|
|
118
|
+
data = await self._post("/turnstile", payload)
|
|
119
|
+
return TurnstileResult(
|
|
120
|
+
token=data["token"],
|
|
121
|
+
elapsed=data.get("elapsed", ""),
|
|
122
|
+
status=data.get("status", ""),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def iuam(
|
|
126
|
+
self,
|
|
127
|
+
url: str,
|
|
128
|
+
*,
|
|
129
|
+
proxy: Optional[str] = None,
|
|
130
|
+
) -> IUAMResult:
|
|
131
|
+
"""
|
|
132
|
+
Pass a Cloudflare IUAM challenge (async).
|
|
133
|
+
|
|
134
|
+
Parameters mirror :meth:`CloudflareSolver.iuam`.
|
|
135
|
+
"""
|
|
136
|
+
payload: dict = {"url": url}
|
|
137
|
+
if proxy is not None:
|
|
138
|
+
payload["proxy"] = proxy
|
|
139
|
+
|
|
140
|
+
data = await self._post("/iuam", payload)
|
|
141
|
+
return IUAMResult(
|
|
142
|
+
headers=data.get("headers", {}),
|
|
143
|
+
ip=data.get("ip", ""),
|
|
144
|
+
elapsed=data.get("elapsed", ""),
|
|
145
|
+
status=data.get("status", ""),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# Context manager support
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
async def __aenter__(self) -> "AsyncCloudflareSolver":
|
|
153
|
+
if self._owns_session:
|
|
154
|
+
self._session = aiohttp.ClientSession(
|
|
155
|
+
headers={"Content-Type": "application/json"},
|
|
156
|
+
timeout=self._timeout,
|
|
157
|
+
)
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
async def __aexit__(self, *_) -> None:
|
|
161
|
+
await self.close()
|
|
162
|
+
|
|
163
|
+
async def close(self) -> None:
|
|
164
|
+
"""Close the underlying aiohttp session (if owned by this client)."""
|
|
165
|
+
if self._owns_session and self._session and not self._session.closed:
|
|
166
|
+
await self._session.close()
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
# Internal helpers
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def _ensure_session(self) -> aiohttp.ClientSession:
|
|
173
|
+
if self._session is None or self._session.closed:
|
|
174
|
+
self._session = aiohttp.ClientSession(
|
|
175
|
+
headers={"Content-Type": "application/json"},
|
|
176
|
+
timeout=self._timeout,
|
|
177
|
+
)
|
|
178
|
+
self._owns_session = True
|
|
179
|
+
return self._session
|
|
180
|
+
|
|
181
|
+
async def _get(self, path: str) -> dict:
|
|
182
|
+
session = self._ensure_session()
|
|
183
|
+
try:
|
|
184
|
+
async with session.get(f"{self._base}{path}") as resp:
|
|
185
|
+
resp.raise_for_status()
|
|
186
|
+
return await resp.json()
|
|
187
|
+
except aiohttp.ClientConnectorError as exc:
|
|
188
|
+
raise SolverUnavailableError(
|
|
189
|
+
f"Cannot connect to solver at {self._base}. "
|
|
190
|
+
"Is the Docker container running?"
|
|
191
|
+
) from exc
|
|
192
|
+
except aiohttp.ServerTimeoutError as exc:
|
|
193
|
+
raise TimeoutError("Health check timed out.") from exc
|
|
194
|
+
|
|
195
|
+
async def _post(self, path: str, payload: dict) -> dict:
|
|
196
|
+
session = self._ensure_session()
|
|
197
|
+
try:
|
|
198
|
+
async with session.post(f"{self._base}{path}", json=payload) as resp:
|
|
199
|
+
try:
|
|
200
|
+
data = await resp.json(content_type=None)
|
|
201
|
+
except Exception:
|
|
202
|
+
text = await resp.text()
|
|
203
|
+
raise SolverError(f"Non-JSON response (HTTP {resp.status}): {text[:200]}")
|
|
204
|
+
|
|
205
|
+
status = data.get("status", "")
|
|
206
|
+
if status == "timeout" or resp.status == 408:
|
|
207
|
+
raise TimeoutError(
|
|
208
|
+
f"Solver timed out on challenge. elapsed={data.get('elapsed')}"
|
|
209
|
+
)
|
|
210
|
+
if resp.status >= 400 or status == "error":
|
|
211
|
+
raise SolverError(
|
|
212
|
+
f"Solver returned error (HTTP {resp.status}): "
|
|
213
|
+
f"{data.get('error') or data}"
|
|
214
|
+
)
|
|
215
|
+
return data
|
|
216
|
+
|
|
217
|
+
except aiohttp.ClientConnectorError as exc:
|
|
218
|
+
raise SolverUnavailableError(
|
|
219
|
+
f"Cannot connect to solver at {self._base}. "
|
|
220
|
+
"Is the Docker container running?"
|
|
221
|
+
) from exc
|
|
222
|
+
except aiohttp.ServerTimeoutError as exc:
|
|
223
|
+
raise TimeoutError(
|
|
224
|
+
f"No response from solver within the configured timeout."
|
|
225
|
+
) from exc
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synchronous client for the flarebreak service.
|
|
3
|
+
Requires: requests
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Union, List
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .models import (
|
|
11
|
+
TurnstileResult,
|
|
12
|
+
IUAMResult,
|
|
13
|
+
HealthResult,
|
|
14
|
+
SolverError,
|
|
15
|
+
TimeoutError,
|
|
16
|
+
SolverUnavailableError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CloudflareSolver:
|
|
21
|
+
"""
|
|
22
|
+
Synchronous wrapper around the flarebreak HTTP service.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
host:
|
|
27
|
+
Hostname / IP of the running solver container. Default ``"localhost"``.
|
|
28
|
+
port:
|
|
29
|
+
Port the solver listens on. Default ``407``.
|
|
30
|
+
timeout:
|
|
31
|
+
Seconds to wait for a solve to complete. Default ``60``.
|
|
32
|
+
Set higher if you see timeouts under heavy load.
|
|
33
|
+
session:
|
|
34
|
+
Optional pre-configured ``requests.Session`` to reuse (useful for
|
|
35
|
+
proxies, custom TLS, retries, etc.).
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
Basic Turnstile solve::
|
|
40
|
+
|
|
41
|
+
from flarebreak import CloudflareSolver
|
|
42
|
+
|
|
43
|
+
solver = CloudflareSolver()
|
|
44
|
+
result = solver.turnstile(
|
|
45
|
+
url="https://example.com",
|
|
46
|
+
sitekey="0x4AAAAAAAGzw6rXeQWJ_y2P",
|
|
47
|
+
)
|
|
48
|
+
print(result.token)
|
|
49
|
+
|
|
50
|
+
IUAM (cf_clearance) solve::
|
|
51
|
+
|
|
52
|
+
result = solver.iuam(url="https://nowsecure.nl")
|
|
53
|
+
print(result.cf_clearance)
|
|
54
|
+
print(result.user_agent)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
host: str = "localhost",
|
|
60
|
+
port: int = 407,
|
|
61
|
+
timeout: float = 60,
|
|
62
|
+
session: Optional[requests.Session] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self._base = f"http://{host}:{port}"
|
|
65
|
+
self._timeout = timeout
|
|
66
|
+
self._session = session or requests.Session()
|
|
67
|
+
self._session.headers.update({"Content-Type": "application/json"})
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
# Public API
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def health(self) -> HealthResult:
|
|
74
|
+
"""
|
|
75
|
+
Check solver service health and available capacity.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
HealthResult
|
|
80
|
+
"""
|
|
81
|
+
data = self._get("/health")
|
|
82
|
+
return HealthResult(
|
|
83
|
+
status=data.get("status", "unknown"),
|
|
84
|
+
dyno=data.get("dyno", ""),
|
|
85
|
+
capacity=data.get("capacity", 0),
|
|
86
|
+
available=data.get("available", 0),
|
|
87
|
+
active=data.get("active", 0),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def turnstile(
|
|
91
|
+
self,
|
|
92
|
+
url: str,
|
|
93
|
+
sitekey: Union[str, List[str]],
|
|
94
|
+
*,
|
|
95
|
+
cdata: Optional[str] = None,
|
|
96
|
+
action: Optional[str] = None,
|
|
97
|
+
proxy: Optional[str] = None,
|
|
98
|
+
) -> TurnstileResult:
|
|
99
|
+
"""
|
|
100
|
+
Solve a Cloudflare Turnstile challenge.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
url:
|
|
105
|
+
The page URL that hosts the Turnstile widget.
|
|
106
|
+
sitekey:
|
|
107
|
+
Turnstile sitekey (``0x4AAA…``). Pass a list to solve
|
|
108
|
+
multiple widgets in one request.
|
|
109
|
+
cdata:
|
|
110
|
+
Optional Turnstile ``cData`` field.
|
|
111
|
+
action:
|
|
112
|
+
Optional Turnstile ``action`` field.
|
|
113
|
+
proxy:
|
|
114
|
+
Optional proxy, e.g. ``"http://user:pass@host:port"`` or
|
|
115
|
+
``"socks5://host:port"``.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
TurnstileResult
|
|
120
|
+
|
|
121
|
+
Raises
|
|
122
|
+
------
|
|
123
|
+
TimeoutError
|
|
124
|
+
If the solver service gives up on the challenge.
|
|
125
|
+
SolverError
|
|
126
|
+
For any other solver-side error.
|
|
127
|
+
SolverUnavailableError
|
|
128
|
+
If the service cannot be reached.
|
|
129
|
+
"""
|
|
130
|
+
payload: dict = {"url": url, "sitekey": sitekey}
|
|
131
|
+
if cdata is not None:
|
|
132
|
+
payload["cdata"] = cdata
|
|
133
|
+
if action is not None:
|
|
134
|
+
payload["action"] = action
|
|
135
|
+
if proxy is not None:
|
|
136
|
+
payload["proxy"] = proxy
|
|
137
|
+
|
|
138
|
+
data = self._post("/turnstile", payload)
|
|
139
|
+
return TurnstileResult(
|
|
140
|
+
token=data["token"],
|
|
141
|
+
elapsed=data.get("elapsed", ""),
|
|
142
|
+
status=data.get("status", ""),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def iuam(
|
|
146
|
+
self,
|
|
147
|
+
url: str,
|
|
148
|
+
*,
|
|
149
|
+
proxy: Optional[str] = None,
|
|
150
|
+
) -> IUAMResult:
|
|
151
|
+
"""
|
|
152
|
+
Pass a Cloudflare IUAM ("I'm Under Attack Mode") challenge and
|
|
153
|
+
return a ready-to-use ``cf_clearance`` cookie + User-Agent pair.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
url:
|
|
158
|
+
The protected URL.
|
|
159
|
+
proxy:
|
|
160
|
+
Optional proxy (same format as :meth:`turnstile`).
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
IUAMResult
|
|
165
|
+
|
|
166
|
+
Raises
|
|
167
|
+
------
|
|
168
|
+
TimeoutError
|
|
169
|
+
SolverError
|
|
170
|
+
SolverUnavailableError
|
|
171
|
+
"""
|
|
172
|
+
payload: dict = {"url": url}
|
|
173
|
+
if proxy is not None:
|
|
174
|
+
payload["proxy"] = proxy
|
|
175
|
+
|
|
176
|
+
data = self._post("/iuam", payload)
|
|
177
|
+
return IUAMResult(
|
|
178
|
+
headers=data.get("headers", {}),
|
|
179
|
+
ip=data.get("ip", ""),
|
|
180
|
+
elapsed=data.get("elapsed", ""),
|
|
181
|
+
status=data.get("status", ""),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# Context manager support
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> "CloudflareSolver":
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def __exit__(self, *_) -> None:
|
|
192
|
+
self.close()
|
|
193
|
+
|
|
194
|
+
def close(self) -> None:
|
|
195
|
+
"""Close the underlying requests session."""
|
|
196
|
+
self._session.close()
|
|
197
|
+
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
# Internal helpers
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def _get(self, path: str) -> dict:
|
|
203
|
+
try:
|
|
204
|
+
resp = self._session.get(f"{self._base}{path}", timeout=self._timeout)
|
|
205
|
+
resp.raise_for_status()
|
|
206
|
+
return resp.json()
|
|
207
|
+
except requests.exceptions.ConnectionError as exc:
|
|
208
|
+
raise SolverUnavailableError(
|
|
209
|
+
f"Cannot connect to solver at {self._base}. "
|
|
210
|
+
"Is the Docker container running?"
|
|
211
|
+
) from exc
|
|
212
|
+
except requests.exceptions.Timeout as exc:
|
|
213
|
+
raise TimeoutError("Health check timed out.") from exc
|
|
214
|
+
|
|
215
|
+
def _post(self, path: str, payload: dict) -> dict:
|
|
216
|
+
try:
|
|
217
|
+
resp = self._session.post(
|
|
218
|
+
f"{self._base}{path}",
|
|
219
|
+
json=payload,
|
|
220
|
+
timeout=self._timeout,
|
|
221
|
+
)
|
|
222
|
+
except requests.exceptions.ConnectionError as exc:
|
|
223
|
+
raise SolverUnavailableError(
|
|
224
|
+
f"Cannot connect to solver at {self._base}. "
|
|
225
|
+
"Is the Docker container running?"
|
|
226
|
+
) from exc
|
|
227
|
+
except requests.exceptions.Timeout as exc:
|
|
228
|
+
raise TimeoutError(
|
|
229
|
+
f"No response from solver within {self._timeout}s."
|
|
230
|
+
) from exc
|
|
231
|
+
|
|
232
|
+
# Parse body regardless of status code (solver sends errors as JSON too)
|
|
233
|
+
try:
|
|
234
|
+
data = resp.json()
|
|
235
|
+
except Exception:
|
|
236
|
+
resp.raise_for_status()
|
|
237
|
+
raise SolverError(f"Unexpected non-JSON response: {resp.text[:200]}")
|
|
238
|
+
|
|
239
|
+
status = data.get("status", "")
|
|
240
|
+
if status == "timeout" or resp.status_code == 408:
|
|
241
|
+
raise TimeoutError(
|
|
242
|
+
f"Solver timed out on challenge. elapsed={data.get('elapsed')}"
|
|
243
|
+
)
|
|
244
|
+
if resp.status_code >= 400 or status == "error":
|
|
245
|
+
raise SolverError(
|
|
246
|
+
f"Solver returned error (HTTP {resp.status_code}): "
|
|
247
|
+
f"{data.get('error') or data}"
|
|
248
|
+
)
|
|
249
|
+
return data
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models and exceptions for flarebreak-py.
|
|
3
|
+
"""
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Optional, Dict, List, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Exceptions
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
class SolverError(Exception):
|
|
13
|
+
"""Base exception for all solver errors."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TimeoutError(SolverError):
|
|
17
|
+
"""Raised when the solver service times out on a challenge."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SolverUnavailableError(SolverError):
|
|
21
|
+
"""Raised when the solver service is unreachable or has no capacity."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Result dataclasses
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TurnstileResult:
|
|
30
|
+
"""Result of a Turnstile solve."""
|
|
31
|
+
|
|
32
|
+
token: Union[str, List[str]]
|
|
33
|
+
"""
|
|
34
|
+
The cf-turnstile-response token(s).
|
|
35
|
+
A list when multiple sitekeys were submitted.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
elapsed: str
|
|
39
|
+
"""Human-readable elapsed time, e.g. '2.94s'."""
|
|
40
|
+
|
|
41
|
+
status: str
|
|
42
|
+
"""'completed' on success."""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def success(self) -> bool:
|
|
46
|
+
return self.status == "completed"
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str: # noqa: D105
|
|
49
|
+
tok = self.token if isinstance(self.token, str) else f"[{len(self.token)} tokens]"
|
|
50
|
+
return f"<TurnstileResult status={self.status!r} elapsed={self.elapsed!r} token={tok[:30]!r}…>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class IUAMResult:
|
|
55
|
+
"""Result of an IUAM (cf_clearance) solve."""
|
|
56
|
+
|
|
57
|
+
headers: Dict[str, str]
|
|
58
|
+
"""
|
|
59
|
+
Ready-to-use headers dict containing at minimum:
|
|
60
|
+
- ``Cookie`` – includes ``cf_clearance=…``
|
|
61
|
+
- ``User-Agent`` – must be replayed verbatim
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
ip: str
|
|
65
|
+
"""The egress IP the solve was performed from (important for proxy users)."""
|
|
66
|
+
|
|
67
|
+
elapsed: str
|
|
68
|
+
"""Human-readable elapsed time, e.g. '2.87s'."""
|
|
69
|
+
|
|
70
|
+
status: str
|
|
71
|
+
"""'completed' on success."""
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def success(self) -> bool:
|
|
75
|
+
return self.status == "completed"
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def cf_clearance(self) -> Optional[str]:
|
|
79
|
+
"""Extract just the cf_clearance value from the Cookie header."""
|
|
80
|
+
cookie = self.headers.get("Cookie", "")
|
|
81
|
+
for part in cookie.split(";"):
|
|
82
|
+
part = part.strip()
|
|
83
|
+
if part.startswith("cf_clearance="):
|
|
84
|
+
return part[len("cf_clearance="):]
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def user_agent(self) -> Optional[str]:
|
|
89
|
+
return self.headers.get("User-Agent")
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str: # noqa: D105
|
|
92
|
+
return (
|
|
93
|
+
f"<IUAMResult status={self.status!r} elapsed={self.elapsed!r} "
|
|
94
|
+
f"ip={self.ip!r} cf_clearance={'…' if self.cf_clearance else None}>"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class HealthResult:
|
|
100
|
+
"""Result of GET /health."""
|
|
101
|
+
|
|
102
|
+
status: str
|
|
103
|
+
dyno: str
|
|
104
|
+
capacity: int
|
|
105
|
+
available: int
|
|
106
|
+
active: int
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def healthy(self) -> bool:
|
|
110
|
+
return self.status == "ok" and self.available > 0
|
|
111
|
+
|
|
112
|
+
def __repr__(self) -> str: # noqa: D105
|
|
113
|
+
return (
|
|
114
|
+
f"<HealthResult status={self.status!r} "
|
|
115
|
+
f"available={self.available}/{self.capacity}>"
|
|
116
|
+
)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flarebreak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python wrapper for cloudflare-solver — bypass Turnstile & IUAM with ease
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/B00H0O/cloudflare-solver
|
|
7
|
+
Keywords: cloudflare,turnstile,captcha,iuam,cf-clearance,scraping,bypass
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests>=2.28
|
|
15
|
+
Provides-Extra: async
|
|
16
|
+
Requires-Dist: aiohttp>=3.9; extra == "async"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
20
|
+
Requires-Dist: aiohttp>=3.9; extra == "dev"
|
|
21
|
+
Requires-Dist: responses; extra == "dev"
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
|
|
24
|
+
# flarebreak
|
|
25
|
+
|
|
26
|
+
Python wrapper for the [cloudflare-solver](https://github.com/B00H0O/cloudflare-solver) Rust service.
|
|
27
|
+
Supports **sync** (`requests`) and **async** (`aiohttp`) usage out of the box.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
The Rust service must be running before you use this library:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
docker build -t turnstile-solver .
|
|
37
|
+
docker run -d -p 407:407 turnstile-solver
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Sync only
|
|
46
|
+
pip install flarebreak
|
|
47
|
+
|
|
48
|
+
# Sync + Async
|
|
49
|
+
pip install "flarebreak[async]"
|
|
50
|
+
|
|
51
|
+
# From source
|
|
52
|
+
pip install -e ".[async]"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Turnstile (sync)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from flarebreak import CloudflareSolver
|
|
63
|
+
|
|
64
|
+
with CloudflareSolver() as solver:
|
|
65
|
+
result = solver.turnstile(
|
|
66
|
+
url="https://bypass.city",
|
|
67
|
+
sitekey="0x4AAAAAAAGzw6rXeQWJ_y2P",
|
|
68
|
+
)
|
|
69
|
+
print(result.token) # 1.kMLfH4VM8kCMPXvMy-QcrmU…
|
|
70
|
+
print(result.elapsed) # 2.94s
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### IUAM / cf_clearance (sync)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import requests
|
|
77
|
+
from flarebreak import CloudflareSolver
|
|
78
|
+
|
|
79
|
+
with CloudflareSolver() as solver:
|
|
80
|
+
result = solver.iuam(url="https://nowsecure.nl")
|
|
81
|
+
|
|
82
|
+
print(result.cf_clearance) # 155jEz2BCC8oFRCOu0x8…
|
|
83
|
+
print(result.user_agent)
|
|
84
|
+
print(result.ip) # egress IP used during solve
|
|
85
|
+
|
|
86
|
+
# Replay the session — same IP + UA is mandatory
|
|
87
|
+
session = requests.Session()
|
|
88
|
+
session.headers.update(result.headers) # sets Cookie + User-Agent
|
|
89
|
+
resp = session.get("https://nowsecure.nl")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Async / parallel
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import asyncio
|
|
96
|
+
from flarebreak import AsyncCloudflareSolver
|
|
97
|
+
|
|
98
|
+
async def main():
|
|
99
|
+
async with AsyncCloudflareSolver() as solver:
|
|
100
|
+
results = await asyncio.gather(
|
|
101
|
+
solver.turnstile(url="https://a.com", sitekey="0x..."),
|
|
102
|
+
solver.turnstile(url="https://b.com", sitekey="0x..."),
|
|
103
|
+
solver.iuam(url="https://c.com"),
|
|
104
|
+
)
|
|
105
|
+
for r in results:
|
|
106
|
+
print(r)
|
|
107
|
+
|
|
108
|
+
asyncio.run(main())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
### `CloudflareSolver(host, port, timeout, session)`
|
|
116
|
+
|
|
117
|
+
| Param | Default | Description |
|
|
118
|
+
|-------|---------|-------------|
|
|
119
|
+
| `host` | `"localhost"` | Solver service host |
|
|
120
|
+
| `port` | `407` | Solver service port |
|
|
121
|
+
| `timeout` | `60` | Seconds before giving up |
|
|
122
|
+
| `session` | `None` | Custom `requests.Session` |
|
|
123
|
+
|
|
124
|
+
#### `.health() → HealthResult`
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
h = solver.health()
|
|
128
|
+
print(h.available, h.capacity) # 18 / 20
|
|
129
|
+
print(h.healthy) # True
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `.turnstile(url, sitekey, *, cdata, action, proxy) → TurnstileResult`
|
|
133
|
+
|
|
134
|
+
| Param | Required | Description |
|
|
135
|
+
|-------|----------|-------------|
|
|
136
|
+
| `url` | ✅ | Page hosting the widget |
|
|
137
|
+
| `sitekey` | ✅ | `"0x4AAA…"` or `["0x…", "0x…"]` for multiple widgets |
|
|
138
|
+
| `cdata` | ❌ | Turnstile cData field |
|
|
139
|
+
| `action` | ❌ | Turnstile action field |
|
|
140
|
+
| `proxy` | ❌ | `"http://user:pass@host:port"` / `"socks5://…"` |
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
result.token # str or list[str]
|
|
144
|
+
result.elapsed # "2.94s"
|
|
145
|
+
result.success # True / False
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `.iuam(url, *, proxy) → IUAMResult`
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
result.cf_clearance # extracted cookie value
|
|
152
|
+
result.user_agent # must be replayed verbatim
|
|
153
|
+
result.ip # egress IP (important for proxy users)
|
|
154
|
+
result.headers # {"Cookie": "…", "User-Agent": "…"}
|
|
155
|
+
result.elapsed # "2.87s"
|
|
156
|
+
result.success # True / False
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `AsyncCloudflareSolver`
|
|
160
|
+
|
|
161
|
+
Same API as `CloudflareSolver`, but every method is `async`. Use as an async context manager.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Exceptions
|
|
166
|
+
|
|
167
|
+
| Exception | When |
|
|
168
|
+
|-----------|------|
|
|
169
|
+
| `SolverError` | Base class for all errors |
|
|
170
|
+
| `TimeoutError` | Solver gave up on the challenge |
|
|
171
|
+
| `SolverUnavailableError` | Service is unreachable |
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from flarebreak import SolverError, TimeoutError, SolverUnavailableError
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
result = solver.turnstile(url=..., sitekey=...)
|
|
178
|
+
except TimeoutError:
|
|
179
|
+
# retry or scale up
|
|
180
|
+
pass
|
|
181
|
+
except SolverUnavailableError:
|
|
182
|
+
# docker container not running
|
|
183
|
+
pass
|
|
184
|
+
except SolverError as e:
|
|
185
|
+
print(e)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Custom host / scaling
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# Point to a different host or port
|
|
194
|
+
solver = CloudflareSolver(host="192.168.1.10", port=408)
|
|
195
|
+
|
|
196
|
+
# Or load-balance across scaled containers manually
|
|
197
|
+
import itertools
|
|
198
|
+
ports = itertools.cycle([407, 408, 409, 410])
|
|
199
|
+
solver = CloudflareSolver(port=next(ports))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Running tests
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
pytest tests/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
flarebreak/__init__.py
|
|
5
|
+
flarebreak/async_client.py
|
|
6
|
+
flarebreak/client.py
|
|
7
|
+
flarebreak/models.py
|
|
8
|
+
flarebreak.egg-info/PKG-INFO
|
|
9
|
+
flarebreak.egg-info/SOURCES.txt
|
|
10
|
+
flarebreak.egg-info/dependency_links.txt
|
|
11
|
+
flarebreak.egg-info/requires.txt
|
|
12
|
+
flarebreak.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flarebreak
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.backends.legacy:build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flarebreak"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python wrapper for cloudflare-solver — bypass Turnstile & IUAM with ease"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
keywords = ["cloudflare", "turnstile", "captcha", "iuam", "cf-clearance", "scraping", "bypass"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests>=2.28",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
async = ["aiohttp>=3.9"]
|
|
26
|
+
dev = ["pytest>=7", "pytest-asyncio", "aiohttp>=3.9", "responses"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
"Homepage" = "https://github.com/B00H0O/cloudflare-solver"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["."]
|
|
33
|
+
include = ["flarebreak*"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="flarebreak",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Python wrapper for cloudflare-solver — bypass Turnstile & IUAM with ease",
|
|
7
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
license="MIT",
|
|
10
|
+
python_requires=">=3.8",
|
|
11
|
+
packages=find_packages(include=["flarebreak*"]),
|
|
12
|
+
install_requires=["requests>=2.28"],
|
|
13
|
+
extras_require={
|
|
14
|
+
"async": ["aiohttp>=3.9"],
|
|
15
|
+
},
|
|
16
|
+
keywords=["cloudflare", "turnstile", "captcha", "iuam", "cf-clearance", "scraping", "bypass"],
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
22
|
+
],
|
|
23
|
+
project_urls={
|
|
24
|
+
"Homepage": "https://github.com/B00H0O/cloudflare-solver",
|
|
25
|
+
},
|
|
26
|
+
)
|