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.
@@ -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,10 @@
1
+ requests>=2.28
2
+
3
+ [async]
4
+ aiohttp>=3.9
5
+
6
+ [dev]
7
+ pytest>=7
8
+ pytest-asyncio
9
+ aiohttp>=3.9
10
+ responses
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )