cloudsneak 1.0.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,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloudsneak
3
+ Version: 1.0.0
4
+ Summary: Cloudflare clearance cookie and Turnstile token solver service
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastapi>=0.100.0
8
+ Requires-Dist: uvicorn[standard]>=0.20.0
9
+ Requires-Dist: playwright>=1.35.0
10
+ Requires-Dist: pydantic>=2.0.0
11
+ Requires-Dist: py-ghost-cursor>=0.1.1
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: httpx; extra == "dev"
15
+
16
+ # cloudsneak
17
+
18
+ Cloudflare clearance cookie (`cf_clearance`) and Turnstile token solver service ported to Python.
19
+
20
+ ## Features
21
+
22
+ - **Turnstile Token Solver**: Render and solve Cloudflare Turnstile challenge widgets using synthetic injection and local script replacement.
23
+ - **IUAM clearance cookie Extraction**: Intercept responses to extract `cf_clearance` cookies and corresponding `User-Agent`.
24
+ - **Anti-Detect Browser Integration**: Custom Chrome launch configurations with Playwright.
25
+ - **FastAPI REST API**: Server with TTL caching, authentication token checks, and concurrency throttling.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install .
31
+ ```
32
+
33
+ Ensure Playwright browsers are installed:
34
+
35
+ ```bash
36
+ playwright install chromium
37
+ ```
38
+
39
+ ## Running the Server
40
+
41
+ Start the REST service:
42
+
43
+ ```bash
44
+ cloudsneak --host 0.0.0.0 --port 8742
45
+ ```
46
+
47
+ Or via python:
48
+
49
+ ```bash
50
+ python -m cloudsneak.cli
51
+ ```
52
+
53
+ ## API Usage
54
+
55
+ ### `POST /cloudflare`
56
+
57
+ #### Turnstile Mode
58
+
59
+ ```json
60
+ {
61
+ "mode": "turnstile",
62
+ "domain": "https://example.com",
63
+ "siteKey": "0x4AAAAAAACk534..."
64
+ }
65
+ ```
66
+
67
+ #### IUAM Mode
68
+
69
+ ```json
70
+ {
71
+ "mode": "iuam",
72
+ "domain": "https://example.com"
73
+ }
74
+ ```
@@ -0,0 +1,59 @@
1
+ # cloudsneak
2
+
3
+ Cloudflare clearance cookie (`cf_clearance`) and Turnstile token solver service ported to Python.
4
+
5
+ ## Features
6
+
7
+ - **Turnstile Token Solver**: Render and solve Cloudflare Turnstile challenge widgets using synthetic injection and local script replacement.
8
+ - **IUAM clearance cookie Extraction**: Intercept responses to extract `cf_clearance` cookies and corresponding `User-Agent`.
9
+ - **Anti-Detect Browser Integration**: Custom Chrome launch configurations with Playwright.
10
+ - **FastAPI REST API**: Server with TTL caching, authentication token checks, and concurrency throttling.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install .
16
+ ```
17
+
18
+ Ensure Playwright browsers are installed:
19
+
20
+ ```bash
21
+ playwright install chromium
22
+ ```
23
+
24
+ ## Running the Server
25
+
26
+ Start the REST service:
27
+
28
+ ```bash
29
+ cloudsneak --host 0.0.0.0 --port 8742
30
+ ```
31
+
32
+ Or via python:
33
+
34
+ ```bash
35
+ python -m cloudsneak.cli
36
+ ```
37
+
38
+ ## API Usage
39
+
40
+ ### `POST /cloudflare`
41
+
42
+ #### Turnstile Mode
43
+
44
+ ```json
45
+ {
46
+ "mode": "turnstile",
47
+ "domain": "https://example.com",
48
+ "siteKey": "0x4AAAAAAACk534..."
49
+ }
50
+ ```
51
+
52
+ #### IUAM Mode
53
+
54
+ ```json
55
+ {
56
+ "mode": "iuam",
57
+ "domain": "https://example.com"
58
+ }
59
+ ```
@@ -0,0 +1,16 @@
1
+ """
2
+ cloudsneak package initialization.
3
+ """
4
+
5
+ __version__ = "1.0.0"
6
+
7
+ from cloudsneak.browser import connect, check_turnstile
8
+ from cloudsneak.iuam import solve_iuam
9
+ from cloudsneak.turnstile import solve_turnstile
10
+
11
+ __all__ = [
12
+ "connect",
13
+ "check_turnstile",
14
+ "solve_iuam",
15
+ "solve_turnstile",
16
+ ]
@@ -0,0 +1,196 @@
1
+ """
2
+ Browser management module for cloudsneak.
3
+ Handles anti-detect Playwright browser context initialization, mouse movements,
4
+ and Turnstile iframe / widget click detection.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+ from playwright.async_api import async_playwright, Browser, BrowserContext, Page
11
+ from py_ghost_cursor import path
12
+
13
+ logger = logging.getLogger("cloudsneak.browser")
14
+
15
+ DEFAULT_CHROME_FLAGS = [
16
+ "--no-sandbox",
17
+ "--disable-dev-shm-usage",
18
+ "--disable-blink-features=AutomationControlled",
19
+ "--window-size=1920,1080",
20
+ ]
21
+
22
+
23
+ async def check_turnstile(page: Page) -> bool:
24
+ """
25
+ Checks for Turnstile challenge response inputs or bounding box coordinates
26
+ and simulates a click if found.
27
+ """
28
+ try:
29
+ # Check for explicit turnstile response elements
30
+ elements = await page.query_selector_all('[name="cf-turnstile-response"]')
31
+ if elements:
32
+ for element in elements:
33
+ try:
34
+ parent = await element.evaluate_handle("el => el.parentElement")
35
+ element_handle = parent.as_element()
36
+ if element_handle:
37
+ box = await element_handle.bounding_box()
38
+ if box:
39
+ click_x = box["x"] + 30
40
+ click_y = box["y"] + box["height"] / 2
41
+ await page.mouse.click(click_x, click_y)
42
+ except Exception as err:
43
+ logger.debug(f"Error clicking turnstile response parent: {err}")
44
+ return True
45
+
46
+ # Fallback: search for div elements with width between 290px and 310px
47
+ js_find_coords = """
48
+ () => {
49
+ let coordinates = [];
50
+ document.querySelectorAll('div').forEach(item => {
51
+ try {
52
+ let rect = item.getBoundingClientRect();
53
+ let css = window.getComputedStyle(item);
54
+ if (css.margin === "0px" && css.padding === "0px" && rect.width > 290 && rect.width <= 310 && !item.querySelector('*')) {
55
+ coordinates.push({ x: rect.x, y: rect.y, w: rect.width, h: rect.height });
56
+ }
57
+ } catch (e) {}
58
+ });
59
+
60
+ if (coordinates.length === 0) {
61
+ document.querySelectorAll('div').forEach(item => {
62
+ try {
63
+ let rect = item.getBoundingClientRect();
64
+ if (rect.width > 290 && rect.width <= 310 && !item.querySelector('*')) {
65
+ coordinates.push({ x: rect.x, y: rect.y, w: rect.width, h: rect.height });
66
+ }
67
+ } catch (e) {}
68
+ });
69
+ }
70
+
71
+ return coordinates;
72
+ }
73
+ """
74
+ coords: List[Dict[str, float]] = await page.evaluate(js_find_coords)
75
+ for item in coords:
76
+ try:
77
+ click_x = item["x"] + 30
78
+ click_y = item["y"] + item["h"] / 2
79
+ await page.mouse.click(click_x, click_y)
80
+ except Exception as err:
81
+ logger.debug(f"Error clicking candidate div: {err}")
82
+ return True
83
+ except Exception as e:
84
+ logger.debug(f"check_turnstile exception: {e}")
85
+ return False
86
+
87
+
88
+ async def start_turnstile_loop(page: Page, stop_event: asyncio.Event) -> None:
89
+ """
90
+ Runs a loop to check and click turnstile widgets until stop_event is set or page closes.
91
+ """
92
+ while not stop_event.is_set():
93
+ if page.is_closed():
94
+ break
95
+ try:
96
+ await check_turnstile(page)
97
+ except Exception:
98
+ pass
99
+ await asyncio.sleep(1.0)
100
+
101
+
102
+ class PlaywrightManager:
103
+ """
104
+ Manager for Playwright lifecycle and anti-detect page instances.
105
+ """
106
+
107
+ def __init__(self) -> None:
108
+ self.pw = None
109
+ self.browser: Optional[Browser] = None
110
+ self.context: Optional[BrowserContext] = None
111
+
112
+ async def initialize(
113
+ self,
114
+ headless: bool = False,
115
+ args: Optional[List[str]] = None,
116
+ proxy: Optional[Dict[str, str]] = None,
117
+ ) -> Tuple[Browser, BrowserContext]:
118
+ if self.browser and self.browser.is_connected():
119
+ return self.browser, self.context
120
+
121
+ self.pw = await async_playwright().start()
122
+
123
+ launch_args = list(DEFAULT_CHROME_FLAGS)
124
+ if args:
125
+ launch_args.extend(args)
126
+
127
+ proxy_config = None
128
+ if proxy and proxy.get("host") and proxy.get("port"):
129
+ server_str = f"{proxy['host']}:{proxy['port']}"
130
+ if not server_str.startswith("http://") and not server_str.startswith("https://") and not server_str.startswith("socks5://"):
131
+ server_str = f"http://{server_str}"
132
+ proxy_config = {"server": server_str}
133
+ if proxy.get("username") and proxy.get("password"):
134
+ proxy_config["username"] = proxy["username"]
135
+ proxy_config["password"] = proxy["password"]
136
+
137
+ self.browser = await self.pw.chromium.launch(
138
+ headless=headless,
139
+ args=launch_args,
140
+ proxy=proxy_config,
141
+ )
142
+
143
+ self.context = await self.browser.new_context(
144
+ viewport={"width": 1920, "height": 1080},
145
+ user_agent=(
146
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
147
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
148
+ "Chrome/120.0.0.0 Safari/537.36"
149
+ ),
150
+ )
151
+
152
+ # Anti-detection script injection
153
+ await self.context.add_init_script(
154
+ "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
155
+ )
156
+
157
+ return self.browser, self.context
158
+
159
+ async def get_page(self, turnstile: bool = False) -> Tuple[Page, Optional[asyncio.Event]]:
160
+ if not self.context:
161
+ await self.initialize()
162
+
163
+ page = await self.context.new_page()
164
+
165
+ stop_event = None
166
+ if turnstile:
167
+ stop_event = asyncio.Event()
168
+ asyncio.create_task(start_turnstile_loop(page, stop_event))
169
+
170
+ return page, stop_event
171
+
172
+ async def close(self) -> None:
173
+ if self.context:
174
+ await self.context.close()
175
+ self.context = None
176
+ if self.browser:
177
+ await self.browser.close()
178
+ self.browser = None
179
+ if self.pw:
180
+ await self.pw.stop()
181
+ self.pw = None
182
+
183
+
184
+ async def connect(
185
+ headless: bool = False,
186
+ args: Optional[List[str]] = None,
187
+ proxy: Optional[Dict[str, str]] = None,
188
+ turnstile: bool = False,
189
+ ) -> Tuple[PlaywrightManager, Page]:
190
+ """
191
+ Connect function matching the reference API. Returns (manager, page).
192
+ """
193
+ manager = PlaywrightManager()
194
+ await manager.initialize(headless=headless, args=args, proxy=proxy)
195
+ page, _ = await manager.get_page(turnstile=turnstile)
196
+ return manager, page
@@ -0,0 +1,33 @@
1
+ """
2
+ CLI entry point for cloudsneak.
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+ from cloudsneak.server import start_server, PORT
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser(
12
+ description="cloudsneak - Cloudflare clearance cookie and Turnstile token solver service"
13
+ )
14
+ parser.add_argument(
15
+ "--host",
16
+ type=str,
17
+ default="0.0.0.0",
18
+ help="Host address to bind the server (default: 0.0.0.0)",
19
+ )
20
+ parser.add_argument(
21
+ "--port",
22
+ type=int,
23
+ default=PORT,
24
+ help=f"Port to run the server on (default: {PORT})",
25
+ )
26
+
27
+ args = parser.parse_args()
28
+ print(f"Starting cloudsneak server on {args.host}:{args.port}...")
29
+ start_server(host=args.host, port=args.port)
30
+
31
+
32
+ if __name__ == "__main__":
33
+ main()
@@ -0,0 +1,117 @@
1
+ """
2
+ Cloudflare Under Attack Mode (IUAM) clearance solver.
3
+ Extracts cf_clearance cookie and associated User-Agent.
4
+ """
5
+
6
+ import asyncio
7
+ import re
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+ from playwright.async_api import Page, Request, Response
12
+
13
+ API_JS_PATH = Path(__file__).parent / "js" / "api.js"
14
+
15
+
16
+ async def solve_iuam(
17
+ data: Dict[str, Any],
18
+ page: Page,
19
+ timeout_ms: int = 60000,
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Solves Cloudflare IUAM challenge on target domain and returns cf_clearance cookie info.
23
+
24
+ :param data: Dictionary containing 'domain' and optional 'proxy' info.
25
+ :param page: Playwright Page instance.
26
+ :param timeout_ms: Maximum time to wait for solution in milliseconds.
27
+ :return: Dict containing cf_clearance, user_agent, and elapsed time.
28
+ """
29
+ domain = data.get("domain")
30
+ if not domain:
31
+ raise ValueError("Missing domain parameter")
32
+
33
+ proxy = data.get("proxy", {})
34
+ if proxy.get("username") and proxy.get("password"):
35
+ # Credentials are set at context launch or routed in playwright context
36
+ pass
37
+
38
+ api_js_content: Optional[bytes] = None
39
+ if API_JS_PATH.exists():
40
+ try:
41
+ api_js_content = API_JS_PATH.read_bytes()
42
+ except Exception as e:
43
+ print(f"Failed to read local api.js: {e}")
44
+
45
+ start_time = time.time()
46
+ solved_future: asyncio.Future = asyncio.get_running_loop().create_future()
47
+
48
+ async def handle_route(route: Any, request: Request):
49
+ url = request.url
50
+ try:
51
+ if "challenges.cloudflare.com/turnstile/v0/b/88d68f5d5ea3/api.js" in url:
52
+ if api_js_content:
53
+ await route.fulfill(
54
+ status=200,
55
+ content_type="application/javascript",
56
+ body=api_js_content,
57
+ headers={"Access-Control-Allow-Origin": "*"},
58
+ )
59
+ else:
60
+ await route.continue_()
61
+ elif (
62
+ url == domain
63
+ or url == domain.rstrip("/") + "/"
64
+ or "challenges.cloudflare.com" in url
65
+ or "/cdn-cgi/challenge-platform/" in url
66
+ ):
67
+ await route.continue_()
68
+ else:
69
+ await route.abort()
70
+ except Exception:
71
+ try:
72
+ await route.continue_()
73
+ except Exception:
74
+ pass
75
+
76
+ async def handle_response(response: Response):
77
+ url = response.url
78
+ if "/cdn-cgi/challenge-platform/" in url:
79
+ try:
80
+ headers = await response.headers_array()
81
+ set_cookie_headers = [
82
+ h["value"] for h in headers if h["name"].lower() == "set-cookie"
83
+ ]
84
+ combined_cookies = "\n".join(set_cookie_headers)
85
+ match = re.search(r"cf_clearance=([^;]+)", combined_cookies)
86
+ if match and not solved_future.done():
87
+ cf_clearance = match.group(1)
88
+ req_headers = await response.request.all_headers()
89
+ user_agent = req_headers.get("user-agent", "")
90
+ elapsed = f"{time.time() - start_time:.2f}s"
91
+ solved_future.set_result(
92
+ {
93
+ "cf_clearance": cf_clearance,
94
+ "user_agent": user_agent,
95
+ "elapsed": elapsed,
96
+ }
97
+ )
98
+ except Exception:
99
+ pass
100
+
101
+ # Register route interception and response listener
102
+ await page.route("**/*", handle_route)
103
+ page.on("response", handle_response)
104
+
105
+ try:
106
+ await page.goto(domain, wait_until="domcontentloaded")
107
+
108
+ result = await asyncio.wait_for(solved_future, timeout=timeout_ms / 1000.0)
109
+ return result
110
+ except asyncio.TimeoutError:
111
+ raise TimeoutError("Timeout Error resolving IUAM challenge")
112
+ finally:
113
+ try:
114
+ await page.unroute("**/*", handle_route)
115
+ except Exception:
116
+ pass
117
+ page.remove_listener("response", handle_response)