Hiro121234 commited on
Commit
75fddaa
·
verified ·
1 Parent(s): 1f82855

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +499 -477
app.py CHANGED
@@ -1,477 +1,499 @@
1
- """
2
- Turnstile Solver API - Hugging Face Spaces Deployment
3
- A web service for solving Cloudflare Turnstile challenges
4
- """
5
-
6
- import os
7
- import sys
8
- import time
9
- import uuid
10
- import json
11
- import logging
12
- import asyncio
13
- from quart import Quart, request, jsonify, render_template_string
14
- from patchright.async_api import async_playwright
15
-
16
- # Configure logging
17
- logging.basicConfig(
18
- level=logging.INFO,
19
- format='[%(asctime)s] [%(levelname)s] -> %(message)s',
20
- datefmt='%H:%M:%S'
21
- )
22
- logger = logging.getLogger("TurnstileAPI")
23
-
24
- app = Quart(__name__)
25
-
26
- # Store results in memory
27
- results = {}
28
-
29
- # HTML template for the Turnstile widget
30
- HTML_TEMPLATE = """
31
- <!DOCTYPE html>
32
- <html lang="en">
33
- <head>
34
- <meta charset="UTF-8">
35
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
- <title>Turnstile Solver</title>
37
- <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async></script>
38
- </head>
39
- <body>
40
- <!-- cf turnstile -->
41
- </body>
42
- </html>
43
- """
44
-
45
- # Home page
46
- HOME_PAGE = """
47
- <!DOCTYPE html>
48
- <html lang="en">
49
- <head>
50
- <meta charset="UTF-8">
51
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
52
- <title>Turnstile Solver API</title>
53
- <style>
54
- * { margin: 0; padding: 0; box-sizing: border-box; }
55
- body {
56
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
57
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
58
- min-height: 100vh;
59
- display: flex;
60
- align-items: center;
61
- justify-content: center;
62
- padding: 20px;
63
- }
64
- .container {
65
- background: white;
66
- border-radius: 20px;
67
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
68
- max-width: 800px;
69
- width: 100%;
70
- padding: 40px;
71
- }
72
- h1 {
73
- color: #667eea;
74
- margin-bottom: 10px;
75
- font-size: 2.5em;
76
- }
77
- .subtitle {
78
- color: #666;
79
- margin-bottom: 30px;
80
- font-size: 1.1em;
81
- }
82
- .endpoint {
83
- background: #f7f7f7;
84
- border-left: 4px solid #667eea;
85
- padding: 20px;
86
- margin: 20px 0;
87
- border-radius: 8px;
88
- }
89
- .endpoint h3 {
90
- color: #333;
91
- margin-bottom: 10px;
92
- }
93
- .endpoint code {
94
- background: #e8e8e8;
95
- padding: 2px 8px;
96
- border-radius: 4px;
97
- font-family: 'Courier New', monospace;
98
- color: #d63384;
99
- }
100
- .endpoint pre {
101
- background: #2d2d2d;
102
- color: #f8f8f2;
103
- padding: 15px;
104
- border-radius: 8px;
105
- overflow-x: auto;
106
- margin-top: 10px;
107
- }
108
- .params {
109
- margin: 15px 0;
110
- }
111
- .param {
112
- margin: 8px 0;
113
- padding-left: 20px;
114
- }
115
- .param strong {
116
- color: #667eea;
117
- }
118
- .badge {
119
- display: inline-block;
120
- background: #667eea;
121
- color: white;
122
- padding: 4px 12px;
123
- border-radius: 20px;
124
- font-size: 0.85em;
125
- margin-right: 10px;
126
- }
127
- .warning {
128
- background: #fff3cd;
129
- border-left: 4px solid #ffc107;
130
- padding: 15px;
131
- margin: 20px 0;
132
- border-radius: 8px;
133
- }
134
- .footer {
135
- text-align: center;
136
- margin-top: 30px;
137
- color: #666;
138
- font-size: 0.9em;
139
- }
140
- a {
141
- color: #667eea;
142
- text-decoration: none;
143
- }
144
- a:hover {
145
- text-decoration: underline;
146
- }
147
- </style>
148
- </head>
149
- <body>
150
- <div class="container">
151
- <h1>🔐 Turnstile Solver API</h1>
152
- <p class="subtitle">Solve Cloudflare Turnstile challenges programmatically</p>
153
-
154
- <div class="warning">
155
- <strong>⚠️ Note:</strong> This service is for educational and testing purposes only.
156
- Please respect website terms of service and rate limits.
157
- </div>
158
-
159
- <div class="endpoint">
160
- <h3><span class="badge">GET</span><span class="badge" style="background: #28a745;">POST</span> /solve</h3>
161
- <p><strong>⚡ Synchronous Solve</strong> - Send request and get token immediately (recommended)</p>
162
-
163
- <div class="params">
164
- <div class="param"><strong>url</strong> (required) - Target website URL</div>
165
- <div class="param"><strong>sitekey</strong> (required) - Turnstile sitekey</div>
166
- <div class="param"><strong>action</strong> (optional) - Action parameter</div>
167
- <div class="param"><strong>cdata</strong> (optional) - Custom data</div>
168
- </div>
169
-
170
- <p style="margin-top: 10px;"><strong>GET Request:</strong></p>
171
- <pre>curl "https://your-space.hf.space/solve?url=https://example.com&sitekey=0x4AAA..."</pre>
172
-
173
- <p style="margin-top: 10px;"><strong>POST Request:</strong></p>
174
- <pre>curl -X POST "https://your-space.hf.space/solve" \
175
- -H "Content-Type: application/json" \
176
- -d '{"url":"https://example.com","sitekey":"0x4AAA..."}'</pre>
177
-
178
- <p style="margin-top: 10px;"><strong>Response (success):</strong></p>
179
- <pre>{"success": true, "token": "0.KBtT-r...", "elapsed_time": 7.5}</pre>
180
-
181
- <p style="margin-top: 10px;"><strong>Response (failed):</strong></p>
182
- <pre>{"success": false, "error": "Failed to solve", "elapsed_time": 10.2}</pre>
183
- </div>
184
-
185
- <div class="endpoint">
186
- <h3><span class="badge">GET</span> /turnstile</h3>
187
- <p><strong>Async Mode</strong> - Create a solve task (for advanced use)</p>
188
-
189
- <div class="params">
190
- <div class="param"><strong>url</strong> (required) - Target website URL</div>
191
- <div class="param"><strong>sitekey</strong> (required) - Turnstile sitekey</div>
192
- <div class="param"><strong>action</strong> (optional) - Action parameter</div>
193
- <div class="param"><strong>cdata</strong> (optional) - Custom data</div>
194
- </div>
195
-
196
- <pre>curl "https://your-space.hf.space/turnstile?url=https://example.com&sitekey=0x4AAA..."</pre>
197
-
198
- <p style="margin-top: 10px;"><strong>Response:</strong></p>
199
- <pre>{"task_id": "abc-123-def"}</pre>
200
- </div>
201
-
202
- <div class="endpoint">
203
- <h3><span class="badge">GET</span> /result</h3>
204
- <p>Get the result of an async task</p>
205
-
206
- <div class="params">
207
- <div class="param"><strong>id</strong> (required) - Task ID from /turnstile</div>
208
- </div>
209
-
210
- <pre>curl "https://your-space.hf.space/result?id=abc-123-def"</pre>
211
-
212
- <p style="margin-top: 10px;"><strong>Response (success):</strong></p>
213
- <pre>{"elapsed_time": 7.5, "value": "0.KBtT-r..."}</pre>
214
-
215
- <p style="margin-top: 10px;"><strong>Response (pending):</strong></p>
216
- <pre>"CAPTCHA_NOT_READY"</pre>
217
- </div>
218
-
219
- <div class="endpoint">
220
- <h3>Example Usage - Synchronous (Recommended)</h3>
221
- <pre>
222
- # Simple GET request - returns token immediately
223
- curl "https://your-space.hf.space/solve?url=https://createvision.ai&sitekey=0x4AAAAAAB6qwG1HcvybuFht"
224
-
225
- # Response:
226
- # {"success": true, "token": "0.KBtT-r...", "elapsed_time": 7.5}
227
- </pre>
228
- </div>
229
-
230
- <div class="endpoint">
231
- <h3>Example Usage - Async Mode</h3>
232
- <pre>
233
- # Step 1: Create task
234
- TASK_ID=$(curl -s "https://your-space.hf.space/turnstile?url=https://example.com&sitekey=0x4AAA..." | jq -r .task_id)
235
-
236
- # Step 2: Wait and get result
237
- sleep 10
238
- curl "https://your-space.hf.space/result?id=$TASK_ID"
239
- </pre>
240
- </div>
241
-
242
- <div class="footer">
243
- <p>Powered by <a href="https://github.com/Theyka/Turnstile-Solver" target="_blank">Turnstile-Solver</a></p>
244
- <p>Deployed on <a href="https://huggingface.co/spaces" target="_blank">Hugging Face Spaces</a></p>
245
- </div>
246
- </div>
247
- </body>
248
- </html>
249
- """
250
-
251
-
252
- async def solve_turnstile(task_id: str, url: str, sitekey: str, action: str = None, cdata: str = None):
253
- """Solve the Turnstile challenge"""
254
- start_time = time.time()
255
-
256
- try:
257
- logger.info(f"Task {task_id}: Starting solve for {url} with sitekey {sitekey}")
258
-
259
- playwright = await async_playwright().start()
260
- browser = await playwright.chromium.launch(
261
- headless=True,
262
- args=['--no-sandbox', '--disable-setuid-sandbox']
263
- )
264
-
265
- context = await browser.new_context()
266
- page = await context.new_page()
267
-
268
- # Setup page with Turnstile widget
269
- url_with_slash = url + "/" if not url.endswith("/") else url
270
- turnstile_div = f'<div class="cf-turnstile" style="background: white;" data-sitekey="{sitekey}"'
271
- if action:
272
- turnstile_div += f' data-action="{action}"'
273
- if cdata:
274
- turnstile_div += f' data-cdata="{cdata}"'
275
- turnstile_div += '></div>'
276
-
277
- page_data = HTML_TEMPLATE.replace("<!-- cf turnstile -->", turnstile_div)
278
-
279
- await page.route(url_with_slash, lambda route: route.fulfill(body=page_data, status=200))
280
- await page.goto(url_with_slash)
281
-
282
- # Set widget dimensions
283
- await page.eval_on_selector("//div[@class='cf-turnstile']", "el => el.style.width = '70px'")
284
-
285
- # Try to get the response
286
- for attempt in range(10):
287
- try:
288
- turnstile_check = await page.input_value("[name=cf-turnstile-response]", timeout=2000)
289
- if turnstile_check == "":
290
- await page.locator("//div[@class='cf-turnstile']").click(timeout=1000)
291
- await asyncio.sleep(0.5)
292
- else:
293
- elapsed_time = round(time.time() - start_time, 3)
294
- logger.info(f"Task {task_id}: Solved in {elapsed_time}s")
295
- results[task_id] = {"value": turnstile_check, "elapsed_time": elapsed_time}
296
- break
297
- except:
298
- pass
299
-
300
- if results.get(task_id) == "CAPTCHA_NOT_READY":
301
- elapsed_time = round(time.time() - start_time, 3)
302
- results[task_id] = {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time}
303
- logger.error(f"Task {task_id}: Failed after {elapsed_time}s")
304
-
305
- await context.close()
306
- await browser.close()
307
- await playwright.stop()
308
-
309
- except Exception as e:
310
- elapsed_time = round(time.time() - start_time, 3)
311
- results[task_id] = {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time}
312
- logger.error(f"Task {task_id}: Error - {str(e)}")
313
-
314
-
315
- @app.route('/')
316
- async def index():
317
- """Serve the home page"""
318
- return HOME_PAGE
319
-
320
-
321
- @app.route('/solve', methods=['GET', 'POST'])
322
- async def solve_sync():
323
- """Solve Turnstile and return token immediately (synchronous)"""
324
- # Get parameters from GET or POST
325
- if request.method == 'POST':
326
- data = await request.get_json()
327
- url = data.get('url')
328
- sitekey = data.get('sitekey')
329
- action = data.get('action')
330
- cdata = data.get('cdata')
331
- else:
332
- url = request.args.get('url')
333
- sitekey = request.args.get('sitekey')
334
- action = request.args.get('action')
335
- cdata = request.args.get('cdata')
336
-
337
- if not url or not sitekey:
338
- return jsonify({
339
- "success": False,
340
- "error": "Both 'url' and 'sitekey' are required"
341
- }), 400
342
-
343
- task_id = str(uuid.uuid4())
344
- logger.info(f"Task {task_id}: Starting synchronous solve for {url}")
345
-
346
- start_time = time.time()
347
-
348
- try:
349
- playwright = await async_playwright().start()
350
- browser = await playwright.chromium.launch(
351
- headless=True,
352
- args=['--no-sandbox', '--disable-setuid-sandbox']
353
- )
354
-
355
- context = await browser.new_context()
356
- page = await context.new_page()
357
-
358
- # Setup page with Turnstile widget
359
- url_with_slash = url + "/" if not url.endswith("/") else url
360
- turnstile_div = f'<div class="cf-turnstile" style="background: white;" data-sitekey="{sitekey}"'
361
- if action:
362
- turnstile_div += f' data-action="{action}"'
363
- if cdata:
364
- turnstile_div += f' data-cdata="{cdata}"'
365
- turnstile_div += '></div>'
366
-
367
- page_data = HTML_TEMPLATE.replace("<!-- cf turnstile -->", turnstile_div)
368
-
369
- await page.route(url_with_slash, lambda route: route.fulfill(body=page_data, status=200))
370
- await page.goto(url_with_slash)
371
-
372
- # Set widget dimensions
373
- await page.eval_on_selector("//div[@class='cf-turnstile']", "el => el.style.width = '70px'")
374
-
375
- # Try to get the response
376
- token = None
377
- for attempt in range(10):
378
- try:
379
- turnstile_check = await page.input_value("[name=cf-turnstile-response]", timeout=2000)
380
- if turnstile_check == "":
381
- await page.locator("//div[@class='cf-turnstile']").click(timeout=1000)
382
- await asyncio.sleep(0.5)
383
- else:
384
- token = turnstile_check
385
- break
386
- except:
387
- pass
388
-
389
- await context.close()
390
- await browser.close()
391
- await playwright.stop()
392
-
393
- elapsed_time = round(time.time() - start_time, 3)
394
-
395
- if token:
396
- logger.info(f"Task {task_id}: Solved in {elapsed_time}s")
397
- return jsonify({
398
- "success": True,
399
- "token": token,
400
- "elapsed_time": elapsed_time
401
- }), 200
402
- else:
403
- logger.error(f"Task {task_id}: Failed after {elapsed_time}s")
404
- return jsonify({
405
- "success": False,
406
- "error": "Failed to solve Turnstile",
407
- "elapsed_time": elapsed_time
408
- }), 422
409
-
410
- except Exception as e:
411
- elapsed_time = round(time.time() - start_time, 3)
412
- logger.error(f"Task {task_id}: Error - {str(e)}")
413
- return jsonify({
414
- "success": False,
415
- "error": str(e),
416
- "elapsed_time": elapsed_time
417
- }), 500
418
-
419
-
420
- @app.route('/turnstile', methods=['GET'])
421
- async def process_turnstile():
422
- """Handle the /turnstile endpoint requests (async task-based)"""
423
- url = request.args.get('url')
424
- sitekey = request.args.get('sitekey')
425
- action = request.args.get('action')
426
- cdata = request.args.get('cdata')
427
-
428
- if not url or not sitekey:
429
- return jsonify({
430
- "status": "error",
431
- "error": "Both 'url' and 'sitekey' are required"
432
- }), 400
433
-
434
- task_id = str(uuid.uuid4())
435
- results[task_id] = "CAPTCHA_NOT_READY"
436
-
437
- try:
438
- asyncio.create_task(solve_turnstile(task_id=task_id, url=url, sitekey=sitekey, action=action, cdata=cdata))
439
- logger.info(f"Task {task_id}: Created")
440
- return jsonify({"task_id": task_id}), 202
441
- except Exception as e:
442
- logger.error(f"Error creating task: {str(e)}")
443
- return jsonify({
444
- "status": "error",
445
- "error": str(e)
446
- }), 500
447
-
448
-
449
- @app.route('/result', methods=['GET'])
450
- async def get_result():
451
- """Return solved data"""
452
- task_id = request.args.get('id')
453
-
454
- if not task_id or task_id not in results:
455
- return jsonify({"status": "error", "error": "Invalid task ID"}), 400
456
-
457
- result = results[task_id]
458
-
459
- if result == "CAPTCHA_NOT_READY":
460
- return "CAPTCHA_NOT_READY", 200
461
-
462
- status_code = 200
463
- if isinstance(result, dict) and "CAPTCHA_FAIL" in result.get("value", ""):
464
- status_code = 422
465
-
466
- return jsonify(result), status_code
467
-
468
-
469
- @app.route('/health', methods=['GET'])
470
- async def health():
471
- """Health check endpoint"""
472
- return jsonify({"status": "healthy", "tasks": len(results)}), 200
473
-
474
-
475
- if __name__ == '__main__':
476
- port = int(os.environ.get('PORT', 7860))
477
- app.run(host='0.0.0.0', port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Turnstile Solver API - Hugging Face Spaces Deployment
3
+ A web service for solving Cloudflare Turnstile challenges
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import time
9
+ import uuid
10
+ import json
11
+ import logging
12
+ import asyncio
13
+ from quart import Quart, request, jsonify, render_template_string
14
+ from patchright.async_api import async_playwright
15
+
16
+ # Configure logging
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='[%(asctime)s] [%(levelname)s] -> %(message)s',
20
+ datefmt='%H:%M:%S'
21
+ )
22
+ logger = logging.getLogger("TurnstileAPI")
23
+
24
+ app = Quart(__name__)
25
+
26
+ # Store results in memory
27
+ results = {}
28
+
29
+ # HTML template for the Turnstile widget
30
+ HTML_TEMPLATE = """
31
+ <!DOCTYPE html>
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="UTF-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
+ <title>Turnstile Solver</title>
37
+ <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async></script>
38
+ </head>
39
+ <body>
40
+ <!-- cf turnstile -->
41
+ </body>
42
+ </html>
43
+ """
44
+
45
+ # Home page
46
+ HOME_PAGE = """
47
+ <!DOCTYPE html>
48
+ <html lang="en">
49
+ <head>
50
+ <meta charset="UTF-8">
51
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
52
+ <title>Turnstile Solver API</title>
53
+ <style>
54
+ * { margin: 0; padding: 0; box-sizing: border-box; }
55
+ body {
56
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
57
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
58
+ min-height: 100vh;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ padding: 20px;
63
+ }
64
+ .container {
65
+ background: white;
66
+ border-radius: 20px;
67
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
68
+ max-width: 800px;
69
+ width: 100%;
70
+ padding: 40px;
71
+ }
72
+ h1 {
73
+ color: #667eea;
74
+ margin-bottom: 10px;
75
+ font-size: 2.5em;
76
+ }
77
+ .subtitle {
78
+ color: #666;
79
+ margin-bottom: 30px;
80
+ font-size: 1.1em;
81
+ }
82
+ .endpoint {
83
+ background: #f7f7f7;
84
+ border-left: 4px solid #667eea;
85
+ padding: 20px;
86
+ margin: 20px 0;
87
+ border-radius: 8px;
88
+ }
89
+ .endpoint h3 {
90
+ color: #333;
91
+ margin-bottom: 10px;
92
+ }
93
+ .endpoint code {
94
+ background: #e8e8e8;
95
+ padding: 2px 8px;
96
+ border-radius: 4px;
97
+ font-family: 'Courier New', monospace;
98
+ color: #d63384;
99
+ }
100
+ .endpoint pre {
101
+ background: #2d2d2d;
102
+ color: #f8f8f2;
103
+ padding: 15px;
104
+ border-radius: 8px;
105
+ overflow-x: auto;
106
+ margin-top: 10px;
107
+ }
108
+ .params {
109
+ margin: 15px 0;
110
+ }
111
+ .param {
112
+ margin: 8px 0;
113
+ padding-left: 20px;
114
+ }
115
+ .param strong {
116
+ color: #667eea;
117
+ }
118
+ .badge {
119
+ display: inline-block;
120
+ background: #667eea;
121
+ color: white;
122
+ padding: 4px 12px;
123
+ border-radius: 20px;
124
+ font-size: 0.85em;
125
+ margin-right: 10px;
126
+ }
127
+ .warning {
128
+ background: #fff3cd;
129
+ border-left: 4px solid #ffc107;
130
+ padding: 15px;
131
+ margin: 20px 0;
132
+ border-radius: 8px;
133
+ }
134
+ .footer {
135
+ text-align: center;
136
+ margin-top: 30px;
137
+ color: #666;
138
+ font-size: 0.9em;
139
+ }
140
+ a {
141
+ color: #667eea;
142
+ text-decoration: none;
143
+ }
144
+ a:hover {
145
+ text-decoration: underline;
146
+ }
147
+ </style>
148
+ </head>
149
+ <body>
150
+ <div class="container">
151
+ <h1>🔐 Turnstile Solver API</h1>
152
+ <p class="subtitle">Solve Cloudflare Turnstile challenges programmatically</p>
153
+
154
+ <div class="warning">
155
+ <strong>⚠️ Note:</strong> This service is for educational and testing purposes only.
156
+ Please respect website terms of service and rate limits.
157
+ </div>
158
+
159
+ <div class="endpoint">
160
+ <h3><span class="badge">GET</span><span class="badge" style="background: #28a745;">POST</span> /solve</h3>
161
+ <p><strong>⚡ Synchronous Solve</strong> - Send request and get token immediately (recommended)</p>
162
+
163
+ <div class="params">
164
+ <div class="param"><strong>url</strong> (required) - Target website URL</div>
165
+ <div class="param"><strong>sitekey</strong> (required) - Turnstile sitekey</div>
166
+ <div class="param"><strong>action</strong> (optional) - Action parameter</div>
167
+ <div class="param"><strong>cdata</strong> (optional) - Custom data</div>
168
+ </div>
169
+
170
+ <p style="margin-top: 10px;"><strong>GET Request:</strong></p>
171
+ <pre>curl "https://your-space.hf.space/solve?url=https://example.com&sitekey=0x4AAA..."</pre>
172
+
173
+ <p style="margin-top: 10px;"><strong>POST Request:</strong></p>
174
+ <pre>curl -X POST "https://your-space.hf.space/solve" \
175
+ -H "Content-Type: application/json" \
176
+ -d '{"url":"https://example.com","sitekey":"0x4AAA..."}'</pre>
177
+
178
+ <p style="margin-top: 10px;"><strong>Response (success):</strong></p>
179
+ <pre>{"success": true, "token": "0.KBtT-r...", "elapsed_time": 7.5}</pre>
180
+
181
+ <p style="margin-top: 10px;"><strong>Response (failed):</strong></p>
182
+ <pre>{"success": false, "error": "Failed to solve", "elapsed_time": 10.2}</pre>
183
+ </div>
184
+
185
+ <div class="endpoint">
186
+ <h3><span class="badge">GET</span> /turnstile</h3>
187
+ <p><strong>Async Mode</strong> - Create a solve task (for advanced use)</p>
188
+
189
+ <div class="params">
190
+ <div class="param"><strong>url</strong> (required) - Target website URL</div>
191
+ <div class="param"><strong>sitekey</strong> (required) - Turnstile sitekey</div>
192
+ <div class="param"><strong>action</strong> (optional) - Action parameter</div>
193
+ <div class="param"><strong>cdata</strong> (optional) - Custom data</div>
194
+ </div>
195
+
196
+ <pre>curl "https://your-space.hf.space/turnstile?url=https://example.com&sitekey=0x4AAA..."</pre>
197
+
198
+ <p style="margin-top: 10px;"><strong>Response:</strong></p>
199
+ <pre>{"task_id": "abc-123-def"}</pre>
200
+ </div>
201
+
202
+ <div class="endpoint">
203
+ <h3><span class="badge">GET</span> /result</h3>
204
+ <p>Get the result of an async task</p>
205
+
206
+ <div class="params">
207
+ <div class="param"><strong>id</strong> (required) - Task ID from /turnstile</div>
208
+ </div>
209
+
210
+ <pre>curl "https://your-space.hf.space/result?id=abc-123-def"</pre>
211
+
212
+ <p style="margin-top: 10px;"><strong>Response (success):</strong></p>
213
+ <pre>{"elapsed_time": 7.5, "value": "0.KBtT-r..."}</pre>
214
+
215
+ <p style="margin-top: 10px;"><strong>Response (pending):</strong></p>
216
+ <pre>"CAPTCHA_NOT_READY"</pre>
217
+ </div>
218
+
219
+ <div class="endpoint">
220
+ <h3>Example Usage - Synchronous (Recommended)</h3>
221
+ <pre>
222
+ # Simple GET request - returns token immediately
223
+ curl "https://your-space.hf.space/solve?url=https://createvision.ai&sitekey=0x4AAAAAAB6qwG1HcvybuFht"
224
+
225
+ # Response:
226
+ # {"success": true, "token": "0.KBtT-r...", "elapsed_time": 7.5}
227
+ </pre>
228
+ </div>
229
+
230
+ <div class="endpoint">
231
+ <h3>Example Usage - Async Mode</h3>
232
+ <pre>
233
+ # Step 1: Create task
234
+ TASK_ID=$(curl -s "https://your-space.hf.space/turnstile?url=https://example.com&sitekey=0x4AAA..." | jq -r .task_id)
235
+
236
+ # Step 2: Wait and get result
237
+ sleep 10
238
+ curl "https://your-space.hf.space/result?id=$TASK_ID"
239
+ </pre>
240
+ </div>
241
+
242
+ <div class="footer">
243
+ <p>Powered by <a href="https://github.com/Theyka/Turnstile-Solver" target="_blank">Turnstile-Solver</a></p>
244
+ <p>Deployed on <a href="https://huggingface.co/spaces" target="_blank">Hugging Face Spaces</a></p>
245
+ </div>
246
+ </div>
247
+ </body>
248
+ </html>
249
+ """
250
+
251
+
252
+ async def solve_turnstile(task_id: str, url: str, sitekey: str, action: str = None, cdata: str = None):
253
+ """Solve the Turnstile challenge"""
254
+ start_time = time.time()
255
+
256
+ try:
257
+ logger.info(f"Task {task_id}: Starting solve for {url} with sitekey {sitekey}")
258
+
259
+ playwright = await async_playwright().start()
260
+ browser = await playwright.chromium.launch(
261
+ headless=True,
262
+ args=['--no-sandbox', '--disable-setuid-sandbox']
263
+ )
264
+
265
+ context = await browser.new_context()
266
+ page = await context.new_page()
267
+
268
+ # Setup page with Turnstile widget
269
+ url_with_slash = url + "/" if not url.endswith("/") else url
270
+ turnstile_div = f'<div class="cf-turnstile" style="background: white;" data-sitekey="{sitekey}"'
271
+ if action:
272
+ turnstile_div += f' data-action="{action}"'
273
+ if cdata:
274
+ turnstile_div += f' data-cdata="{cdata}"'
275
+ turnstile_div += '></div>'
276
+
277
+ page_data = HTML_TEMPLATE.replace("<!-- cf turnstile -->", turnstile_div)
278
+
279
+ await page.route(url_with_slash, lambda route: route.fulfill(body=page_data, status=200))
280
+ await page.goto(url_with_slash)
281
+
282
+ # Set widget dimensions
283
+ await page.eval_on_selector("//div[@class='cf-turnstile']", "el => el.style.width = '70px'")
284
+
285
+ # Try to get the response
286
+ for attempt in range(10):
287
+ try:
288
+ turnstile_check = await page.input_value("[name=cf-turnstile-response]", timeout=2000)
289
+ if turnstile_check == "":
290
+ await page.locator("//div[@class='cf-turnstile']").click(timeout=1000)
291
+ await asyncio.sleep(0.5)
292
+ else:
293
+ elapsed_time = round(time.time() - start_time, 3)
294
+ logger.info(f"Task {task_id}: Solved in {elapsed_time}s")
295
+ results[task_id] = {"value": turnstile_check, "elapsed_time": elapsed_time}
296
+ break
297
+ except:
298
+ pass
299
+
300
+ if results.get(task_id) == "CAPTCHA_NOT_READY":
301
+ elapsed_time = round(time.time() - start_time, 3)
302
+ results[task_id] = {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time}
303
+ logger.error(f"Task {task_id}: Failed after {elapsed_time}s")
304
+
305
+ await context.close()
306
+ await browser.close()
307
+ await playwright.stop()
308
+
309
+ except Exception as e:
310
+ elapsed_time = round(time.time() - start_time, 3)
311
+ results[task_id] = {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time}
312
+ logger.error(f"Task {task_id}: Error - {str(e)}")
313
+
314
+
315
+ @app.route('/')
316
+ async def index():
317
+ """Serve the home page"""
318
+ return HOME_PAGE
319
+
320
+
321
+ async def solve_single_attempt(url: str, sitekey: str, action: str = None, cdata: str = None):
322
+ """Single solve attempt"""
323
+ try:
324
+ playwright = await async_playwright().start()
325
+ browser = await playwright.chromium.launch(
326
+ headless=True,
327
+ args=['--no-sandbox', '--disable-setuid-sandbox']
328
+ )
329
+
330
+ context = await browser.new_context()
331
+ page = await context.new_page()
332
+
333
+ # Setup page with Turnstile widget
334
+ url_with_slash = url + "/" if not url.endswith("/") else url
335
+ turnstile_div = f'<div class="cf-turnstile" style="background: white;" data-sitekey="{sitekey}"'
336
+ if action:
337
+ turnstile_div += f' data-action="{action}"'
338
+ if cdata:
339
+ turnstile_div += f' data-cdata="{cdata}"'
340
+ turnstile_div += '></div>'
341
+
342
+ page_data = HTML_TEMPLATE.replace("<!-- cf turnstile -->", turnstile_div)
343
+
344
+ await page.route(url_with_slash, lambda route: route.fulfill(body=page_data, status=200))
345
+ await page.goto(url_with_slash)
346
+
347
+ # Set widget dimensions
348
+ await page.eval_on_selector("//div[@class='cf-turnstile']", "el => el.style.width = '70px'")
349
+
350
+ # Try to get the response
351
+ token = None
352
+ for attempt in range(10):
353
+ try:
354
+ turnstile_check = await page.input_value("[name=cf-turnstile-response]", timeout=2000)
355
+ if turnstile_check == "":
356
+ await page.locator("//div[@class='cf-turnstile']").click(timeout=1000)
357
+ await asyncio.sleep(0.5)
358
+ else:
359
+ token = turnstile_check
360
+ break
361
+ except:
362
+ pass
363
+
364
+ await context.close()
365
+ await browser.close()
366
+ await playwright.stop()
367
+
368
+ return token
369
+
370
+ except Exception as e:
371
+ logger.error(f"Solve attempt error: {str(e)}")
372
+ return None
373
+
374
+
375
+ @app.route('/solve', methods=['GET', 'POST'])
376
+ async def solve_sync():
377
+ """Solve Turnstile with automatic retry (60 second timeout)"""
378
+ # Get parameters from GET or POST
379
+ if request.method == 'POST':
380
+ data = await request.get_json()
381
+ url = data.get('url')
382
+ sitekey = data.get('sitekey')
383
+ action = data.get('action')
384
+ cdata = data.get('cdata')
385
+ else:
386
+ url = request.args.get('url')
387
+ sitekey = request.args.get('sitekey')
388
+ action = request.args.get('action')
389
+ cdata = request.args.get('cdata')
390
+
391
+ if not url or not sitekey:
392
+ return jsonify({
393
+ "success": False,
394
+ "error": "Both 'url' and 'sitekey' are required"
395
+ }), 400
396
+
397
+ task_id = str(uuid.uuid4())
398
+ logger.info(f"Task {task_id}: Starting solve with 60s retry for {url}")
399
+
400
+ start_time = time.time()
401
+ attempt = 0
402
+ timeout = 60 # 60 second timeout
403
+
404
+ # Keep trying for 60 seconds
405
+ while True:
406
+ attempt += 1
407
+ elapsed = time.time() - start_time
408
+
409
+ # Check if timeout reached
410
+ if elapsed >= timeout:
411
+ logger.error(f"Task {task_id}: Timeout after {elapsed:.1f}s ({attempt} attempts)")
412
+ return jsonify({
413
+ "success": False,
414
+ "error": "Timeout: Failed to solve after 60 seconds",
415
+ "elapsed_time": round(elapsed, 3),
416
+ "attempts": attempt
417
+ }), 422
418
+
419
+ logger.info(f"Task {task_id}: Attempt {attempt} (elapsed: {elapsed:.1f}s)")
420
+
421
+ # Try to solve
422
+ attempt_start = time.time()
423
+ token = await solve_single_attempt(url, sitekey, action, cdata)
424
+ attempt_time = time.time() - attempt_start
425
+
426
+ if token:
427
+ total_time = time.time() - start_time
428
+ logger.info(f"Task {task_id}: SUCCESS on attempt {attempt} (total: {total_time:.1f}s)")
429
+ return jsonify({
430
+ "success": True,
431
+ "token": token,
432
+ "elapsed_time": round(total_time, 3),
433
+ "attempts": attempt,
434
+ "solve_time": round(attempt_time, 3)
435
+ }), 200
436
+ else:
437
+ logger.warning(f"Task {task_id}: Attempt {attempt} failed (took {attempt_time:.1f}s)")
438
+ # Small delay before retry
439
+ await asyncio.sleep(1)
440
+
441
+
442
+ @app.route('/turnstile', methods=['GET'])
443
+ async def process_turnstile():
444
+ """Handle the /turnstile endpoint requests (async task-based)"""
445
+ url = request.args.get('url')
446
+ sitekey = request.args.get('sitekey')
447
+ action = request.args.get('action')
448
+ cdata = request.args.get('cdata')
449
+
450
+ if not url or not sitekey:
451
+ return jsonify({
452
+ "status": "error",
453
+ "error": "Both 'url' and 'sitekey' are required"
454
+ }), 400
455
+
456
+ task_id = str(uuid.uuid4())
457
+ results[task_id] = "CAPTCHA_NOT_READY"
458
+
459
+ try:
460
+ asyncio.create_task(solve_turnstile(task_id=task_id, url=url, sitekey=sitekey, action=action, cdata=cdata))
461
+ logger.info(f"Task {task_id}: Created")
462
+ return jsonify({"task_id": task_id}), 202
463
+ except Exception as e:
464
+ logger.error(f"Error creating task: {str(e)}")
465
+ return jsonify({
466
+ "status": "error",
467
+ "error": str(e)
468
+ }), 500
469
+
470
+
471
+ @app.route('/result', methods=['GET'])
472
+ async def get_result():
473
+ """Return solved data"""
474
+ task_id = request.args.get('id')
475
+
476
+ if not task_id or task_id not in results:
477
+ return jsonify({"status": "error", "error": "Invalid task ID"}), 400
478
+
479
+ result = results[task_id]
480
+
481
+ if result == "CAPTCHA_NOT_READY":
482
+ return "CAPTCHA_NOT_READY", 200
483
+
484
+ status_code = 200
485
+ if isinstance(result, dict) and "CAPTCHA_FAIL" in result.get("value", ""):
486
+ status_code = 422
487
+
488
+ return jsonify(result), status_code
489
+
490
+
491
+ @app.route('/health', methods=['GET'])
492
+ async def health():
493
+ """Health check endpoint"""
494
+ return jsonify({"status": "healthy", "tasks": len(results)}), 200
495
+
496
+
497
+ if __name__ == '__main__':
498
+ port = int(os.environ.get('PORT', 7860))
499
+ app.run(host='0.0.0.0', port=port)