105 lines
4.7 KiB
Python
105 lines
4.7 KiB
Python
"""captcha.py — клиент captcha-инфраструктуры.
|
||
|
||
Два бэкенда:
|
||
1. Локальный sidecar (waguriagentic/captcha-solver, http://127.0.0.1:8877) — БЕСПЛАТНО:
|
||
turnstile / recaptcha v2-v3 / hcaptcha / arkose(funcaptcha) / aliyun / geetest и др. (11 типов).
|
||
Запуск sidecar: см. docs/SETUP.md. Для новых версий нужен SOLVER_ALLOW_UNAUTHENTICATED=1.
|
||
2. 2captcha fallback (платный) — когда sidecar не справляется (нативный Turnstile под
|
||
siteverify-проверкой, DataDome, press&hold PerimeterX).
|
||
|
||
Все ключи — из config.json (НЕ хардкодить).
|
||
"""
|
||
import json, os, time, urllib.request, urllib.parse
|
||
|
||
CFG_FILE = os.environ.get("UA_CONFIG", os.path.join(os.path.dirname(__file__), "..", "config.json"))
|
||
SIDECAR = os.environ.get("UA_SIDECAR", "http://127.0.0.1:8877")
|
||
|
||
|
||
def _cfg():
|
||
if os.path.exists(CFG_FILE):
|
||
return json.load(open(CFG_FILE, encoding="utf-8"))
|
||
return {}
|
||
|
||
|
||
def _post_json(url, payload, timeout=300):
|
||
req = urllib.request.Request(url, data=json.dumps(payload).encode(),
|
||
headers={"Content-Type": "application/json"})
|
||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||
return json.loads(r.read().decode())
|
||
|
||
|
||
def sidecar_health():
|
||
try:
|
||
with urllib.request.urlopen(SIDECAR + "/health", timeout=5) as r:
|
||
return json.loads(r.read().decode())
|
||
except Exception as e:
|
||
return {"ok": False, "error": str(e)}
|
||
|
||
|
||
def solve_sidecar(captcha_type: str, sitekey: str, pageurl: str, **kw):
|
||
"""Решить капчу локальным sidecar'ом (бесплатно).
|
||
|
||
captcha_type: turnstile | recaptcha_v2 | recaptcha_v3 | hcaptcha | funcaptcha | ...
|
||
Для recaptcha v3 обязательны: page_action (action сайта, напр. 'signup').
|
||
Для turnstile рекомендуется real_page режим у sidecar'а (REAL_PAGE=1).
|
||
"""
|
||
payload = {"type": captcha_type, "sitekey": sitekey, "pageurl": pageurl}
|
||
payload.update(kw)
|
||
return _post_json(SIDECAR + "/solve", payload)
|
||
|
||
|
||
def solve_2captcha(captcha_type: str, sitekey: str, pageurl: str, **kw):
|
||
"""2captcha fallback. ВАЖНО: submit СТРОГО на in.php (res.php = ERROR_EMPTY_METHOD),
|
||
баланс СТРОГО POST api.2captcha.com/res.php (GET = ложный ERROR_WRONG_USER_KEY)."""
|
||
key = _cfg().get("twocaptcha_key")
|
||
if not key:
|
||
raise RuntimeError("twocaptcha_key not set in config.json")
|
||
method = {"turnstile": "turnstile", "recaptcha_v2": "userrecaptcha",
|
||
"recaptcha_v3": "recaptcha", "hcaptcha": "hcaptcha",
|
||
"funcaptcha": "funcaptcha"}.get(captcha_type, captcha_type)
|
||
params = {"key": key, "method": method, "googlekey" if method == "userrecaptcha" else "sitekey": sitekey,
|
||
"pageurl": pageurl, "json": 1}
|
||
params.update(kw)
|
||
data = urllib.parse.urlencode(params).encode()
|
||
with urllib.request.urlopen("https://api.2captcha.com/in.php", data=data, timeout=30) as r:
|
||
resp = json.loads(r.read().decode())
|
||
if resp.get("status") != 1:
|
||
raise RuntimeError(f"2captcha submit failed: {resp}")
|
||
cid = resp["request"]
|
||
for _ in range(60):
|
||
time.sleep(5)
|
||
q = urllib.parse.urlencode({"key": key, "action": "get", "id": cid, "json": 1}).encode()
|
||
with urllib.request.urlopen("https://api.2captcha.com/res.php", data=q, timeout=30) as r:
|
||
resp = json.loads(r.read().decode())
|
||
if resp.get("status") == 1:
|
||
return {"token": resp["request"]}
|
||
if resp.get("request") != "CAPCHA_NOT_READY":
|
||
raise RuntimeError(f"2captcha: {resp}")
|
||
raise TimeoutError("2captcha timeout 300s")
|
||
|
||
|
||
def solve(captcha_type: str, sitekey: str, pageurl: str, prefer="sidecar", **kw):
|
||
"""Умный решатель: sidecar → при ошибке 2captcha (если включён fallback)."""
|
||
if prefer == "sidecar":
|
||
try:
|
||
return solve_sidecar(captcha_type, sitekey, pageurl, **kw)
|
||
except Exception:
|
||
if _cfg().get("twocaptcha_fallback", True):
|
||
return solve_2captcha(captcha_type, sitekey, pageurl, **kw)
|
||
raise
|
||
return solve_2captcha(captcha_type, sitekey, pageurl, **kw)
|
||
|
||
|
||
def balance_2captcha():
|
||
key = _cfg().get("twocaptcha_key")
|
||
if not key:
|
||
return None
|
||
data = urllib.parse.urlencode({"key": key, "action": "getbalance"}).encode()
|
||
with urllib.request.urlopen("https://api.2captcha.com/res.php", data=data, timeout=15) as r:
|
||
return json.loads(r.read().decode())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print("sidecar health:", sidecar_health())
|
||
print("2captcha balance:", balance_2captcha())
|