Skip to content

Commit 4cde212

Browse files
committed
v2.0 - Global browser proxy feature added
1 parent 9862ad5 commit 4cde212

File tree

8 files changed

+96
-6
lines changed

8 files changed

+96
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
build/
22
site-keys.txt
3+
*.env

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Python server to automatically solve Cloudflare Turnstile CAPTCHA with an averag
55
PD: This repository was initially created for personal use. I've adjusted it for sharing, but it might still be slightly disorganized. Feel free to contribute, open issues, and request new features.
66

77
## Screenshots
8+
9+
TODO: Update
810
![Help Menu](images/help_menu.png)
911

1012
![Server Console](images/server_console.png)
@@ -30,6 +32,16 @@ solver
3032
```bash
3133
solver --port 8088 --secret jWRN7DH6 --browser-position --max-attempts 3 --captcha-timeout 30 --page-load-timeout 30 --reload-on-overrun
3234
```
35+
#### Use global browser proxy
36+
```bash
37+
solver --proxy-server http://myproxy.com:3128 --proxy-username user --proxy-password pass
38+
```
39+
##### Load proxy parameters from environment variables (all caps)
40+
```bash
41+
solver --proxy-server MY_PROXY_SERVER --proxy-username MY_PROXY_USERNAME --proxy-password MY_PROXY_PASSWORD
42+
```
43+
44+
TODO: Implement proxy rotation system
3345

3446
### Get token
3547

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66
#dynamic = ["version"]
77
dynamic = ["dependencies"]
88
name = "turnstile_solver"
9-
version = "1.21"
9+
version = "2.0"
1010
description = "Python server to automatically solve Cloudflare Turnstile CAPTCHA with an average solving time of two seconds"
1111
readme = "README.md"
1212
authors = [{ name = "OGM" }]
@@ -19,7 +19,7 @@ classifiers = [
1919
]
2020
# keywords = [ ]
2121

22-
requires-python = ">=3.11"
22+
requires-python = ">=3.10"
2323

2424
[project.urls]
2525
Repository = "https://github.com/odell0111/turnstile_solver"

src/turnstile_solver/main.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import argparse
55
import random
66
import time
7+
import dotenv
78
from pathlib import Path
89
from threading import Thread
910

@@ -28,7 +29,7 @@
2829
__pname__ = "Turnstile Solver"
2930

3031
# mdata = metadata.metadata(__pname__)
31-
__version__ = "1.21" # mdata['Version']
32+
__version__ = "2.0" # mdata['Version']
3233
__homepage__ = "https://github.com/odell0111/turnstile_solver" # mdata['Home-page']
3334
__author__ = "OGM" # mdata['Author']
3435
__summary__ = "Automatically solve Cloudflare Turnstile captcha" # mdata['Summary']
@@ -100,9 +101,13 @@ def positive_integer_exclusive(value):
100101
parser.add_argument("--headless", action="store_true", help=f"Open browser in headless mode. WARNING: This feature has never worked so far, captcha always fail! It's here only in case it works on future version of Playwright.")
101102
parser.add_argument("-bep", "--browser-executable-path", help=f"Chromium-based browser executable path. If not specified, Patchright (Playwright) will attempt to use its bundled version. Ensure you are using a Chromium-based browser installed with the command `patchright install chromium`. Other browsers may be detected by Cloudflare, which could result in the CAPTCHA not being solved.")
102103
parser.add_argument("-bp", "--browser-position", type=int, nargs='*', metavar="x|y", default=c.BROWSER_POSITION, help=f"Browser position x, y. Default: {c.BROWSER_POSITION}. If the browser window is positioned beyond the screen's resolution, it will be inaccessible, behaving similar to headless mode.")
104+
parser.add_argument("-ps", "--proxy-server", help=f"Global browser proxy server in the format SCHEME://IP|ADDRESS:PORT. Ex: http://myproxy.com:3128")
105+
parser.add_argument("-pun", "--proxy-username", help=f"Global browser proxy username: Use all caps to load from environment variables.")
106+
parser.add_argument("-pp", "--proxy-password", help=f"Global browser proxy password: Use all caps to load from environment variables.")
107+
103108
parser.add_argument("-nfl", "--no-file-logs", action="store_true", help=f"Do not log to file '$HOME.turnstile_solver/logs.log'.")
104-
#
105-
# # Solver
109+
110+
# Solver
106111
solver = parser.add_argument_group("Solver")
107112
solver.add_argument("-ma", "--max-attempts", type=positive_integer, metavar="N", default=c.MAX_ATTEMPTS_TO_SOLVE_CAPTCHA, help=f"Max attempts to perform to solve captcha. Default: {c.MAX_ATTEMPTS_TO_SOLVE_CAPTCHA}.")
108113
solver.add_argument("-cto", "--captcha-timeout", type=positive_float, metavar="N.", default=c.CAPTCHA_ATTEMPT_TIMEOUT, help=f"Max time to wait for captcha to solve before reloading page. Default: {c.CAPTCHA_ATTEMPT_TIMEOUT} seconds.")
@@ -211,7 +216,9 @@ async def run_server(
211216
attempt_timeout: int = c.CAPTCHA_ATTEMPT_TIMEOUT,
212217
headless: bool = False,
213218
solver_log_level: int | str = logging.INFO,
214-
219+
proxy_server: str | None = None,
220+
proxy_username: str | None = None,
221+
proxy_password: str | None = None,
215222
):
216223
server = TurnstileSolverServer(
217224
host=host,
@@ -236,6 +243,9 @@ async def run_server(
236243
headless=headless,
237244
console=console,
238245
log_level=solver_log_level,
246+
proxy_server=proxy_server,
247+
proxy_username=proxy_username,
248+
proxy_password=proxy_password,
239249
)
240250
server.solver = solver
241251
await solver.server.create_page_pool()
@@ -259,6 +269,8 @@ async def run_server(
259269

260270
async def main():
261271

272+
dotenv.load_dotenv()
273+
262274
from rich import traceback
263275
traceback.install(
264276
show_locals=True,
@@ -315,6 +327,9 @@ async def main():
315327
attempt_timeout=args.captcha_timeout,
316328
headless=args.headless,
317329
solver_log_level=args.solver_log_level,
330+
proxy_server=args.proxy_server,
331+
proxy_username=args.proxy_username,
332+
proxy_password=args.proxy_password,
318333
)
319334

320335

src/turnstile_solver/solver.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import datetime
22
import logging
3+
import os
4+
import re
35
import time
46
from pathlib import Path
57
from typing import Callable, Awaitable
68
from patchright.async_api import async_playwright, Page, BrowserContext, Browser, Playwright
9+
from turnstile_solver.utils import is_all_caps
710

811
from . import constants as c
912
from .enums import CaptchaApiMessageEvent
@@ -80,6 +83,9 @@ def __init__(self,
8083
headless: bool = False,
8184
console: SolverConsole = SolverConsole(),
8285
log_level: int | str = logging.INFO,
86+
proxy_server: str | None = None,
87+
proxy_username: str | None = None,
88+
proxy_password: str | None = None,
8389
):
8490

8591
logger.setLevel(log_level)
@@ -98,6 +104,13 @@ def __init__(self,
98104
self.max_attempts = max_attempts
99105
self.attempt_timeout = attempt_timeout
100106

107+
if bool(proxy_username) ^ bool(proxy_password):
108+
raise ValueError("Proxy username and password must both be specified")
109+
110+
self.proxy_server = self._load_proxy_param(proxy_server)
111+
self.proxy_username = self._load_proxy_param(proxy_username)
112+
self.proxy_password = self._load_proxy_param(proxy_password)
113+
101114
@property
102115
def _server_down(self) -> bool:
103116
if self.server.down:
@@ -267,10 +280,38 @@ async def _setup_page(
267280

268281
async def _get_browser_context(self) -> tuple[BrowserContext, Playwright]:
269282
playwright = await async_playwright().start()
283+
284+
if self.proxy_server:
285+
logger.debug(f"Using proxy: '{self.proxy_server}'")
286+
if not re.search(r':\d+$', self.proxy_server):
287+
logger.warning("No proxy port specified")
288+
proxy = {
289+
'server': self.proxy_server,
290+
}
291+
if self.proxy_username:
292+
logger.debug("Using proxy credentials")
293+
# logger.debug(f"Proxy Username: {self.proxy_username}")
294+
# logger.debug(f"Proxy Password: {self.proxy_password}")
295+
proxy['username'] = self.proxy_username
296+
proxy['password'] = self.proxy_password
297+
else:
298+
proxy = None
299+
270300
browser: Browser | None = await playwright.chromium.launch(
271301
executable_path=self.browser_executable_path,
272302
# channel=channel,
273303
args=self.browser_args,
274304
headless=self.headless,
305+
proxy=proxy,
275306
)
276307
return await browser.new_context(), playwright
308+
309+
@staticmethod
310+
def _load_proxy_param(param) -> str | None:
311+
if is_all_caps(param):
312+
if p := os.environ.get(param):
313+
logger.debug("Proxy parameter loaded from environment variables")
314+
return p
315+
else:
316+
logger.warning("Proxy parameter intended to be loaded from environment variables was not found")
317+
return param

src/turnstile_solver/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,12 @@ def get_file_handler(
5555
formatter = logging.Formatter('%(asctime)s::%(levelname)s::%(name)s %(message)s, line %(lineno)d')
5656
handler.setFormatter(formatter)
5757
return handler
58+
59+
60+
def is_all_caps(word: str) -> bool:
61+
if not word:
62+
return False
63+
filtered = [c.isupper() for c in word if c.isalpha()]
64+
return filtered and all(filtered)
65+
66+

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import logging
66
import pytest
7+
import dotenv
78

89
from turnstile_solver.solver_console import SolverConsole
910
from turnstile_solver.utils import init_logger, get_file_handler
@@ -30,6 +31,8 @@ def console() -> SolverConsole:
3031
def pytest_configure(config: pytest.Config):
3132
print()
3233

34+
dotenv.load_dotenv()
35+
3336
if not PROJECT_HOME_DIR.exists():
3437
PROJECT_HOME_DIR.mkdir(parents=True)
3538

tests/test_solver.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import logging
22
import asyncio
3+
import os
4+
35
import pytest
46
import requests
57

@@ -37,6 +39,10 @@ def server(console: SolverConsole) -> TurnstileSolverServer:
3739
def solver(server: TurnstileSolverServer) -> TurnstileSolver:
3840
EXECUTABLE_PATH = Path.home() / "AppData/Local/ms-playwright/chromium-1155/chrome-win/chrome.exe"
3941

42+
proxyServer = os.environ.get('PROXY_SERVER')
43+
proxyUsername = os.environ.get('PROXY_USERNAME')
44+
proxyPassword = os.environ.get('PROXY_PASSWORD')
45+
4046
s = TurnstileSolver(
4147
console=server.console,
4248
log_level=logging.DEBUG,
@@ -46,6 +52,9 @@ def solver(server: TurnstileSolverServer) -> TurnstileSolver:
4652
browser_position=None,
4753
browser_executable_path=EXECUTABLE_PATH,
4854
headless=False,
55+
proxy_server=proxyServer,
56+
proxy_username=proxyUsername,
57+
proxy_password=proxyPassword,
4958
)
5059
server.solver = s
5160
return s

0 commit comments

Comments
 (0)