StarrySkyWorld commited on
Commit
df4f314
·
verified ·
1 Parent(s): 2410868

Create api_solver.py

Browse files
Files changed (1) hide show
  1. api_solver.py +1027 -0
api_solver.py ADDED
@@ -0,0 +1,1027 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import uuid
5
+ import random
6
+ import logging
7
+ import asyncio
8
+ from typing import Optional, Union
9
+ import argparse
10
+ from quart import Quart, request, jsonify
11
+ from camoufox.async_api import AsyncCamoufox
12
+ from patchright.async_api import async_playwright
13
+ from db_results import init_db, save_result, load_result, cleanup_old_results
14
+ from browser_configs import browser_config
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.text import Text
18
+ from rich.align import Align
19
+ from rich import box
20
+
21
+
22
+
23
+ COLORS = {
24
+ 'MAGENTA': '\033[35m',
25
+ 'BLUE': '\033[34m',
26
+ 'GREEN': '\033[32m',
27
+ 'YELLOW': '\033[33m',
28
+ 'RED': '\033[31m',
29
+ 'RESET': '\033[0m',
30
+ }
31
+
32
+
33
+ class CustomLogger(logging.Logger):
34
+ @staticmethod
35
+ def format_message(level, color, message):
36
+ timestamp = time.strftime('%H:%M:%S')
37
+ return f"[{timestamp}] [{COLORS.get(color)}{level}{COLORS.get('RESET')}] -> {message}"
38
+
39
+ def debug(self, message, *args, **kwargs):
40
+ super().debug(self.format_message('DEBUG', 'MAGENTA', message), *args, **kwargs)
41
+
42
+ def info(self, message, *args, **kwargs):
43
+ super().info(self.format_message('INFO', 'BLUE', message), *args, **kwargs)
44
+
45
+ def success(self, message, *args, **kwargs):
46
+ super().info(self.format_message('SUCCESS', 'GREEN', message), *args, **kwargs)
47
+
48
+ def warning(self, message, *args, **kwargs):
49
+ super().warning(self.format_message('WARNING', 'YELLOW', message), *args, **kwargs)
50
+
51
+ def error(self, message, *args, **kwargs):
52
+ super().error(self.format_message('ERROR', 'RED', message), *args, **kwargs)
53
+
54
+
55
+ logging.setLoggerClass(CustomLogger)
56
+ logger: CustomLogger = logging.getLogger("TurnstileAPIServer") # type: ignore
57
+ logger.setLevel(logging.DEBUG)
58
+ handler = logging.StreamHandler(sys.stdout)
59
+ logger.addHandler(handler)
60
+
61
+
62
+ class TurnstileAPIServer:
63
+
64
+ def __init__(self, headless: bool, useragent: Optional[str], debug: bool, browser_type: str, thread: int, proxy_support: bool, use_random_config: bool = False, browser_name: Optional[str] = None, browser_version: Optional[str] = None):
65
+ self.app = Quart(__name__)
66
+ self.debug = debug
67
+ self.browser_type = browser_type
68
+ self.headless = headless
69
+ self.thread_count = thread
70
+ self.proxy_support = proxy_support
71
+ self.browser_pool = asyncio.Queue()
72
+ self.use_random_config = use_random_config
73
+ self.browser_name = browser_name
74
+ self.browser_version = browser_version
75
+ self.console = Console()
76
+
77
+ # Initialize useragent and sec_ch_ua attributes
78
+ self.useragent = useragent
79
+ self.sec_ch_ua = None
80
+
81
+
82
+ if self.browser_type in ['chromium', 'chrome', 'msedge']:
83
+ if browser_name and browser_version:
84
+ config = browser_config.get_browser_config(browser_name, browser_version)
85
+ if config:
86
+ useragent, sec_ch_ua = config
87
+ self.useragent = useragent
88
+ self.sec_ch_ua = sec_ch_ua
89
+ elif useragent:
90
+ self.useragent = useragent
91
+ else:
92
+ browser, version, useragent, sec_ch_ua = browser_config.get_random_browser_config(self.browser_type)
93
+ self.browser_name = browser
94
+ self.browser_version = version
95
+ self.useragent = useragent
96
+ self.sec_ch_ua = sec_ch_ua
97
+
98
+ self.browser_args = []
99
+ if self.useragent:
100
+ self.browser_args.append(f"--user-agent={self.useragent}")
101
+
102
+ self._setup_routes()
103
+
104
+ def display_welcome(self):
105
+ """Displays welcome screen with logo."""
106
+ self.console.clear()
107
+
108
+ combined_text = Text()
109
+ combined_text.append("\n📢 Channel: ", style="bold white")
110
+ combined_text.append("https://t.me/D3_vin", style="cyan")
111
+ combined_text.append("\n💬 Chat: ", style="bold white")
112
+ combined_text.append("https://t.me/D3vin_chat", style="cyan")
113
+ combined_text.append("\n📁 GitHub: ", style="bold white")
114
+ combined_text.append("https://github.com/D3-vin", style="cyan")
115
+ combined_text.append("\n📁 Version: ", style="bold white")
116
+ combined_text.append("1.2a", style="green")
117
+ combined_text.append("\n")
118
+
119
+ info_panel = Panel(
120
+ Align.left(combined_text),
121
+ title="[bold blue]Turnstile Solver[/bold blue]",
122
+ subtitle="[bold magenta]Dev by D3vin[/bold magenta]",
123
+ box=box.ROUNDED,
124
+ border_style="bright_blue",
125
+ padding=(0, 1),
126
+ width=50
127
+ )
128
+
129
+ self.console.print(info_panel)
130
+ self.console.print()
131
+
132
+
133
+
134
+
135
+ def _setup_routes(self) -> None:
136
+ """Set up the application routes."""
137
+ self.app.before_serving(self._startup)
138
+ self.app.route('/turnstile', methods=['GET'])(self.process_turnstile)
139
+ self.app.route('/result', methods=['GET'])(self.get_result)
140
+ self.app.route('/')(self.index)
141
+
142
+
143
+ async def _startup(self) -> None:
144
+ """Initialize the browser and page pool on startup."""
145
+ self.display_welcome()
146
+ logger.info("Starting browser initialization")
147
+ try:
148
+ await init_db()
149
+ await self._initialize_browser()
150
+
151
+ # Запускаем периодическую очистку старых результатов
152
+ asyncio.create_task(self._periodic_cleanup())
153
+
154
+ except Exception as e:
155
+ logger.error(f"Failed to initialize browser: {str(e)}")
156
+ raise
157
+
158
+ async def _initialize_browser(self) -> None:
159
+ """Initialize the browser and create the page pool."""
160
+ playwright = None
161
+ camoufox = None
162
+
163
+ if self.browser_type in ['chromium', 'chrome', 'msedge']:
164
+ playwright = await async_playwright().start()
165
+ elif self.browser_type == "camoufox":
166
+ camoufox = AsyncCamoufox(headless=self.headless)
167
+
168
+ browser_configs = []
169
+ for _ in range(self.thread_count):
170
+ if self.browser_type in ['chromium', 'chrome', 'msedge']:
171
+ if self.use_random_config:
172
+ browser, version, useragent, sec_ch_ua = browser_config.get_random_browser_config(self.browser_type)
173
+ elif self.browser_name and self.browser_version:
174
+ config = browser_config.get_browser_config(self.browser_name, self.browser_version)
175
+ if config:
176
+ useragent, sec_ch_ua = config
177
+ browser = self.browser_name
178
+ version = self.browser_version
179
+ else:
180
+ browser, version, useragent, sec_ch_ua = browser_config.get_random_browser_config(self.browser_type)
181
+ else:
182
+ browser = getattr(self, 'browser_name', 'custom')
183
+ version = getattr(self, 'browser_version', 'custom')
184
+ useragent = self.useragent
185
+ sec_ch_ua = getattr(self, 'sec_ch_ua', '')
186
+ else:
187
+ # Для camoufox и других браузеров используем значения по умолчанию
188
+ browser = self.browser_type
189
+ version = 'custom'
190
+ useragent = self.useragent
191
+ sec_ch_ua = getattr(self, 'sec_ch_ua', '')
192
+
193
+
194
+ browser_configs.append({
195
+ 'browser_name': browser,
196
+ 'browser_version': version,
197
+ 'useragent': useragent,
198
+ 'sec_ch_ua': sec_ch_ua
199
+ })
200
+
201
+ for i in range(self.thread_count):
202
+ config = browser_configs[i]
203
+
204
+ browser_args = [
205
+ "--window-position=0,0",
206
+ "--force-device-scale-factor=1"
207
+ ]
208
+ if config['useragent']:
209
+ browser_args.append(f"--user-agent={config['useragent']}")
210
+
211
+ browser = None
212
+ if self.browser_type in ['chromium', 'chrome', 'msedge'] and playwright:
213
+ browser = await playwright.chromium.launch(
214
+ channel=self.browser_type,
215
+ headless=self.headless,
216
+ args=browser_args
217
+ )
218
+ elif self.browser_type == "camoufox" and camoufox:
219
+ browser = await camoufox.start()
220
+
221
+ if browser:
222
+ await self.browser_pool.put((i+1, browser, config))
223
+
224
+ if self.debug:
225
+ logger.info(f"Browser {i + 1} initialized successfully with {config['browser_name']} {config['browser_version']}")
226
+
227
+ logger.info(f"Browser pool initialized with {self.browser_pool.qsize()} browsers")
228
+
229
+ if self.use_random_config:
230
+ logger.info(f"Each browser in pool received random configuration")
231
+ elif self.browser_name and self.browser_version:
232
+ logger.info(f"All browsers using configuration: {self.browser_name} {self.browser_version}")
233
+ else:
234
+ logger.info("Using custom configuration")
235
+
236
+ if self.debug:
237
+ for i, config in enumerate(browser_configs):
238
+ logger.debug(f"Browser {i+1} config: {config['browser_name']} {config['browser_version']}")
239
+ logger.debug(f"Browser {i+1} User-Agent: {config['useragent']}")
240
+ logger.debug(f"Browser {i+1} Sec-CH-UA: {config['sec_ch_ua']}")
241
+
242
+ async def _periodic_cleanup(self):
243
+ """Periodic cleanup of old results every hour"""
244
+ while True:
245
+ try:
246
+ await asyncio.sleep(3600)
247
+ deleted_count = await cleanup_old_results(days_old=7)
248
+ if deleted_count > 0:
249
+ logger.info(f"Cleaned up {deleted_count} old results")
250
+ except Exception as e:
251
+ logger.error(f"Error during periodic cleanup: {e}")
252
+
253
+ async def _antishadow_inject(self, page):
254
+ await page.add_init_script("""
255
+ (function() {
256
+ const originalAttachShadow = Element.prototype.attachShadow;
257
+ Element.prototype.attachShadow = function(init) {
258
+ const shadow = originalAttachShadow.call(this, init);
259
+ if (init.mode === 'closed') {
260
+ window.__lastClosedShadowRoot = shadow;
261
+ }
262
+ return shadow;
263
+ };
264
+ })();
265
+ """)
266
+
267
+
268
+
269
+ async def _optimized_route_handler(self, route):
270
+ """Оптимизированный обработчик маршрутов для экономии ресурсов."""
271
+ url = route.request.url
272
+ resource_type = route.request.resource_type
273
+
274
+ allowed_types = {'document', 'script', 'xhr', 'fetch'}
275
+
276
+ allowed_domains = [
277
+ 'challenges.cloudflare.com',
278
+ 'static.cloudflareinsights.com',
279
+ 'cloudflare.com'
280
+ ]
281
+
282
+ if resource_type in allowed_types:
283
+ await route.continue_()
284
+ elif any(domain in url for domain in allowed_domains):
285
+ await route.continue_()
286
+ else:
287
+ await route.abort()
288
+
289
+ async def _block_rendering(self, page):
290
+ """Блокировка рендеринга для экономии ресурсов"""
291
+ await page.route("**/*", self._optimized_route_handler)
292
+
293
+ async def _unblock_rendering(self, page):
294
+ """Разблокировка рендеринга"""
295
+ await page.unroute("**/*", self._optimized_route_handler)
296
+
297
+ async def _find_turnstile_elements(self, page, index: int):
298
+ """Умная проверка всех возможных Turnstile элементов"""
299
+ selectors = [
300
+ '.cf-turnstile',
301
+ '[data-sitekey]',
302
+ 'iframe[src*="turnstile"]',
303
+ 'iframe[title*="widget"]',
304
+ 'div[id*="turnstile"]',
305
+ 'div[class*="turnstile"]'
306
+ ]
307
+
308
+ elements = []
309
+ for selector in selectors:
310
+ try:
311
+ # Безопасная проверка count()
312
+ try:
313
+ count = await page.locator(selector).count()
314
+ except Exception:
315
+ # Если count() дает ошибку, пропускаем этот селектор
316
+ continue
317
+
318
+ if count > 0:
319
+ elements.append((selector, count))
320
+ if self.debug:
321
+ logger.debug(f"Browser {index}: Found {count} elements with selector '{selector}'")
322
+ except Exception as e:
323
+ if self.debug:
324
+ logger.debug(f"Browser {index}: Selector '{selector}' failed: {str(e)}")
325
+ continue
326
+
327
+ return elements
328
+
329
+ async def _find_and_click_checkbox(self, page, index: int):
330
+ """Найти и кликнуть по чекбоксу Turnstile CAPTCHA внутри iframe"""
331
+ try:
332
+ # Пробуем разные селекторы iframe с защитой от ошибок
333
+ iframe_selectors = [
334
+ 'iframe[src*="challenges.cloudflare.com"]',
335
+ 'iframe[src*="turnstile"]',
336
+ 'iframe[title*="widget"]'
337
+ ]
338
+
339
+ iframe_locator = None
340
+ for selector in iframe_selectors:
341
+ try:
342
+ test_locator = page.locator(selector).first
343
+ # Безопасная проверка count для iframe
344
+ try:
345
+ iframe_count = await test_locator.count()
346
+ except Exception:
347
+ iframe_count = 0
348
+
349
+ if iframe_count > 0:
350
+ iframe_locator = test_locator
351
+ if self.debug:
352
+ logger.debug(f"Browser {index}: Found Turnstile iframe with selector: {selector}")
353
+ break
354
+ except Exception as e:
355
+ if self.debug:
356
+ logger.debug(f"Browser {index}: Iframe selector '{selector}' failed: {str(e)}")
357
+ continue
358
+
359
+ if iframe_locator:
360
+ try:
361
+ # Получаем frame из iframe
362
+ iframe_element = await iframe_locator.element_handle()
363
+ frame = await iframe_element.content_frame()
364
+
365
+ if frame:
366
+ # Ищем чекбокс внутри iframe
367
+ checkbox_selectors = [
368
+ 'input[type="checkbox"]',
369
+ '.cb-lb input[type="checkbox"]',
370
+ 'label input[type="checkbox"]'
371
+ ]
372
+
373
+ for selector in checkbox_selectors:
374
+ try:
375
+ # Полностью избегаем locator.count() в iframe - используем альтернативный подход
376
+ try:
377
+ # Пробуем кликнуть напрямую без count проверки
378
+ checkbox = frame.locator(selector).first
379
+ await checkbox.click(timeout=2000)
380
+ if self.debug:
381
+ logger.debug(f"Browser {index}: Successfully clicked checkbox in iframe with selector '{selector}'")
382
+ return True
383
+ except Exception as click_e:
384
+ # Если прямой клик не сработал, записываем в debug но не падаем
385
+ if self.debug:
386
+ logger.debug(f"Browser {index}: Direct checkbox click failed for '{selector}': {str(click_e)}")
387
+ continue
388
+ except Exception as e:
389
+ if self.debug:
390
+ logger.debug(f"Browser {index}: Iframe checkbox selector '{selector}' failed: {str(e)}")
391
+ continue
392
+
393
+ # Если нашли iframe, но не смогли кликнуть чекбокс, пробуем клик по iframe
394
+ try:
395
+ if self.debug:
396
+ logger.debug(f"Browser {index}: Trying to click iframe directly as fallback")
397
+ await iframe_locator.click(timeout=1000)
398
+ return True
399
+ except Exception as e:
400
+ if self.debug:
401
+ logger.debug(f"Browser {index}: Iframe direct click failed: {str(e)}")
402
+
403
+ except Exception as e:
404
+ if self.debug:
405
+ logger.debug(f"Browser {index}: Failed to access iframe content: {str(e)}")
406
+
407
+ except Exception as e:
408
+ if self.debug:
409
+ logger.debug(f"Browser {index}: General iframe search failed: {str(e)}")
410
+
411
+ return False
412
+
413
+ async def _try_click_strategies(self, page, index: int):
414
+ strategies = [
415
+ ('checkbox_click', lambda: self._find_and_click_checkbox(page, index)),
416
+ ('direct_widget', lambda: self._safe_click(page, '.cf-turnstile', index)),
417
+ ('iframe_click', lambda: self._safe_click(page, 'iframe[src*="turnstile"]', index)),
418
+ ('js_click', lambda: page.evaluate("document.querySelector('.cf-turnstile')?.click()")),
419
+ ('sitekey_attr', lambda: self._safe_click(page, '[data-sitekey]', index)),
420
+ ('any_turnstile', lambda: self._safe_click(page, '*[class*="turnstile"]', index)),
421
+ ('xpath_click', lambda: self._safe_click(page, "//div[@class='cf-turnstile']", index))
422
+ ]
423
+
424
+ for strategy_name, strategy_func in strategies:
425
+ try:
426
+ result = await strategy_func()
427
+ if result is True or result is None: # None означает успех для большинства стратегий
428
+ if self.debug:
429
+ logger.debug(f"Browser {index}: Click strategy '{strategy_name}' succeeded")
430
+ return True
431
+ except Exception as e:
432
+ if self.debug:
433
+ logger.debug(f"Browser {index}: Click strategy '{strategy_name}' failed: {str(e)}")
434
+ continue
435
+
436
+ return False
437
+
438
+ async def _safe_click(self, page, selector: str, index: int):
439
+ """Полностью безопасный клик с максимальной защитой от ошибок"""
440
+ try:
441
+ # Пробуем кликнуть напрямую без count() проверки
442
+ locator = page.locator(selector).first
443
+ await locator.click(timeout=1000)
444
+ return True
445
+ except Exception as e:
446
+ # Логируем ошибку только в debug режиме
447
+ if self.debug and "Can't query n-th element" not in str(e):
448
+ logger.debug(f"Browser {index}: Safe click failed for '{selector}': {str(e)}")
449
+ return False
450
+
451
+ async def _inject_captcha_directly(self, page, websiteKey: str, action: str = '', cdata: str = '', index: int = 0):
452
+ """Inject CAPTCHA directly into the target website"""
453
+ script = f"""
454
+ // Remove any existing turnstile widgets first
455
+ document.querySelectorAll('.cf-turnstile').forEach(el => el.remove());
456
+ document.querySelectorAll('[data-sitekey]').forEach(el => el.remove());
457
+
458
+ // Create turnstile widget directly on the page
459
+ const captchaDiv = document.createElement('div');
460
+ captchaDiv.className = 'cf-turnstile';
461
+ captchaDiv.setAttribute('data-sitekey', '{websiteKey}');
462
+ captchaDiv.setAttribute('data-callback', 'onTurnstileCallback');
463
+ {f'captchaDiv.setAttribute("data-action", "{action}");' if action else ''}
464
+ {f'captchaDiv.setAttribute("data-cdata", "{cdata}");' if cdata else ''}
465
+ captchaDiv.style.position = 'fixed';
466
+ captchaDiv.style.top = '20px';
467
+ captchaDiv.style.left = '20px';
468
+ captchaDiv.style.zIndex = '9999';
469
+ captchaDiv.style.backgroundColor = 'white';
470
+ captchaDiv.style.padding = '15px';
471
+ captchaDiv.style.border = '2px solid #0f79af';
472
+ captchaDiv.style.borderRadius = '8px';
473
+ captchaDiv.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
474
+
475
+ // Add to body immediately
476
+ document.body.appendChild(captchaDiv);
477
+
478
+ // Load Turnstile script and render widget
479
+ const loadTurnstile = () => {{
480
+ const script = document.createElement('script');
481
+ script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
482
+ script.async = true;
483
+ script.defer = true;
484
+ script.onload = function() {{
485
+ console.log('Turnstile script loaded');
486
+ // Wait a bit for script to initialize
487
+ setTimeout(() => {{
488
+ if (window.turnstile && window.turnstile.render) {{
489
+ try {{
490
+ window.turnstile.render(captchaDiv, {{
491
+ sitekey: '{websiteKey}',
492
+ {f'action: "{action}",' if action else ''}
493
+ {f'cdata: "{cdata}",' if cdata else ''}
494
+ callback: function(token) {{
495
+ console.log('Turnstile solved with token:', token);
496
+ // Create hidden input for token
497
+ let tokenInput = document.querySelector('input[name="cf-turnstile-response"]');
498
+ if (!tokenInput) {{
499
+ tokenInput = document.createElement('input');
500
+ tokenInput.type = 'hidden';
501
+ tokenInput.name = 'cf-turnstile-response';
502
+ document.body.appendChild(tokenInput);
503
+ }}
504
+ tokenInput.value = token;
505
+ }},
506
+ 'error-callback': function(error) {{
507
+ console.log('Turnstile error:', error);
508
+ }}
509
+ }});
510
+ }} catch (e) {{
511
+ console.log('Turnstile render error:', e);
512
+ }}
513
+ }} else {{
514
+ console.log('Turnstile API not available');
515
+ }}
516
+ }}, 1000);
517
+ }};
518
+ script.onerror = function() {{
519
+ console.log('Failed to load Turnstile script');
520
+ }};
521
+ document.head.appendChild(script);
522
+ }};
523
+
524
+ // Check if Turnstile is already loaded
525
+ if (window.turnstile) {{
526
+ console.log('Turnstile already loaded, rendering immediately');
527
+ try {{
528
+ window.turnstile.render(captchaDiv, {{
529
+ sitekey: '{websiteKey}',
530
+ {f'action: "{action}",' if action else ''}
531
+ {f'cdata: "{cdata}",' if cdata else ''}
532
+ callback: function(token) {{
533
+ console.log('Turnstile solved with token:', token);
534
+ let tokenInput = document.querySelector('input[name="cf-turnstile-response"]');
535
+ if (!tokenInput) {{
536
+ tokenInput = document.createElement('input');
537
+ tokenInput.type = 'hidden';
538
+ tokenInput.name = 'cf-turnstile-response';
539
+ document.body.appendChild(tokenInput);
540
+ }}
541
+ tokenInput.value = token;
542
+ }},
543
+ 'error-callback': function(error) {{
544
+ console.log('Turnstile error:', error);
545
+ }}
546
+ }});
547
+ }} catch (e) {{
548
+ console.log('Immediate render error:', e);
549
+ loadTurnstile();
550
+ }}
551
+ }} else {{
552
+ loadTurnstile();
553
+ }}
554
+
555
+ // Setup global callback
556
+ window.onTurnstileCallback = function(token) {{
557
+ console.log('Global turnstile callback executed:', token);
558
+ }};
559
+ """
560
+
561
+ await page.evaluate(script)
562
+ if self.debug:
563
+ logger.debug(f"Browser {index}: Injected CAPTCHA directly into website with sitekey: {websiteKey}")
564
+
565
+ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: Optional[str] = None, cdata: Optional[str] = None):
566
+ """Solve the Turnstile challenge."""
567
+ proxy = None
568
+
569
+ index, browser, browser_config = await self.browser_pool.get()
570
+
571
+ try:
572
+ if hasattr(browser, 'is_connected') and not browser.is_connected():
573
+ if self.debug:
574
+ logger.warning(f"Browser {index}: Browser disconnected, skipping")
575
+ await self.browser_pool.put((index, browser, browser_config))
576
+ await save_result(task_id, "turnstile", {"value": "CAPTCHA_FAIL", "elapsed_time": 0})
577
+ return
578
+ except Exception as e:
579
+ if self.debug:
580
+ logger.warning(f"Browser {index}: Cannot check browser state: {str(e)}")
581
+
582
+ if self.proxy_support:
583
+ proxy_file_path = os.path.join(os.getcwd(), "proxies.txt")
584
+
585
+ try:
586
+ with open(proxy_file_path) as proxy_file:
587
+ proxies = [line.strip() for line in proxy_file if line.strip()]
588
+
589
+ proxy = random.choice(proxies) if proxies else None
590
+
591
+ if self.debug and proxy:
592
+ logger.debug(f"Browser {index}: Selected proxy: {proxy}")
593
+ elif self.debug and not proxy:
594
+ logger.debug(f"Browser {index}: No proxies available")
595
+
596
+ except FileNotFoundError:
597
+ logger.warning(f"Proxy file not found: {proxy_file_path}")
598
+ proxy = None
599
+ except Exception as e:
600
+ logger.error(f"Error reading proxy file: {str(e)}")
601
+ proxy = None
602
+
603
+ if proxy:
604
+ if '@' in proxy:
605
+ try:
606
+ scheme_part, auth_part = proxy.split('://')
607
+ auth, address = auth_part.split('@')
608
+ username, password = auth.split(':')
609
+ ip, port = address.split(':')
610
+ if self.debug:
611
+ logger.debug(f"Browser {index}: Creating context with proxy {scheme_part}://{ip}:{port} (auth: {username}:***)")
612
+ context_options = {
613
+ "proxy": {
614
+ "server": f"{scheme_part}://{ip}:{port}",
615
+ "username": username,
616
+ "password": password
617
+ },
618
+ "user_agent": browser_config['useragent']
619
+ }
620
+
621
+ if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
622
+ context_options['extra_http_headers'] = {
623
+ 'sec-ch-ua': browser_config['sec_ch_ua']
624
+ }
625
+
626
+ context = await browser.new_context(**context_options)
627
+ except ValueError:
628
+ raise ValueError(f"Invalid proxy format: {proxy}")
629
+ else:
630
+ parts = proxy.split(':')
631
+ if len(parts) == 5:
632
+ proxy_scheme, proxy_ip, proxy_port, proxy_user, proxy_pass = parts
633
+ if self.debug:
634
+ logger.debug(f"Browser {index}: Creating context with proxy {proxy_scheme}://{proxy_ip}:{proxy_port} (auth: {proxy_user}:***)")
635
+ context_options = {
636
+ "proxy": {
637
+ "server": f"{proxy_scheme}://{proxy_ip}:{proxy_port}",
638
+ "username": proxy_user,
639
+ "password": proxy_pass
640
+ },
641
+ "user_agent": browser_config['useragent']
642
+ }
643
+
644
+ if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
645
+ context_options['extra_http_headers'] = {
646
+ 'sec-ch-ua': browser_config['sec_ch_ua']
647
+ }
648
+
649
+ context = await browser.new_context(**context_options)
650
+ elif len(parts) == 3:
651
+ if self.debug:
652
+ logger.debug(f"Browser {index}: Creating context with proxy {proxy}")
653
+ context_options = {
654
+ "proxy": {"server": f"{proxy}"},
655
+ "user_agent": browser_config['useragent']
656
+ }
657
+
658
+ if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
659
+ context_options['extra_http_headers'] = {
660
+ 'sec-ch-ua': browser_config['sec_ch_ua']
661
+ }
662
+
663
+ context = await browser.new_context(**context_options)
664
+ else:
665
+ raise ValueError(f"Invalid proxy format: {proxy}")
666
+ else:
667
+ if self.debug:
668
+ logger.debug(f"Browser {index}: Creating context without proxy")
669
+ context_options = {"user_agent": browser_config['useragent']}
670
+
671
+ if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
672
+ context_options['extra_http_headers'] = {
673
+ 'sec-ch-ua': browser_config['sec_ch_ua']
674
+ }
675
+
676
+ context = await browser.new_context(**context_options)
677
+ else:
678
+ context_options = {"user_agent": browser_config['useragent']}
679
+
680
+ if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
681
+ context_options['extra_http_headers'] = {
682
+ 'sec-ch-ua': browser_config['sec_ch_ua']
683
+ }
684
+
685
+ context = await browser.new_context(**context_options)
686
+
687
+ page = await context.new_page()
688
+
689
+ await self._antishadow_inject(page)
690
+
691
+ await self._block_rendering(page)
692
+
693
+ await page.add_init_script("""
694
+ Object.defineProperty(navigator, 'webdriver', {
695
+ get: () => undefined,
696
+ });
697
+
698
+ window.chrome = {
699
+ runtime: {},
700
+ loadTimes: function() {},
701
+ csi: function() {},
702
+ };
703
+ """)
704
+
705
+ if self.browser_type in ['chromium', 'chrome', 'msedge']:
706
+ await page.set_viewport_size({"width": 500, "height": 100})
707
+ if self.debug:
708
+ logger.debug(f"Browser {index}: Set viewport size to 500x240")
709
+
710
+ start_time = time.time()
711
+
712
+ try:
713
+ if self.debug:
714
+ logger.debug(f"Browser {index}: Starting Turnstile solve for URL: {url} with Sitekey: {sitekey} | Action: {action} | Cdata: {cdata} | Proxy: {proxy}")
715
+ logger.debug(f"Browser {index}: Setting up optimized page loading with resource blocking")
716
+
717
+ if self.debug:
718
+ logger.debug(f"Browser {index}: Loading real website directly: {url}")
719
+
720
+ await page.goto(url, wait_until='domcontentloaded', timeout=30000)
721
+
722
+ await self._unblock_rendering(page)
723
+
724
+ # Сразу инъектируем виджет Turnstile на целевой сайт
725
+ if self.debug:
726
+ logger.debug(f"Browser {index}: Injecting Turnstile widget directly into target site")
727
+
728
+ await self._inject_captcha_directly(page, sitekey, action or '', cdata or '', index)
729
+
730
+ # Ждем время для загрузки и рендеринга виджета
731
+ await asyncio.sleep(3)
732
+
733
+ locator = page.locator('input[name="cf-turnstile-response"]')
734
+ max_attempts = 30
735
+ click_count = 0
736
+ max_clicks = 10
737
+
738
+ for attempt in range(max_attempts):
739
+ try:
740
+ # Безопасная проверка количества элементов с токеном
741
+ try:
742
+ count = await locator.count()
743
+ except Exception as e:
744
+ if self.debug:
745
+ logger.debug(f"Browser {index}: Locator count failed on attempt {attempt + 1}: {str(e)}")
746
+ count = 0
747
+
748
+ if count == 0:
749
+ if self.debug and attempt % 5 == 0:
750
+ logger.debug(f"Browser {index}: No token elements found on attempt {attempt + 1}")
751
+ elif count == 1:
752
+ # Если только один элемент, проверяем его токен
753
+ try:
754
+ token = await locator.input_value(timeout=500)
755
+ if token:
756
+ elapsed_time = round(time.time() - start_time, 3)
757
+ logger.success(f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds")
758
+ await save_result(task_id, "turnstile", {"value": token, "elapsed_time": elapsed_time})
759
+ return
760
+ except Exception as e:
761
+ if self.debug:
762
+ logger.debug(f"Browser {index}: Single token element check failed: {str(e)}")
763
+ else:
764
+ # Если несколько элементов, проверяем все по очереди
765
+ if self.debug:
766
+ logger.debug(f"Browser {index}: Found {count} token elements, checking all")
767
+
768
+ for i in range(count):
769
+ try:
770
+ element_token = await locator.nth(i).input_value(timeout=500)
771
+ if element_token:
772
+ elapsed_time = round(time.time() - start_time, 3)
773
+ logger.success(f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{element_token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds")
774
+ await save_result(task_id, "turnstile", {"value": element_token, "elapsed_time": elapsed_time})
775
+ return
776
+ except Exception as e:
777
+ if self.debug:
778
+ logger.debug(f"Browser {index}: Token element {i} check failed: {str(e)}")
779
+ continue
780
+
781
+ if attempt > 2 and attempt % 3 == 0 and click_count < max_clicks:
782
+ click_success = await self._try_click_strategies(page, index)
783
+ click_count += 1
784
+ if click_success and self.debug:
785
+ logger.debug(f"Browser {index}: Click successful (click #{click_count}/{max_clicks})")
786
+ elif not click_success and self.debug:
787
+ logger.debug(f"Browser {index}: All click strategies failed on attempt {attempt + 1} (click #{click_count}/{max_clicks})")
788
+
789
+ # Адаптивное ожидание
790
+ wait_time = min(0.5 + (attempt * 0.05), 2.0)
791
+ await asyncio.sleep(wait_time)
792
+
793
+ if self.debug and attempt % 5 == 0:
794
+ logger.debug(f"Browser {index}: Attempt {attempt + 1}/{max_attempts} - Waiting for token (clicks: {click_count}/{max_clicks})")
795
+
796
+ except Exception as e:
797
+ if self.debug:
798
+ logger.debug(f"Browser {index}: Attempt {attempt + 1} error: {str(e)}")
799
+ continue
800
+
801
+ elapsed_time = round(time.time() - start_time, 3)
802
+ await save_result(task_id, "turnstile", {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time})
803
+ if self.debug:
804
+ logger.error(f"Browser {index}: Error solving Turnstile in {COLORS.get('RED')}{elapsed_time}{COLORS.get('RESET')} Seconds")
805
+ except Exception as e:
806
+ elapsed_time = round(time.time() - start_time, 3)
807
+ await save_result(task_id, "turnstile", {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time})
808
+ if self.debug:
809
+ logger.error(f"Browser {index}: Error solving Turnstile: {str(e)}")
810
+ finally:
811
+ if self.debug:
812
+ logger.debug(f"Browser {index}: Closing browser context and cleaning up")
813
+
814
+ try:
815
+ await context.close()
816
+ if self.debug:
817
+ logger.debug(f"Browser {index}: Context closed successfully")
818
+ except Exception as e:
819
+ if self.debug:
820
+ logger.warning(f"Browser {index}: Error closing context: {str(e)}")
821
+
822
+ try:
823
+ if hasattr(browser, 'is_connected') and browser.is_connected():
824
+ await self.browser_pool.put((index, browser, browser_config))
825
+ if self.debug:
826
+ logger.debug(f"Browser {index}: Browser returned to pool")
827
+ else:
828
+ if self.debug:
829
+ logger.warning(f"Browser {index}: Browser disconnected, not returning to pool")
830
+ except Exception as e:
831
+ if self.debug:
832
+ logger.warning(f"Browser {index}: Error returning browser to pool: {str(e)}")
833
+
834
+
835
+
836
+
837
+
838
+
839
+ async def process_turnstile(self):
840
+ """Handle the /turnstile endpoint requests."""
841
+ url = request.args.get('url')
842
+ sitekey = request.args.get('sitekey')
843
+ action = request.args.get('action')
844
+ cdata = request.args.get('cdata')
845
+
846
+ if not url or not sitekey:
847
+ return jsonify({
848
+ "errorId": 1,
849
+ "errorCode": "ERROR_WRONG_PAGEURL",
850
+ "errorDescription": "Both 'url' and 'sitekey' are required"
851
+ }), 200
852
+
853
+ task_id = str(uuid.uuid4())
854
+ await save_result(task_id, "turnstile", {
855
+ "status": "CAPTCHA_NOT_READY",
856
+ "createTime": int(time.time()),
857
+ "url": url,
858
+ "sitekey": sitekey,
859
+ "action": action,
860
+ "cdata": cdata
861
+ })
862
+
863
+ try:
864
+ asyncio.create_task(self._solve_turnstile(task_id=task_id, url=url, sitekey=sitekey, action=action, cdata=cdata))
865
+
866
+ if self.debug:
867
+ logger.debug(f"Request completed with taskid {task_id}.")
868
+ return jsonify({
869
+ "errorId": 0,
870
+ "taskId": task_id
871
+ }), 200
872
+ except Exception as e:
873
+ logger.error(f"Unexpected error processing request: {str(e)}")
874
+ return jsonify({
875
+ "errorId": 1,
876
+ "errorCode": "ERROR_UNKNOWN",
877
+ "errorDescription": str(e)
878
+ }), 200
879
+
880
+ async def get_result(self):
881
+ """Return solved data"""
882
+ task_id = request.args.get('id')
883
+
884
+ if not task_id:
885
+ return jsonify({
886
+ "errorId": 1,
887
+ "errorCode": "ERROR_WRONG_CAPTCHA_ID",
888
+ "errorDescription": "Invalid task ID/Request parameter"
889
+ }), 200
890
+
891
+ result = await load_result(task_id)
892
+ if not result:
893
+ return jsonify({
894
+ "errorId": 1,
895
+ "errorCode": "ERROR_CAPTCHA_UNSOLVABLE",
896
+ "errorDescription": "Task not found"
897
+ }), 200
898
+
899
+ if result == "CAPTCHA_NOT_READY" or (isinstance(result, dict) and result.get("status") == "CAPTCHA_NOT_READY"):
900
+ return jsonify({"status": "processing"}), 200
901
+
902
+ if isinstance(result, dict) and result.get("value") == "CAPTCHA_FAIL":
903
+ return jsonify({
904
+ "errorId": 1,
905
+ "errorCode": "ERROR_CAPTCHA_UNSOLVABLE",
906
+ "errorDescription": "Workers could not solve the Captcha"
907
+ }), 200
908
+
909
+ if isinstance(result, dict) and result.get("value") and result.get("value") != "CAPTCHA_FAIL":
910
+ return jsonify({
911
+ "errorId": 0,
912
+ "status": "ready",
913
+ "solution": {
914
+ "token": result["value"]
915
+ }
916
+ }), 200
917
+ else:
918
+ return jsonify({
919
+ "errorId": 1,
920
+ "errorCode": "ERROR_CAPTCHA_UNSOLVABLE",
921
+ "errorDescription": "Workers could not solve the Captcha"
922
+ }), 200
923
+
924
+
925
+
926
+ @staticmethod
927
+ async def index():
928
+ """Serve the API documentation page."""
929
+ return """
930
+ <!DOCTYPE html>
931
+ <html lang="en">
932
+ <head>
933
+ <meta charset="UTF-8">
934
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
935
+ <title>Turnstile Solver API</title>
936
+ <script src="https://cdn.tailwindcss.com"></script>
937
+ </head>
938
+ <body class="bg-gray-900 text-gray-200 min-h-screen flex items-center justify-center">
939
+ <div class="bg-gray-800 p-8 rounded-lg shadow-md max-w-2xl w-full border border-red-500">
940
+ <h1 class="text-3xl font-bold mb-6 text-center text-red-500">Welcome to Turnstile Solver API</h1>
941
+
942
+ <p class="mb-4 text-gray-300">To use the turnstile service, send a GET request to
943
+ <code class="bg-red-700 text-white px-2 py-1 rounded">/turnstile</code> with the following query parameters:</p>
944
+
945
+ <ul class="list-disc pl-6 mb-6 text-gray-300">
946
+ <li><strong>url</strong>: The URL where Turnstile is to be validated</li>
947
+ <li><strong>sitekey</strong>: The site key for Turnstile</li>
948
+ </ul>
949
+
950
+ <div class="bg-gray-700 p-4 rounded-lg mb-6 border border-red-500">
951
+ <p class="font-semibold mb-2 text-red-400">Example usage:</p>
952
+ <code class="text-sm break-all text-red-300">/turnstile?url=https://example.com&sitekey=sitekey</code>
953
+ </div>
954
+
955
+
956
+ <div class="bg-gray-700 p-4 rounded-lg mb-6">
957
+ <p class="text-gray-200 font-semibold mb-3">📢 Connect with Us</p>
958
+ <div class="space-y-2 text-sm">
959
+ <p class="text-gray-300">
960
+ 📢 <strong>Channel:</strong>
961
+ <a href="https://t.me/D3_vin" class="text-red-300 hover:underline">https://t.me/D3_vin</a>
962
+ - Latest updates and releases
963
+ </p>
964
+ <p class="text-gray-300">
965
+ 💬 <strong>Chat:</strong>
966
+ <a href="https://t.me/D3vin_chat" class="text-red-300 hover:underline">https://t.me/D3vin_chat</a>
967
+ - Community support and discussions
968
+ </p>
969
+ <p class="text-gray-300">
970
+ 📁 <strong>GitHub:</strong>
971
+ <a href="https://github.com/D3-vin" class="text-red-300 hover:underline">https://github.com/D3-vin</a>
972
+ - Source code and development
973
+ </p>
974
+ </div>
975
+ </div>
976
+ </div>
977
+ </body>
978
+ </html>
979
+ """
980
+
981
+
982
+ def parse_args():
983
+ """Parse command-line arguments."""
984
+ parser = argparse.ArgumentParser(description="Turnstile API Server")
985
+
986
+ parser.add_argument('--no-headless', action='store_true', help='Run the browser with GUI (disable headless mode). By default, headless mode is enabled.')
987
+ parser.add_argument('--useragent', type=str, help='User-Agent string (if not specified, random configuration is used)')
988
+ parser.add_argument('--debug', action='store_true', help='Enable or disable debug mode for additional logging and troubleshooting information (default: False)')
989
+ parser.add_argument('--browser_type', type=str, default='chromium', help='Specify the browser type for the solver. Supported options: chromium, chrome, msedge, camoufox (default: chromium)')
990
+ parser.add_argument('--thread', type=int, default=4, help='Set the number of browser threads to use for multi-threaded mode. Increasing this will speed up execution but requires more resources (default: 1)')
991
+ parser.add_argument('--proxy', action='store_true', help='Enable proxy support for the solver (Default: False)')
992
+ parser.add_argument('--random', action='store_true', help='Use random User-Agent and Sec-CH-UA configuration from pool')
993
+ parser.add_argument('--browser', type=str, help='Specify browser name to use (e.g., chrome, firefox)')
994
+ parser.add_argument('--version', type=str, help='Specify browser version to use (e.g., 139, 141)')
995
+ parser.add_argument('--host', type=str, default='0.0.0.0', help='Specify the IP address where the API solver runs. (Default: 127.0.0.1)')
996
+ parser.add_argument('--port', type=str, default='5072', help='Set the port for the API solver to listen on. (Default: 5072)')
997
+ return parser.parse_args()
998
+
999
+
1000
+ def create_app(headless: bool, useragent: str, debug: bool, browser_type: str, thread: int, proxy_support: bool, use_random_config: bool, browser_name: str, browser_version: str) -> Quart:
1001
+ server = TurnstileAPIServer(headless=headless, useragent=useragent, debug=debug, browser_type=browser_type, thread=thread, proxy_support=proxy_support, use_random_config=use_random_config, browser_name=browser_name, browser_version=browser_version)
1002
+ return server.app
1003
+
1004
+
1005
+ if __name__ == '__main__':
1006
+ args = parse_args()
1007
+ browser_types = [
1008
+ 'chromium',
1009
+ 'chrome',
1010
+ 'msedge',
1011
+ 'camoufox',
1012
+ ]
1013
+ if args.browser_type not in browser_types:
1014
+ logger.error(f"Unknown browser type: {COLORS.get('RED')}{args.browser_type}{COLORS.get('RESET')} Available browser types: {browser_types}")
1015
+ else:
1016
+ app = create_app(
1017
+ headless=not args.no_headless,
1018
+ debug=args.debug,
1019
+ useragent=args.useragent,
1020
+ browser_type=args.browser_type,
1021
+ thread=args.thread,
1022
+ proxy_support=args.proxy,
1023
+ use_random_config=args.random,
1024
+ browser_name=args.browser,
1025
+ browser_version=args.version
1026
+ )
1027
+ app.run(host=args.host, port=int(args.port))