#!/usr/bin/env python3 """UNIVERSAL AUTOREG DASHBOARD — единый пульт для 17+ авторег-провайдеров. Stdlib-only (http.server), порт 8900. Запуск: python dashboard.py UI: http://127.0.0.1:8900 Возможности: - Список провайдеров из providers.json (таргет, auth, капча, почта, прокси, награда) - Запуск волн: POST /api/run {provider, count} → subprocess, лог в logs/.log - Live-лог: GET /api/log?provider=X&offset=N - Банк аккаунтов: GET /api/bank (статистика по провайдерам из data/bank.jsonl) - Здоровье инфры: GET /api/health (captcha sidecar :8877, прокси-пул, почтовый пул, 2captcha баланс) - Остановка волн: POST /api/stop {provider} """ import json, os, subprocess, sys, threading, time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse, parse_qs HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) PORT = int(os.environ.get("UA_PORT", "8900")) PYTHON = os.environ.get("UA_PYTHON", sys.executable) LOGS = os.path.join(HERE, "logs") os.makedirs(LOGS, exist_ok=True) os.makedirs(os.path.join(HERE, "data"), exist_ok=True) from lib import bank # noqa: E402 RUNNING = {} # provider -> {"proc": Popen, "started": ts, "count": N} RUN_LOCK = threading.Lock() def load_manifest(): with open(os.path.join(HERE, "providers.json"), encoding="utf-8") as f: return json.load(f) def valid_provider(name): """SECURITY: провайдер — только из манифеста, без path traversal.""" import re as _re if not isinstance(name, str) or not _re.fullmatch(r"[A-Za-z0-9_-]{1,64}", name): return False return name in {p["id"] for p in load_manifest().get("providers", [])} def infra_health(): from lib import captcha, proxies h = {"sidecar": None, "proxies": {"total": 0}, "emails": {"pool": 0}, "twocaptcha": None} try: h["sidecar"] = captcha.sidecar_health() except Exception as e: h["sidecar"] = {"ok": False, "error": str(e)} try: h["proxies"]["total"] = len(proxies.load_pool()) except Exception as e: h["proxies"]["error"] = str(e) mails = os.path.join(HERE, "data", "working_mails.txt") if os.path.exists(mails): h["emails"]["pool"] = sum(1 for _ in open(mails, encoding="utf-8")) try: h["twocaptcha"] = captcha.balance_2captcha() except Exception: pass return h def run_wave(provider, count): if not valid_provider(provider): return {"error": f"unknown provider: {provider!r}"} with RUN_LOCK: if provider in RUNNING and RUNNING[provider]["proc"].poll() is None: return {"error": "already running", "pid": RUNNING[provider]["proc"].pid} script = os.path.join(HERE, "providers", provider + ".py") if not os.path.exists(script): return {"error": f"no such provider script: {script}"} logf = open(os.path.join(LOGS, provider + ".log"), "a", encoding="utf-8") logf.write(f"\n===== wave {count} started {time.strftime('%F %T')} =====\n") logf.flush() env = dict(os.environ) env.setdefault("PYTHONUNBUFFERED", "1") proc = subprocess.Popen([PYTHON, script, str(count)], cwd=HERE, stdout=logf, stderr=subprocess.STDOUT, env=env) logf.close() # HANDLEFIX: child унаследовал dup — родительскую копию закрыть RUNNING[provider] = {"proc": proc, "started": time.time(), "count": count} return {"ok": True, "pid": proc.pid} HTML = """ UNIVERSAL AUTOREG

🪕 UNIVERSAL AUTOREG DASHBOARD

17+ провайдеров · локальная капча · почтовые пулы · прокси-ротация · банк аккаунтов

👻 иишко · universal-autoreg · секретов нет: config.json (gitignored)
""" class H(BaseHTTPRequestHandler): def log_message(self, *a): pass def _send(self, code, body, ctype="application/json"): if isinstance(body, (dict, list)): body = json.dumps(body, ensure_ascii=False) b = body.encode("utf-8") self.send_response(code) self.send_header("Content-Type", ctype + "; charset=utf-8") self.send_header("Content-Length", str(len(b))) self.end_headers() self.wfile.write(b) def _body(self): n = int(self.headers.get("Content-Length", 0)) return json.loads(self.rfile.read(n) or b"{}") def do_GET(self): u = urlparse(self.path) if u.path == "/": return self._send(200, HTML, "text/html") if u.path == "/api/manifest": m = load_manifest() with RUN_LOCK: for p in m["providers"]: r = RUNNING.get(p["id"]) p["running"] = bool(r and r["proc"].poll() is None) p["pid"] = r["proc"].pid if p["running"] else None return self._send(200, m) if u.path == "/api/bank": return self._send(200, bank.stats()) if u.path == "/api/health": return self._send(200, infra_health()) if u.path == "/api/log": q = parse_qs(u.query) prov = q.get("provider", [""])[0] # SECURITY: только валидное имя провайдера — иначе path traversal (../../../etc) import re as _re if not _re.fullmatch(r"[A-Za-z0-9_-]{1,64}", prov or ""): return self._send(400, {"error": "bad provider"}) off = int(q.get("offset", ["0"])[0]) path = os.path.join(LOGS, prov + ".log") if not os.path.exists(path): return self._send(200, {"text": "", "offset": off}) with open(path, encoding="utf-8", errors="replace") as f: f.seek(off) text = f.read(64 * 1024) newoff = f.tell() return self._send(200, {"text": text, "offset": newoff}) return self._send(404, {"error": "not found"}) def do_POST(self): u = urlparse(self.path) if u.path == "/api/run": b = self._body() return self._send(200, run_wave(b.get("provider", ""), int(b.get("count", 1)))) if u.path == "/api/stop": b = self._body() with RUN_LOCK: r = RUNNING.get(b.get("provider", "")) if r and r["proc"].poll() is None: pid = r["proc"].pid if os.name == "nt": # TREEFIX: terminate() убивает только python-обёртку — # дети (camoufox/chrome) выживают сиротами. Убивать всю process tree. subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], capture_output=True) else: r["proc"].terminate() return self._send(200, {"ok": True, "terminated": pid}) return self._send(200, {"ok": False}) return self._send(404, {"error": "not found"}) if __name__ == "__main__": print(f"UNIVERSAL AUTOREG DASHBOARD: http://127.0.0.1:{PORT}") ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()