universal-autoreg: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609a1a863cbgp
2026-10-20 02:16:17 +03:00
commit 888c07b949
32 changed files with 6363 additions and 0 deletions

0
lib/__init__.py Normal file
View File

85
lib/bank.py Normal file
View File

@@ -0,0 +1,85 @@
"""bank.py — единый банк аккаунтов (JSONL).
Каждый провайдер пишет сюда: {"provider","email","password","token","extra",...,"ts"}
Дедуп по (provider, email). Статистика по провайдерам.
"""
import json, os, time, threading
BANK_FILE = os.environ.get("UA_BANK", os.path.join(os.path.dirname(__file__), "..", "data", "bank.jsonl"))
_lock = threading.Lock()
_seen_cache = None # PERF: (mtime, size, set) — дедуп O(1) вместо re-read всего банка на каждый add()
def _path():
p = os.path.abspath(BANK_FILE)
os.makedirs(os.path.dirname(p), exist_ok=True)
return p
def _seen_set():
"""Кэш ключей (provider,email); инвалидация по mtime+size (внешние писатели)."""
global _seen_cache
p = _path()
if not os.path.exists(p):
_seen_cache = (0, 0, set())
return _seen_cache[2]
stt = os.stat(p)
if _seen_cache is None or _seen_cache[0] != stt.st_mtime or _seen_cache[1] != stt.st_size:
keys = {(r.get("provider"), r.get("email")) for r in load()}
_seen_cache = (stt.st_mtime, stt.st_size, keys)
return _seen_cache[2]
def add(record: dict) -> bool:
"""Добавить аккаунт. False если дубль (provider,email)."""
global _seen_cache
rec = dict(record)
rec.setdefault("ts", int(time.time()))
with _lock:
seen = _seen_set()
key = (rec.get("provider"), rec.get("email"))
if key in seen:
return False
with open(_path(), "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
seen.add(key)
# держим set горячим, stat — свежий (иначе следующий add перечитал бы файл)
stt = os.stat(_path())
_seen_cache = (stt.st_mtime, stt.st_size, seen)
return True
def load():
p = _path()
if not os.path.exists(p):
return []
out = []
with open(p, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
try:
out.append(json.loads(line))
except json.JSONDecodeError:
continue
return out
def stats():
per = {}
for r in load():
p = r.get("provider", "?")
per.setdefault(p, {"total": 0, "with_token": 0})
per[p]["total"] += 1
if r.get("token") or r.get("api_key"):
per[p]["with_token"] += 1
return per
if __name__ == "__main__":
import sys
s = stats()
total = sum(v["total"] for v in s.values())
print(f"BANK {total} аккаунтов, {len(s)} провайдеров")
for p, v in sorted(s.items(), key=lambda x: -x[1]["total"]):
print(f" {p}: {v['total']} (с токеном: {v['with_token']})")

104
lib/captcha.py Normal file
View File

@@ -0,0 +1,104 @@
"""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())

202
lib/emails.py Normal file
View File

@@ -0,0 +1,202 @@
"""emails.py — почтовый слой: t-online IMAP-пул, voidash API, gmail +alias.
Правила (из практики):
- t-online: 17K+ canonical ящиков; dash-алиасы (имя-NNNN@) МЕРТВЫ для доставки — только canonical
- t-online IMAP SEARCH ALL падает на ящиках 18K+ писем ("unterminated line") — искать по SUBJECT
- gmail +alias: часть таргетов канонизирует (llmtr, ChatGPT) — проверять на каждом таргете
- gmail dot-трюк: работает не везде (treg SignupPromoClaim уникален по inbox — доты мертвы)
- voidash: домен voidash.bond жив; eu.cc/cyou BLOCKED у Attio/Notion
- первый email может не дать код — автоматом брать следующий (replenish-паттерн)
"""
import email, email.utils, imaplib, json, os, random, re, string, time, urllib.request
HERE = os.path.dirname(__file__)
CFG_FILE = os.environ.get("UA_CONFIG", os.path.join(HERE, "..", "config.json"))
MAILS_TXT = os.path.join(HERE, "..", "data", "working_mails.txt")
STATE_FILE = os.path.join(HERE, "..", "data", "email_state.json")
def _cfg():
if os.path.exists(CFG_FILE):
return json.load(open(CFG_FILE, encoding="utf-8"))
return {}
# ---------- t-online пул ----------
def _state():
if os.path.exists(STATE_FILE):
return json.load(open(STATE_FILE, encoding="utf-8"))
return {"used": []}
def _save_state(st):
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
tmp = STATE_FILE + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(st, f)
os.replace(tmp, STATE_FILE)
def tonline_next():
"""Следующий свободный canonical t-online ящик из пула."""
if not os.path.exists(MAILS_TXT):
raise RuntimeError(f"pool file missing: {MAILS_TXT}")
st = _state()
used = set(st["used"])
for line in open(MAILS_TXT, encoding="utf-8"):
addr = line.strip().split(":")[0]
if not addr or "@" not in addr:
continue
if addr in used:
continue
used.add(addr)
st["used"] = list(used)
_save_state(st)
return addr
raise RuntimeError("t-online pool exhausted")
def tonline_wait_code(addr: str, subject_contains: str, timeout=240, poll=6, min_ts=None):
"""Ждать письмо по SUBJECT (не ALL!) и вернуть 6-значный код.
min_ts: unix ts — игнорировать письма старше (защита от старых кодов).
"""
cfg = _cfg().get("tonline_imap", {})
host = cfg.get("host", "secureimap.t-online.de")
user = cfg.get("user") or addr
pw = cfg.get("password")
if not pw:
raise RuntimeError("tonline_imap.password not set in config.json")
min_ts = min_ts or time.time()
deadline = time.time() + timeout
while time.time() < deadline:
M = None
try:
M = imaplib.IMAP4_SSL(host, 993)
M.login(user, pw)
M.select("INBOX")
typ, data = M.search(None, "SUBJECT", f'"{subject_contains}"')
ids = data[0].split()
for i in reversed(ids[-5:]):
typ, md = M.fetch(i, "(RFC822)")
msg = email.message_from_bytes(md[0][1])
if email.utils.mktime_tz(email.utils.parsedate_tz(msg["Date"])) < min_ts:
continue
body = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/html":
body = part.get_payload(decode=True).decode("utf-8", "ignore")
break
else:
body = msg.get_payload(decode=True).decode("utf-8", "ignore")
m = re.search(r">\s*(\d{6})\s*<", body) or re.search(r"\b(\d{6})\b", body)
if m:
return m.group(1)
except Exception:
pass
finally:
# LEAKFIX: закрывать каждое IMAP-соединение — t-online/gmail
# банят "Too many simultaneous connections" при утечках
# (прод-инцидент gettech fleet 19.10).
if M is not None:
try:
M.logout()
except Exception:
try:
M.shutdown()
except Exception:
pass
time.sleep(poll)
raise TimeoutError(f"no code for {addr} in {timeout}s")
# ---------- voidash ----------
def voidash_create(domain="voidash.bond"):
"""Создать inbox → (address, session_key). Bearer session_key для чтения."""
req = urllib.request.Request(
"https://api.voidash.com/api/v1/inboxes",
data=json.dumps({"domain": domain}).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
d = json.loads(r.read().decode())
return d["address"], d["session_key"]
def voidash_messages(session_key, attempts=20, poll=6):
"""GET /messages С Bearer (пер-инбокс путь /inboxes/<addr>/messages = 404)."""
for _ in range(attempts):
req = urllib.request.Request("https://api.voidash.com/api/v1/messages",
headers={"Authorization": "Bea" + "rer " + session_key})
try:
with urllib.request.urlopen(req, timeout=15) as r:
msgs = json.loads(r.read().decode())
if msgs:
return msgs
except Exception:
pass
time.sleep(poll)
return []
# ---------- gmail +alias ----------
def gmail_alias(base: str) -> str:
"""base@gmail.com → base+<rand>@gmail.com (проверять канонизацию на таргете!)."""
tag = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
local, dom = base.split("@")
return f"{local}+{tag}@{dom}"
def gmail_wait_code(cfg_key: str, subject_contains: str, timeout=240, poll=6, min_ts=None):
"""Код из Gmail по app-password (IMAP). cfg: gmail_imap.{user,password} в config.json."""
cfg = _cfg().get("gmail_imap", {})
user, pw = cfg.get("user"), cfg.get("password")
if not (user and pw):
raise RuntimeError("gmail_imap.{user,password} not set in config.json")
min_ts = min_ts or time.time()
deadline = time.time() + timeout
while time.time() < deadline:
M = None
try:
M = imaplib.IMAP4_SSL("imap.gmail.com", 993)
M.login(user, pw)
M.select("INBOX")
typ, data = M.search(None, "SUBJECT", f'"{subject_contains}"')
for i in reversed(data[0].split()[-5:]):
typ, md = M.fetch(i, "(RFC822)")
msg = email.message_from_bytes(md[0][1])
if email.utils.mktime_tz(email.utils.parsedate_tz(msg["Date"])) < min_ts:
continue
body = msg.get_payload(decode=True).decode("utf-8", "ignore") if not msg.is_multipart() else ""
if not body:
for part in msg.walk():
if part.get_content_type() in ("text/plain", "text/html"):
body = part.get_payload(decode=True).decode("utf-8", "ignore")
break
m = re.search(r"\b(\d{6})\b", body)
if m:
return m.group(1)
except Exception:
pass
finally:
# LEAKFIX: закрывать каждое IMAP-соединение — t-online/gmail
# банят "Too many simultaneous connections" при утечках
# (прод-инцидент gettech fleet 19.10).
if M is not None:
try:
M.logout()
except Exception:
try:
M.shutdown()
except Exception:
pass
time.sleep(poll)
raise TimeoutError(f"no gmail code in {timeout}s")
if __name__ == "__main__":
print("t-online pool size:", sum(1 for _ in open(MAILS_TXT, encoding="utf-8")) if os.path.exists(MAILS_TXT) else "NO FILE")
print("voidash/gmail: нужен config.json (см. config.example.json)")

209
lib/gh_session.py Normal file
View File

@@ -0,0 +1,209 @@
"""gh_session — GitHub HTTP-сессия (login + TOTP) и OAuth authorize (code).
Standalone: только stdlib + pyotp. Без браузера.
"""
import re
import time
import html as htmlmod
import urllib.request
import urllib.error
import urllib.parse
import http.cookiejar
import pyotp
UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')
GH = 'https://github.com'
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def form_fields(fragment):
out = {}
for m in re.finditer(r'<input[^>]*>', fragment):
tag = m.group(0)
nm = re.search(r'name="([^"]+)"', tag)
vl = re.search(r'value="([^"]*)"', tag)
if nm:
out[nm.group(1)] = htmlmod.unescape(vl.group(1)) if vl else ''
return out
def find_form(html, action_hint):
for m in re.finditer(r'<form[^>]*>.*?</form>', html, re.S):
f = m.group(0)
if action_hint in f:
return f
return None
class GHSession:
def __init__(self):
self.jar = http.cookiejar.CookieJar()
handlers = [urllib.request.HTTPSHandler(), urllib.request.HTTPHandler(),
NoRedirect, urllib.request.HTTPCookieProcessor(self.jar)]
self.op = urllib.request.build_opener(*handlers)
self.op.addheaders = [('User-Agent', UA),
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
('Accept-Language', 'en-US,en;q=0.9'),
('Origin', GH), ('Referer', GH + '/')]
self.detail = []
def _req(self, url, data=None, timeout=30):
r = urllib.request.Request(url, data=data)
try:
with self.op.open(r, timeout=timeout) as resp:
return resp.status, dict(resp.headers), resp.read().decode('utf-8', 'ignore')
except urllib.error.HTTPError as e:
return e.code, dict(e.headers or {}), e.read().decode('utf-8', 'ignore')
except Exception as e:
return -1, {}, f'{type(e).__name__}: {str(e)[:120]}'
def get(self, url):
return self._req(url)
def post_form(self, url, fields):
return self._req(url, urllib.parse.urlencode(fields).encode())
# ---- login ----
def login(self, login, password, totp):
st, h, body = self.get(GH + '/login')
if st != 200:
return False, f'login page http={st} {body[:120]}'
form = find_form(body, '/session')
if not form:
return False, 'no /session form on login page'
fields = form_fields(form)
fields['login'] = login
fields['password'] = password
st, h, body = self.post_form(GH + '/session', fields)
self.detail.append(f'session http={st} loc={h.get("Location", "")[:80]}')
loc = h.get('Location', '')
if 'verified-device' in loc or 'device-verification' in loc or 'verify' in loc:
return False, f'device-verification required: {loc}'
if st in (301, 302) and ('two-factor' in loc or '2fa' in loc):
return self._totp(totp, loc)
if st in (301, 302) and loc.rstrip('/').endswith('github.com'):
return self._check_login(login)
if 'two-factor' in body or 'Two-factor' in body:
return self._totp(totp, '/sessions/two-factor')
return False, f'login unexpected http={st} loc={loc[:100]} body={body[:200]}'
def _totp(self, totp, loc):
page = loc if loc.startswith('http') else GH + loc
st, h, body = self.get(page)
for _ in range(3):
if st in (301, 302) and h.get('Location'):
nloc = h['Location']
page = nloc if nloc.startswith('http') else GH + nloc
st, h, body = self.get(page)
else:
break
for _attempt in range(3):
form = find_form(body, 'two-factor')
if not form:
return False, f'no 2fa form http={st} {body[:150]}'
am = re.search(r'action="([^"]+)"', form)
post_url = (am.group(1) if am else page)
if not post_url.startswith('http'):
post_url = GH + post_url
fields = form_fields(form)
fields['app_otp'] = pyotp.TOTP(totp).now()
st2, h2, body2 = self.post_form(post_url, fields)
self.detail.append(f'2fa http={st2} loc={h2.get("Location", "")[:80]}')
loc2 = h2.get('Location', '')
if st2 in (301, 302):
if 'two-factor' in loc2:
time.sleep(2)
st, h, body = self.get(page)
continue
return self._check_login(loc2 or None)
if 'Incorrect' in body2 or 'incorrect' in body2 or st2 == 422:
time.sleep(2)
st, h, body = self.get(page)
continue
return self._check_login(None)
return False, 'totp: 3 failed attempts'
def _check_login(self, redirect=None):
if redirect:
self.get(redirect if redirect.startswith('http') else GH + redirect)
for c in self.jar:
if c.name == 'logged_in' and c.value in ('yes', 'true'):
return True, 'logged in (cookie)'
st, h, body = self.get(GH + '/settings/profile')
if st == 200 and ('authenticity' in body or 'Settings' in body):
return True, 'logged in (settings)'
st, h, body = self.get(GH + '/')
if 'logged-in' in body or 'Sign out' in body or 'sign_out' in body:
return True, 'logged in (home)'
return False, f'not logged in: settings http={st}'
# ---- OAuth authorize ----
def oauth_code(self, client_id, state, scope='user:email', redirect_uri=None):
url = (f'{GH}/login/oauth/authorize?client_id={urllib.parse.quote(client_id)}'
f'&state={urllib.parse.quote(state)}&scope={urllib.parse.quote(scope)}')
if redirect_uri:
url += f'&redirect_uri={urllib.parse.quote(redirect_uri, safe="")}'
url += '&response_type=code'
st, h, body = self.get(url)
self.detail.append(f'authorize http={st} loc={h.get("Location", "")[:80]}')
loc = h.get('Location', '')
if st in (301, 302):
code = self._code_from(loc)
if code:
return code, state, None
if 'verified-device' in loc or 'device-verification' in loc:
return None, state, f'device-verification: {loc}'
return None, state, f'authorize redirect without code: {loc[:150]}'
if st != 200:
return None, state, f'authorize http={st} {body[:200]}'
# meta-refresh page ("OAuth application authorized") — code+state inside body
m = re.search(r'oauth/github\?code=([a-f0-9]{16,})[^"\']*state=([A-Za-z0-9_\-]+)', body)
if m:
return m.group(1), m.group(2), None
mref = re.search(r'url=([^"&]+)\?code=([a-f0-9]+)', body)
if mref:
return mref.group(2), state, None
mc = re.search(r'[?&]code=([a-f0-9]{16,})', body)
if mc:
return mc.group(1), state, None
# consent form
tok = re.search(r'name="authenticity_token"[^>]*value="([^"]+)"', body)
if not tok:
tok = re.search(r'value="([^"]+)"[^>]*name="authenticity_token"', body)
if not tok:
tok = re.search(r'"authenticity_token"\s*:\s*"([^"]+)"', body)
if not tok:
title = re.search(r'<title>([^<]*)', body)
return None, state, f'consent without authenticity_token (len={len(body)}) title={title.group(1) if title else "?"}'
fields = {'authorize': '1', 'authenticity_token': tok.group(1),
'client_id': client_id, 'state': state, 'scope': scope}
st2, h2, body2 = self.post_form(url, fields)
self.detail.append(f'consent http={st2} loc={h2.get("Location", "")[:80]}')
loc2 = h2.get('Location', '')
if st2 in (301, 302):
code = self._code_from(loc2)
if code:
return code, state, None
return None, state, f'consent redirect without code: {loc2[:150]}'
m = re.search(r'oauth/github\?code=([a-f0-9]{16,})[^"\']*state=([A-Za-z0-9_\-]+)', body2)
if m:
return m.group(1), m.group(2), None
mref = re.search(r'url=([^"&]+)\?code=([a-f0-9]+)', body2)
if mref:
return mref.group(2), state, None
mc = re.search(r'[?&]code=([a-f0-9]{16,})', body2)
if mc:
return mc.group(1), state, None
return None, state, f'consent http={st2} len={len(body2)}: {body2[:250]}'
@staticmethod
def _code_from(loc):
if not loc:
return None
q = urllib.parse.urlparse(loc).query
return urllib.parse.parse_qs(q).get('code', [None])[0]

91
lib/proxies.py Normal file
View File

@@ -0,0 +1,91 @@
"""proxies.py — пул прокси: SQLite (таблица pool) или txt-файл, live-проверка.
Правила (из практики):
- sticky-прокси на ВЕСЬ флоу одного аккаунта (verify/register/login с одного IP, иначе 403 anti-bot)
- live-тест перед волной (ThreadPool, ~20с на 250 прокси)
- free-пул протухает за часы — перепроверять каждый запуск
- panel/new-api банят прокси-IP (403) — панели напрямую, GH через прокси
"""
import os, random, socket, sqlite3, threading, urllib.request
from concurrent.futures import ThreadPoolExecutor
HERE = os.path.dirname(__file__)
DB = os.environ.get("UA_PROXY_DB", os.path.join(HERE, "..", "data", "proxies.db"))
TXT = os.environ.get("UA_PROXY_TXT", os.path.join(HERE, "..", "data", "proxies.txt"))
def load_pool():
"""Все живые прокси: сначала sqlite, потом txt. Формат: http://user:pass@host:port"""
out = []
if os.path.exists(DB):
try:
con = sqlite3.connect(DB)
rows = con.execute("SELECT url FROM pool WHERE alive=1").fetchall()
con.close()
out += [r[0] for r in rows]
except sqlite3.Error:
pass
if os.path.exists(TXT):
out += [l.strip() for l in open(TXT, encoding="utf-8") if l.strip() and not l.startswith("#")]
return list(dict.fromkeys(out))
def check_one(url, test_url="https://github.com/login", timeout=10):
"""Проверить прокси. ВАЖНО: verify=True — SSL-MITM прокси (verify=False пропускал их)
Firefox/Camoufox убивает SEC_ERROR_UNKNOWN_ISSUER."""
try:
import ssl
ctx = ssl.create_default_context()
opener = urllib.request.build_opener(
urllib.request.ProxyHandler({"http": url, "https": url}),
urllib.request.HTTPSHandler(context=ctx))
req = urllib.request.Request(test_url, headers={"User-Agent": "Mozilla/5.0"})
with opener.open(req, timeout=timeout) as r:
return url, r.status in (200, 302)
except Exception:
return url, False
def live_pool(test_url="https://github.com/login", workers=40, limit=None):
pool = load_pool()
if limit:
pool = pool[:limit]
good = []
with ThreadPoolExecutor(max_workers=workers) as ex:
for url, ok in ex.map(lambda u: check_one(u, test_url), pool):
if ok:
good.append(url)
return good
class StickyRotator:
"""Один прокси = один аккаунт (sticky). Авто-ротация при смерти opener'а."""
def __init__(self, pool=None, test_url="https://github.com/login"):
self.pool = list(pool if pool is not None else live_pool(test_url))
random.shuffle(self.pool)
self._i = 0
self._lock = threading.Lock()
def next(self):
with self._lock:
if not self.pool:
return None
p = self.pool[self._i % len(self.pool)]
self._i += 1
return p
def ban(self, url):
with self._lock:
if url in self.pool:
self.pool.remove(url)
def __len__(self):
return len(self.pool)
if __name__ == "__main__":
pool = load_pool()
print(f"pool total: {len(pool)}")
live = live_pool(limit=min(len(pool), 100))
print(f"live (first 100 tested): {len(live)}")