universal-autoreg: initial public release (restored, redacted, hardened)
This commit is contained in:
275
providers/conol_captcha.py
Normal file
275
providers/conol_captcha.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""conol_captcha.py — reCAPTCHA v3 solver chain for conol.ai registration.
|
||||
|
||||
Providers, tried in order:
|
||||
1. chrome_cdp — FREE. Local Chrome with a warm profile renders conol.ai and
|
||||
executes window.grecaptcha.execute(siteKey, {action}) over CDP. A real
|
||||
browser + aged profile typically scores >= 0.3, which is all conol's
|
||||
minScore gate requires. No third-party service, no per-solve cost.
|
||||
2. anticaptcha — PAID fallback (existing behaviour), keys from config.json.
|
||||
|
||||
Public API (matches what conol_register.py needs):
|
||||
solve(action) -> Optional[str] # token or None
|
||||
warmup() -> None # optional: pre-launch Chrome + load page
|
||||
|
||||
Chrome path/profile/port come from config.json -> captcha section:
|
||||
{"chrome_path": "...", "cdp_port": 9228, "profile_dir": "...", "providers": ["chrome_cdp","anticaptcha"]}
|
||||
Defaults are sane; env overrides: CONOL_CAPTCHA_PROVIDERS (comma list), CONOL_CDP_PORT.
|
||||
|
||||
Stdlib + playwright only. The browser is launched ONCE and reused across solves
|
||||
(page kept on conol.ai); the token is minted per solve with the right action.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent
|
||||
|
||||
def _cfg() -> dict:
|
||||
p = _ROOT / "config.json"
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
_C = _cfg().get("captcha", {}) or {}
|
||||
CONOL = _cfg().get("conol", {}) or {}
|
||||
|
||||
SITE_KEY = os.environ.get("CONOL_SITE_KEY") or CONOL.get("site_key", "")
|
||||
BASE_URL = os.environ.get("CONOL_BASE_URL") or CONOL.get("base_url", "https://conol.ai")
|
||||
|
||||
DEFAULT_CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
CHROME_PATH = os.environ.get("CONOL_CHROME_PATH") or _C.get("chrome_path") or DEFAULT_CHROME
|
||||
CDP_PORT = int(os.environ.get("CONOL_CDP_PORT") or _C.get("cdp_port") or 9228)
|
||||
PROFILE_DIR = _C.get("profile_dir") or str(_ROOT / ".chrome_cdp_profile")
|
||||
|
||||
_env_providers = os.environ.get("CONOL_CAPTCHA_PROVIDERS")
|
||||
if _env_providers:
|
||||
PROVIDERS = [p.strip() for p in _env_providers.split(",") if p.strip()]
|
||||
else:
|
||||
PROVIDERS = list(_C.get("providers") or ["chrome_cdp", "anticaptcha"])
|
||||
|
||||
ANTICAPTCHA_KEYS = list(_C.get("anticaptcha_keys") or [])
|
||||
|
||||
# ---------------------------------------------------------------- chrome_cdp
|
||||
|
||||
_pw = None # playwright instance
|
||||
_browser = None # CDP browser handle
|
||||
_page = None # tab parked on conol.ai
|
||||
LAST_PROVIDER = None # which solver minted the last successful token
|
||||
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
print("[captcha] %s" % msg, flush=True)
|
||||
|
||||
|
||||
def _cdp_alive() -> bool:
|
||||
try:
|
||||
with urllib.request.urlopen("http://127.0.0.1:%d/json/version" % CDP_PORT, timeout=3) as r:
|
||||
return r.status == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _launch_chrome() -> bool:
|
||||
"""Launch Chrome with remote debugging on CDP_PORT. Reuses a warm profile dir."""
|
||||
if _cdp_alive():
|
||||
return True
|
||||
Path(PROFILE_DIR).mkdir(parents=True, exist_ok=True)
|
||||
cmd = [
|
||||
CHROME_PATH,
|
||||
"--remote-debugging-port=%d" % CDP_PORT,
|
||||
"--user-data-dir=%s" % PROFILE_DIR,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"about:blank",
|
||||
]
|
||||
_cproxy = os.environ.get("CONOL_CHROME_PROXY")
|
||||
if _cproxy:
|
||||
cmd.insert(-1, "--proxy-server=%s" % _cproxy)
|
||||
try:
|
||||
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
creationflags=getattr(subprocess, "DETACHED_PROCESS", 0) |
|
||||
getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0))
|
||||
except FileNotFoundError:
|
||||
_log("chrome not found at %s" % CHROME_PATH)
|
||||
return False
|
||||
for _ in range(30):
|
||||
time.sleep(1)
|
||||
if _cdp_alive():
|
||||
_log("chrome CDP up on :%d" % CDP_PORT)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _connect():
|
||||
"""Connect playwright over CDP and park a page on BASE_URL. Lazy + reused."""
|
||||
global _pw, _browser, _page
|
||||
if _page is not None:
|
||||
try:
|
||||
_ = _page.url # cheap liveness probe
|
||||
return _page
|
||||
except Exception:
|
||||
_page = None
|
||||
if not _launch_chrome():
|
||||
return None
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
_log("playwright not installed — chrome_cdp provider unavailable")
|
||||
return None
|
||||
try:
|
||||
if _pw is None:
|
||||
_pw = sync_playwright().start()
|
||||
if _browser is None:
|
||||
# timeout is mandatory here: connect_over_cdp without it can hang
|
||||
# forever when Chrome's WS endpoint accepts but never completes the
|
||||
# handshake (observed 12-min wedge in an unattended run).
|
||||
_browser = _pw.chromium.connect_over_cdp(
|
||||
"http://127.0.0.1:%d" % CDP_PORT, timeout=30000)
|
||||
ctx = _browser.contexts[0] if _browser.contexts else _browser.new_context()
|
||||
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||
page.set_default_timeout(30000)
|
||||
_log("tab url: %s" % (page.url or "?")[:80])
|
||||
if "conol.ai" not in (page.url or ""):
|
||||
page.goto(BASE_URL, wait_until="domcontentloaded", timeout=45000)
|
||||
# make sure grecaptcha is present (conol loads it lazily on auth pages)
|
||||
page.evaluate("""(siteKey) => new Promise((resolve, reject) => {
|
||||
if (window.grecaptcha) return resolve('already');
|
||||
const old = document.getElementById('eni-recaptcha');
|
||||
if (old) old.remove();
|
||||
const s = document.createElement('script');
|
||||
s.id = 'eni-recaptcha';
|
||||
s.src = 'https://www.recaptcha.net/recaptcha/api.js?render=' + encodeURIComponent(siteKey);
|
||||
s.onload = () => resolve('loaded');
|
||||
s.onerror = () => reject(new Error('recaptcha load failed'));
|
||||
document.head.appendChild(s);
|
||||
})""", SITE_KEY)
|
||||
page.wait_for_function("typeof window.grecaptcha !== 'undefined'", timeout=20000)
|
||||
_log("grecaptcha ready on %s" % (page.url or "?")[:60])
|
||||
_page = page
|
||||
return page
|
||||
except Exception as e:
|
||||
_log("connect failed: %s" % str(e)[:160])
|
||||
_page = None
|
||||
return None
|
||||
|
||||
|
||||
def solve_chrome_cdp(action: str) -> Optional[str]:
|
||||
"""Mint one reCAPTCHA v3 token in the live Chrome tab. Returns token or None."""
|
||||
page = _connect()
|
||||
if page is None:
|
||||
return None
|
||||
try:
|
||||
token = page.evaluate("""([siteKey, action]) => new Promise((resolve, reject) => {
|
||||
window.grecaptcha.ready(() => {
|
||||
window.grecaptcha.execute(siteKey, {action: action}).then(resolve).catch(reject);
|
||||
});
|
||||
})""", [SITE_KEY, action])
|
||||
if isinstance(token, str) and len(token) > 50:
|
||||
return token
|
||||
_log("chrome_cdp returned a short/odd token (%r)" % str(token)[:40])
|
||||
except Exception as e:
|
||||
_log("chrome_cdp solve error: %s" % str(e)[:160])
|
||||
return None
|
||||
|
||||
|
||||
def warmup() -> None:
|
||||
"""Pre-launch Chrome + load conol.ai so the first solve is fast."""
|
||||
_connect()
|
||||
|
||||
|
||||
# -------------------------------------------------------------- anticaptcha
|
||||
|
||||
def solve_anticaptcha(action: str) -> Optional[str]:
|
||||
"""Existing paid path: RecaptchaV3TaskProxyless via api.anti-captcha.com."""
|
||||
import requests
|
||||
for key in ANTICAPTCHA_KEYS:
|
||||
try:
|
||||
resp = requests.post("https://api.anti-captcha.com/createTask", json={
|
||||
"clientKey": key,
|
||||
"task": {
|
||||
"type": "RecaptchaV3TaskProxyless",
|
||||
"websiteURL": BASE_URL,
|
||||
"websiteKey": SITE_KEY,
|
||||
"pageAction": action,
|
||||
"minScore": 0.3,
|
||||
}
|
||||
}, timeout=30)
|
||||
data = resp.json()
|
||||
if data.get("errorId") != 0:
|
||||
_log("createTask fail (%s...): %s" % (key[:8], data.get("errorDescription", "?")))
|
||||
continue
|
||||
task_id = data["taskId"]
|
||||
deadline = time.time() + 90
|
||||
while time.time() < deadline:
|
||||
r = requests.post("https://api.anti-captcha.com/getTaskResult", json={
|
||||
"clientKey": key, "taskId": task_id
|
||||
}, timeout=15).json()
|
||||
if r.get("status") == "ready":
|
||||
token = r["solution"].get("gRecaptchaResponse", "")
|
||||
if token:
|
||||
return token
|
||||
elif r.get("errorId", 0) != 0:
|
||||
_log("getTaskResult error: %s" % r.get("errorDescription", ""))
|
||||
break
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
_log("anticaptcha exception (%s...): %s" % (key[:8], str(e)[:120]))
|
||||
return None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- chain
|
||||
|
||||
_SOLVERS = {"chrome_cdp": solve_chrome_cdp, "anticaptcha": solve_anticaptcha}
|
||||
|
||||
|
||||
def solve(action: str, skip: "set|None" = None) -> Optional[str]:
|
||||
"""Try each configured provider in order; first non-empty token wins.
|
||||
|
||||
`skip` names providers to bypass this round — used to ESCALATE when the
|
||||
server rejects a token (CAPTCHA_VERIFICATION_FAILED): a freshly-created
|
||||
Chrome profile mints a low reCAPTCHA-v3 score, so we skip chrome_cdp and
|
||||
fall through to the paid AntiCaptcha path instead of burning attempts on
|
||||
tokens the server keeps rejecting. The provider that produced the last
|
||||
token is exposed via LAST_PROVIDER so the caller knows whom to skip.
|
||||
"""
|
||||
skip = skip or set()
|
||||
for name in PROVIDERS:
|
||||
if name in skip:
|
||||
continue
|
||||
fn = _SOLVERS.get(name)
|
||||
if fn is None:
|
||||
_log("unknown captcha provider %r — skipped" % name)
|
||||
continue
|
||||
tok = fn(action)
|
||||
if tok:
|
||||
global LAST_PROVIDER
|
||||
LAST_PROVIDER = name
|
||||
return tok
|
||||
_log("provider %s returned nothing for action=%s" % (name, action))
|
||||
# every non-skipped provider failed — retry ignoring the skip list once
|
||||
if skip:
|
||||
_log("all non-skipped providers exhausted; retrying full chain")
|
||||
return solve(action, skip=None)
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# self-test: python conol_captcha.py [action]
|
||||
act = sys.argv[1] if len(sys.argv) > 1 else "sign_up"
|
||||
t0 = time.time()
|
||||
tok = solve(act)
|
||||
if tok:
|
||||
print("OK action=%s len=%d time=%.1fs providers=%s token=%s...%s"
|
||||
% (act, len(tok), time.time() - t0, PROVIDERS, tok[:24], tok[-12:]))
|
||||
else:
|
||||
print("FAIL action=%s time=%.1fs providers=%s" % (act, time.time() - t0, PROVIDERS))
|
||||
Reference in New Issue
Block a user