You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

204 lines
7.4 KiB

#!/usr/bin/env python3
"""
vk-turn-proxy captcha solver — local HTTP server that uses Playwright + headless
Chromium to solve VK's not_robot_captcha and return a success_token.
Run:
pip install playwright requests
playwright install chromium
python3 captcha_solver.py [--listen 127.0.0.1:8766]
HTTP API:
POST /solve
Body (JSON): {"redirect_uri": "https://id.vk.ru/not_robot_captcha?..."}
Response (JSON): {"success_token": "...", "elapsed": 8.3}
or {"error": "...", "elapsed": 8.3}
The Go vk-turn-proxy client can be configured (via -captcha-solver flag) to
call this server instead of trying to solve captcha inline. This bypasses
VK's bot detection because the captchaNotRobot.check request comes from a
real Chromium browser with a real FingerprintJS visitor_id.
Note: Chromium must be installed via `playwright install chromium`.
"""
import argparse
import json
import re
import random
import time
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from playwright.sync_api import sync_playwright
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
# Reuse a single browser instance across requests (Chromium startup is ~500ms)
_browser_lock = threading.Lock()
_browser = None
_playwright_ctx = None
def get_browser():
global _browser, _playwright_ctx
with _browser_lock:
if _browser is None:
_playwright_ctx = sync_playwright().start()
_browser = _playwright_ctx.chromium.launch(
headless=True,
args=["--no-sandbox", "--disable-blink-features=AutomationControlled"],
)
return _browser
def solve_captcha(redirect_uri: str, timeout: float = 30.0) -> dict:
"""Load the captcha page in Chromium, click the checkbox, return success_token."""
start = time.time()
success_token = {"value": None, "error": None}
browser = get_browser()
context = browser.new_context(
user_agent=UA,
viewport={"width": 1920, "height": 1080},
locale="en-US",
extra_http_headers={
"sec-ch-ua": '"Chromium";v="146", "Not A(Brand";v="24", "Google Chrome";v="146"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
},
)
context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
window.chrome = {runtime: {}};
""")
page = context.new_page()
def on_response(response):
if "captchaNotRobot.check" in response.url:
try:
data = response.json()
if data.get("response", {}).get("success_token"):
success_token["value"] = data["response"]["success_token"]
except Exception as e:
success_token["error"] = f"response parse: {e}"
page.on("response", on_response)
try:
page.goto(redirect_uri, wait_until="networkidle", timeout=20000)
checkbox = page.wait_for_selector("#not-robot-captcha-checkbox", timeout=10000)
box = checkbox.bounding_box()
target_x = box["x"] + box["width"] / 2
target_y = box["y"] + box["height"] / 2
# Realistic mouse path (cubic bezier)
start_x = random.randint(100, 400)
start_y = random.randint(100, 400)
steps = 40
for i in range(steps):
t = i / (steps - 1)
cp1x = start_x + random.uniform(50, 200)
cp1y = start_y + random.uniform(50, 200)
cp2x = target_x - random.uniform(20, 100)
cp2y = target_y - random.uniform(20, 100)
x = (1-t)**3 * start_x + 3*(1-t)**2*t * cp1x + 3*(1-t)*t**2 * cp2x + t**3 * target_x
y = (1-t)**3 * start_y + 3*(1-t)**2*t * cp1y + 3*(1-t)*t**2 * cp2y + t**3 * target_y
page.mouse.move(x, y)
page.wait_for_timeout(20)
page.wait_for_timeout(300)
page.mouse.click(target_x, target_y)
# Wait for success_token
deadline = time.time() + timeout
while time.time() < deadline and not success_token["value"] and not success_token["error"]:
page.wait_for_timeout(100)
except Exception as e:
success_token["error"] = str(e)
finally:
context.close()
elapsed = time.time() - start
if success_token["value"]:
return {"success_token": success_token["value"], "elapsed": round(elapsed, 1)}
return {"error": success_token["error"] or "no success_token received", "elapsed": round(elapsed, 1)}
class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
# Log to stdout with timestamp
print(f"[{time.strftime('%H:%M:%S')}] {args[0]}")
def do_GET(self):
if self.path == "/health":
self._json(200, {"ok": True, "engine": "playwright"})
else:
self._json(404, {"error": "not found"})
def do_POST(self):
if self.path != "/solve":
self._json(404, {"error": "not found"})
return
try:
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode("utf-8")
req = json.loads(body) if body else {}
except Exception as e:
self._json(400, {"error": f"bad request: {e}"})
return
redirect_uri = req.get("redirect_uri", "")
if not redirect_uri:
self._json(400, {"error": "missing redirect_uri"})
return
print(f"[/solve] redirect_uri={redirect_uri[:80]}...")
result = solve_captcha(redirect_uri)
print(f"[/solve] result: token={'OK' if result.get('success_token') else 'FAIL'} elapsed={result.get('elapsed')}s")
self._json(200, result)
def _json(self, code, obj):
body = json.dumps(obj).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--listen", default="127.0.0.1:8766", help="listen address")
args = ap.parse_args()
# Warm up the browser instance
print("[init] warming up Chromium...")
get_browser()
print("[init] ready")
host, port = args.listen.split(":")
# IMPORTANT: use plain HTTPServer (single-threaded), NOT ThreadingHTTPServer.
# Playwright's sync API uses greenlets that are pinned to a single thread —
# calling browser.new_context() from a worker thread raises
# "greenlet.error: Cannot switch to a different thread". Serializing all
# requests on the main thread is fine because captcha solving is rare
# (once per ~10 minutes per stream) and takes ~8s.
from http.server import HTTPServer
srv = HTTPServer((host, int(port)), Handler)
srv.daemon_threads = False
print(f"[listen] http://{args.listen} (single-threaded)")
print("[listen] POST /solve with {\"redirect_uri\": \"...\"} to solve captcha")
try:
srv.serve_forever()
except KeyboardInterrupt:
print("\n[shutdown]")
if _browser:
_browser.close()
if _playwright_ctx:
_playwright_ctx.stop()
if __name__ == "__main__":
main()