universal-autoreg: initial public release (restored, redacted, hardened)
This commit is contained in:
235
dashboard.py
Normal file
235
dashboard.py
Normal file
@@ -0,0 +1,235 @@
|
||||
#!/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/<provider>.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 = """<!doctype html><html><head><meta charset="utf-8">
|
||||
<title>UNIVERSAL AUTOREG</title><style>
|
||||
body{background:#0d0f12;color:#d7dae0;font:14px/1.5 Consolas,monospace;margin:0;padding:18px}
|
||||
h1{color:#ff6b35;font-size:20px;margin:0 0 4px}
|
||||
.sub{color:#7a8290;margin-bottom:16px}
|
||||
.hbar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:18px}
|
||||
.hcard{background:#151920;border:1px solid #232a35;border-radius:8px;padding:10px 14px;min-width:150px}
|
||||
.hcard b{color:#ff6b35;font-size:18px;display:block}
|
||||
table{border-collapse:collapse;width:100%;margin-bottom:20px}
|
||||
th{color:#7a8290;text-align:left;padding:6px 8px;border-bottom:1px solid #232a35;font-size:12px}
|
||||
td{padding:7px 8px;border-bottom:1px solid #1a2029;vertical-align:top;font-size:12.5px}
|
||||
tr:hover td{background:#131820}
|
||||
.id{color:#ff6b35;font-weight:bold;white-space:nowrap}
|
||||
.tag{background:#1c2530;border-radius:4px;padding:1px 6px;color:#8fb7d9;white-space:nowrap;font-size:11px}
|
||||
.tag.free{background:#14261a;color:#6fcf7f}
|
||||
.tag.paid{background:#2a1c14;color:#d9a05b}
|
||||
.tag.none{background:#1c2530;color:#5a6472}
|
||||
button{background:#ff6b35;border:0;color:#0d0f12;font-weight:bold;padding:4px 10px;border-radius:5px;cursor:pointer;font-family:inherit}
|
||||
button.stop{background:#3a2028;color:#ff8b8b}
|
||||
button:disabled{background:#232a35;color:#5a6472;cursor:default}
|
||||
input[type=number]{width:52px;background:#0d0f12;border:1px solid #232a35;color:#d7dae0;padding:3px;border-radius:4px;font-family:inherit}
|
||||
pre.log{background:#0a0c0f;border:1px solid #232a35;border-radius:8px;padding:12px;height:280px;overflow-y:auto;font-size:12px;white-space:pre-wrap;display:none}
|
||||
.notes{color:#5a6472;font-size:11.5px}
|
||||
a{color:#8fb7d9}
|
||||
.foot{color:#3d4654;margin-top:24px;font-size:12px}
|
||||
</style></head><body>
|
||||
<h1>🪕 UNIVERSAL AUTOREG DASHBOARD</h1>
|
||||
<div class="sub">17+ провайдеров · локальная капча · почтовые пулы · прокси-ротация · банк аккаунтов</div>
|
||||
<div class="hbar" id="health"></div>
|
||||
<div id="prov"></div>
|
||||
<pre class="log" id="logbox"></pre>
|
||||
<div class="foot">👻 <a href="https://t.me/AlStack">иишко</a> · universal-autoreg · секретов нет: config.json (gitignored)</div>
|
||||
<script>
|
||||
let curLog=null,logOff=0,timer=null;
|
||||
function tag(t){const f=/free|none/i.test(t)?'free':/2captcha|paid/i.test(t)?'paid':'';return `<span class="tag ${f}">${t}</span>`}
|
||||
async function render(){
|
||||
const m=await (await fetch('/api/manifest')).json();
|
||||
const b=await (await fetch('/api/bank')).json();
|
||||
const h=await (await fetch('/api/health')).json();
|
||||
document.getElementById('health').innerHTML=`
|
||||
<div class="hcard"><b>${h.sidecar&&h.sidecar.ok?'UP':'DOWN'}</b>captcha sidecar :8877</div>
|
||||
<div class="hcard"><b>${h.proxies.total}</b>прокси в пуле</div>
|
||||
<div class="hcard"><b>${h.emails.pool}</b>t-online ящиков</div>
|
||||
<div class="hcard"><b>${h.twocaptcha&&h.twocaptcha.status==1?'$'+h.twocaptcha.request:'—'}</b>2captcha баланс</div>
|
||||
<div class="hcard"><b>${Object.values(b).reduce((s,v)=>s+v.total,0)}</b>аккаунтов в банке</div>`;
|
||||
let rows='';
|
||||
for(const p of m.providers){
|
||||
const st=(b[p.id]||{total:0,with_token:0});
|
||||
const run=p.running?`<button class="stop" onclick="stop('${p.id}')">STOP ${p.pid}</button>`:
|
||||
`<input type="number" id="n_${p.id}" value="5" min="1" max="500"> <button onclick="run('${p.id}')">RUN</button>`;
|
||||
rows+=`<tr><td class="id">${p.id}${p.running?' ▶':''}</td>
|
||||
<td>${p.target}</td><td>${tag(p.auth)}</td><td>${tag(p.captcha)}</td>
|
||||
<td>${p.reward}</td><td style="text-align:right"><b>${st.total}</b><br><span class="notes">${st.with_token} с токеном</span></td>
|
||||
<td>${run} <button onclick="showLog('${p.id}')">LOG</button><br><span class="notes">${p.notes||''}</span></td></tr>`;
|
||||
}
|
||||
document.getElementById('prov').innerHTML=`<table><tr><th>provider</th><th>таргет</th><th>auth</th><th>капча</th><th>награда</th><th>банк</th><th>запуск</th></tr>${rows}</table>`;
|
||||
}
|
||||
async function run(id){const n=document.getElementById('n_'+id).value;
|
||||
const r=await (await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider:id,count:+n})})).json();
|
||||
if(r.error)alert(r.error);showLog(id);render();}
|
||||
async function stop(id){await fetch('/api/stop',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider:id})});render();}
|
||||
function showLog(id){curLog=id;logOff=0;const lb=document.getElementById('logbox');lb.style.display='block';lb.textContent='';
|
||||
if(timer)clearInterval(timer);timer=setInterval(pullLog,2000);pullLog();}
|
||||
async function pullLog(){if(!curLog)return;
|
||||
const r=await (await fetch(`/api/log?provider=${curLog}&offset=${logOff}`)).json();
|
||||
if(r.text){const lb=document.getElementById('logbox');lb.textContent+=r.text;logOff=r.offset;lb.scrollTop=lb.scrollHeight;}}
|
||||
render();setInterval(render,10000);
|
||||
</script></body></html>"""
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user