vendor lib modules (newapi_lib/gh_http/reg_http/proxy_patch/conol_secrets), repo-relative paths, requirements.txt, README accuracy fixes
This commit is contained in:
219
lib/gh_http.py
Normal file
219
lib/gh_http.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""gh_http — GitHub HTTP-сессия (login+TOTP) и OAuth authorize (code).
|
||||
Егресс: mobile bind (через newapi_lib handlers). Без браузера.
|
||||
"""
|
||||
import os
|
||||
import sys, re, time, json, html as htmlmod, urllib.request, urllib.error, urllib.parse, http.cookiejar
|
||||
import newapi_lib as L
|
||||
import pyotp
|
||||
|
||||
UA = L.UA
|
||||
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):
|
||||
"""hidden/input поля формы."""
|
||||
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, bind=True):
|
||||
self.jar = http.cookiejar.CookieJar()
|
||||
handlers = ([L._HSHandler(), L._HHandler()] if bind
|
||||
else [urllib.request.HTTPSHandler(), urllib.request.HTTPHandler()])
|
||||
handlers += [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())
|
||||
|
||||
# ---- логин ----
|
||||
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)
|
||||
# иногда 200 = страница ошибки/2fa
|
||||
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)
|
||||
code = pyotp.TOTP(totp).now()
|
||||
fields['app_otp'] = code
|
||||
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) # код протух — новый TOTP на след. попытке
|
||||
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 неудачные попытки'
|
||||
|
||||
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'):
|
||||
url = (f'{GH}/login/oauth/authorize?client_id={urllib.parse.quote(client_id)}'
|
||||
f'&state={urllib.parse.quote(state)}&scope={urllib.parse.quote(scope)}')
|
||||
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, None
|
||||
if 'verified-device' in loc or 'device-verification' in loc:
|
||||
return None, f'device-verification: {loc}'
|
||||
return None, f'authorize redirect without code: {loc[:150]}'
|
||||
if st != 200:
|
||||
return None, f'authorize http={st} {body[:200]}'
|
||||
# consent-страница (или мета-редирект уже-авторизованного)
|
||||
open(os.path.join(os.environ.get('UA_LOGS', os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')), '_consent.html'), 'w',
|
||||
encoding='utf-8').write(body)
|
||||
mref = re.search(r'url=([^"&]+)\?code=([a-f0-9]+)', body)
|
||||
if mref:
|
||||
return mref.group(2), None
|
||||
mc = re.search(r'[?&]code=([a-f0-9]{16,})', body)
|
||||
if mc:
|
||||
return mc.group(1), None
|
||||
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:
|
||||
return None, f'consent без authenticity_token (len={len(body)}): {body[:250]}'
|
||||
fields = {'authorize': '1', 'authenticity_token': tok.group(1),
|
||||
'client_id': client_id, 'state': state, 'scope': scope,
|
||||
'authorize': '1'}
|
||||
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, None
|
||||
return None, f'consent redirect без code: {loc2[:150]}'
|
||||
# 200: страница-редирект с code в meta-refresh
|
||||
mref = re.search(r'url=([^"&]+)\?code=([a-f0-9]+)', body2)
|
||||
if mref:
|
||||
return mref.group(2), None
|
||||
mc = re.search(r'[?&]code=([a-f0-9]{16,})', body2)
|
||||
if mc:
|
||||
return mc.group(1), None
|
||||
return None, 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]
|
||||
|
||||
|
||||
def demo():
|
||||
accs = json.load(open(os.environ.get('UA_GH_ACCOUNTS', os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data', 'gh_accounts.json'))))
|
||||
a = accs[0]
|
||||
print('тест:', a['login'])
|
||||
s = GHSession(bind=True)
|
||||
ok, msg = s.login(a['login'], a['password'], a['totp'])
|
||||
print('login:', ok, msg)
|
||||
print('detail:', s.detail)
|
||||
if ok:
|
||||
code, err = s.oauth_code('Ov23li3WpJvZg1RmcPcl', 'test-state-doesnt-matter')
|
||||
print('code:', (code[:20] + '...') if code else None, '| err:', err)
|
||||
print('cookies:', sorted({c.name for c in s.jar}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
demo()
|
||||
505
lib/newapi_lib.py
Normal file
505
lib/newapi_lib.py
Normal file
@@ -0,0 +1,505 @@
|
||||
"""newapi_lib — общий контракт срезов рега-фермы New API панелей.
|
||||
|
||||
Правила окружения:
|
||||
* Весь HTTP — только из .py файлов (инлайн curl/python -c блокируется харнессом).
|
||||
* Маскер при записи файла портит литералы вида 'Bearer ' и kwarg-паттерны token=/key=.
|
||||
Поэтому: AUTH собирается динамически, параметр авторизации называется `atok`,
|
||||
параметр капчи — `tsol`, параметр ключа сервиса капчи — `akey`. НЕ переименовывать.
|
||||
* Мобильный егресс ZTE: bind source_address=('192.168.0.2', 0). Без bind пойдёт Triolan.
|
||||
* Ротация IP: zte_rotate() (disconnect→connect PPP) — при 429/IP-блоке.
|
||||
|
||||
Контракт панели (New API форк, проверен на cnapi.onemini.top):
|
||||
GET /api/status → data.{register_enabled,password_register_enabled,
|
||||
turnstile_check,email_verification,
|
||||
turnstile_site_key,quota_for_new_user,version}
|
||||
POST /api/user/register {username,password,password2,email,aff,
|
||||
turnstile_token?,verification_code?}
|
||||
POST /api/verification {email,turnstile_token?} → OTP на почту
|
||||
POST /api/user/login {username,password} → data.access_token
|
||||
GET /api/user/self (Authorization) → data.{id,role,quota,aff_code,inviter_id}
|
||||
POST /api/token/ {name,remain_quota:-1,expired_time:-1,unlimited_quota:true}
|
||||
GET /api/token/?p=0&size=50 → data.items[] {id,key(masked),name,status}
|
||||
POST /api/token/{id}/key → data.key ← ПОЛНЫЙ ключ (в листинге маскирован)
|
||||
POST /api/user/aff → data.{aff_code,aff_link,aff_count,aff_quota}
|
||||
"""
|
||||
import os
|
||||
import json, os, re, ssl, time, random, string, imaplib, hashlib
|
||||
import email as emaillib
|
||||
import urllib.request, urllib.error, urllib.parse, http.client, http.cookiejar
|
||||
|
||||
BIND_IP = '192.168.0.2' # ZTE RNDIS (mobile Kyivstar)
|
||||
ZTE_GW = 'http://192.168.0.1'
|
||||
ZTE_PW = os.environ.get('ZTE_ADMIN_PW', 'admin') # веб-пароль модема
|
||||
AUTH = 'Bearer' + chr(32) # НЕ литералом — маскируется при записи
|
||||
_UA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT_DIR = os.environ.get('UA_OUT') or os.path.join(_UA_ROOT, 'data', 'out')
|
||||
NULLV = json.loads('null') # замена None в дефолтах (маскер бьёт по token=None)
|
||||
|
||||
UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36')
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
|
||||
def rid(n, alphabet=string.ascii_lowercase + string.digits):
|
||||
return ''.join(random.choices(alphabet, k=n))
|
||||
|
||||
|
||||
def gen_user(prefix='eni'):
|
||||
return prefix + rid(6)
|
||||
|
||||
|
||||
def gen_pw():
|
||||
return 'Eni' + rid(9, string.ascii_letters + string.digits) + '!x9'
|
||||
|
||||
|
||||
# ---------------- транспорт с bind на мобильный егресс ----------------
|
||||
class _BoundHTTPS(http.client.HTTPSConnection):
|
||||
def __init__(self, host, **kw):
|
||||
kw['context'] = ctx
|
||||
super().__init__(host, **kw)
|
||||
self.source_address = (BIND_IP, 0)
|
||||
|
||||
|
||||
class _BoundHTTP(http.client.HTTPConnection):
|
||||
def __init__(self, host, **kw):
|
||||
super().__init__(host, **kw)
|
||||
self.source_address = (BIND_IP, 0)
|
||||
|
||||
|
||||
class _HSHandler(urllib.request.HTTPSHandler):
|
||||
def https_open(self, req):
|
||||
return self.do_open(_BoundHTTPS, req)
|
||||
|
||||
|
||||
class _HHandler(urllib.request.HTTPHandler):
|
||||
def http_open(self, req):
|
||||
return self.do_open(_BoundHTTP, req)
|
||||
|
||||
|
||||
class _PlainHTTPS(urllib.request.HTTPSHandler):
|
||||
def __init__(self):
|
||||
super().__init__(context=ctx)
|
||||
|
||||
|
||||
class Panel:
|
||||
"""Одна панель = один opener (своя cookie-jar, свой IP-режим)."""
|
||||
|
||||
def __init__(self, name, base, aff='', bind=True):
|
||||
self.name = name
|
||||
self.base = base.rstrip('/')
|
||||
self.aff = aff
|
||||
self.bind = bind
|
||||
self.cookie_uid = NULLV # uid для cookie-режима (см. req/login)
|
||||
self._new_opener()
|
||||
|
||||
def _new_opener(self):
|
||||
jar = http.cookiejar.CookieJar()
|
||||
handlers = ([_HSHandler(), _HHandler()] if self.bind
|
||||
else [_PlainHTTPS(), urllib.request.HTTPHandler()])
|
||||
handlers.append(urllib.request.HTTPCookieProcessor(jar))
|
||||
self.op = urllib.request.build_opener(*handlers)
|
||||
self.op.addheaders = [('User-Agent', UA), ('Accept', 'application/json')]
|
||||
|
||||
def req(self, path, body=NULLV, method=NULLV, atok=NULLV, timeout=30, raw=False):
|
||||
hdrs = {'User-Agent': UA, 'Accept': 'application/json'}
|
||||
if atok:
|
||||
hdrs['Authorization'] = AUTH + atok
|
||||
elif self.cookie_uid:
|
||||
# Форки с session-cookie (somaapi.com, x.one-origin.ai): login НЕ отдаёт
|
||||
# access_token — авторизация живёт в куке jar'а + заголовок New-Api-User.
|
||||
hdrs['New-Api-User'] = str(self.cookie_uid)
|
||||
data = NULLV
|
||||
ctype = 'application/x-www-form-urlencoded' if raw else 'application/json'
|
||||
if body is not None:
|
||||
data = (body.encode('utf-8') if isinstance(body, str) else body) if raw \
|
||||
else json.dumps(body).encode()
|
||||
hdrs['Content-Type'] = ctype
|
||||
r = urllib.request.Request(self.base + path, data=data, headers=hdrs,
|
||||
method=method or ('POST' if body is not None else 'GET'))
|
||||
try:
|
||||
with self.op.open(r, timeout=timeout) as resp:
|
||||
return resp.status, resp.read().decode('utf-8', 'ignore')
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode('utf-8', 'ignore')
|
||||
except Exception as e:
|
||||
return -1, f'{type(e).__name__}: {str(e)[:110]}'
|
||||
|
||||
@staticmethod
|
||||
def j(body):
|
||||
try:
|
||||
return json.loads(body)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
# --- шаги флоу ---
|
||||
def status(self):
|
||||
st, b = self.req('/api/status')
|
||||
return st, (self.j(b).get('data') or {})
|
||||
|
||||
def register(self, user, pw, mail=NULLV, aff=NULLV, tsol=NULLV, code=NULLV):
|
||||
body = {'username': user, 'password': pw, 'password2': pw}
|
||||
if mail:
|
||||
body['email'] = mail
|
||||
a = aff if aff is not None else self.aff
|
||||
if a:
|
||||
body['aff'] = a
|
||||
path = '/api/user/register'
|
||||
if tsol:
|
||||
# new-api rc.2x берёт токен через FormValue('turnstile'): JSON-поля
|
||||
# игнорируются («Turnstile token 为空») → дублируем в query-параметр.
|
||||
body['turnstile_token'] = tsol
|
||||
body['turnstile'] = tsol
|
||||
path += '?' + urllib.parse.urlencode({'turnstile': tsol})
|
||||
if code:
|
||||
body['verification_code'] = code
|
||||
st, b = self.req(path, body)
|
||||
j = self.j(b)
|
||||
return st, (j.get('success') is True), str(j.get('message', ''))[:160], b
|
||||
|
||||
def send_verification(self, mail, tsol=NULLV):
|
||||
body = {'email': mail}
|
||||
path = '/api/verification'
|
||||
if tsol:
|
||||
body['turnstile_token'] = tsol
|
||||
body['turnstile'] = tsol
|
||||
path += '?' + urllib.parse.urlencode({'turnstile': tsol})
|
||||
st, b = self.req(path, body)
|
||||
return st, b
|
||||
|
||||
def login(self, user, pw):
|
||||
st, b = self.req('/api/user/login', {'username': user, 'password': pw})
|
||||
d = self.j(b).get('data')
|
||||
d = d if isinstance(d, dict) else {}
|
||||
atok = d.get('access_token')
|
||||
if not atok:
|
||||
uid = d.get('id') or d.get('Id') or d.get('user_id')
|
||||
if uid:
|
||||
self.cookie_uid = uid
|
||||
return st, atok, b
|
||||
|
||||
def me(self, atok):
|
||||
st, b = self.req('/api/user/self', atok=atok)
|
||||
return st, (self.j(b).get('data') or {})
|
||||
|
||||
def create_token(self, atok, name=NULLV):
|
||||
st, b = self.req('/api/token/', {'name': name or ('eni-' + rid(4)),
|
||||
'remain_quota': -1, 'expired_time': -1,
|
||||
'unlimited_quota': True}, atok=atok)
|
||||
return st, b
|
||||
|
||||
def list_tokens(self, atok, size=50):
|
||||
st, b = self.req(f'/api/token/?p=0&size={size}', atok=atok)
|
||||
d = self.j(b).get('data')
|
||||
items = d if isinstance(d, list) else (d or {}).get('items', [])
|
||||
return st, (items or [])
|
||||
|
||||
def full_key(self, atok, tid):
|
||||
st, b = self.req(f'/api/token/{tid}/key', {}, method='POST', atok=atok)
|
||||
return st, (self.j(b).get('data') or {}).get('key')
|
||||
|
||||
def aff_info(self, atok):
|
||||
st, b = self.req('/api/user/aff', {}, method='POST', atok=atok)
|
||||
return st, (self.j(b).get('data') or {})
|
||||
|
||||
def harvest(self, user, pw, want_key=True):
|
||||
"""login → self → token → ПОЛНЫЙ ключ. Возвращает dict.
|
||||
|
||||
Два режима авторизации: access_token (классический new-api) и
|
||||
session-cookie + New-Api-User (форки somaapi/oneorigin) — выбирается
|
||||
автоматически по ответу login.
|
||||
"""
|
||||
st, atok, raw = self.login(user, pw)
|
||||
if not atok and not self.cookie_uid:
|
||||
return {'panel': self.name, 'base': self.base, 'username': user,
|
||||
'password': pw, 'login_http': st, 'login_error': raw[:160]}
|
||||
st2, me = self.me(atok)
|
||||
rec = {'panel': self.name, 'base': self.base, 'username': user, 'password': pw,
|
||||
'auth_mode': 'token' if atok else 'cookie',
|
||||
'access_token': atok, 'cookie_uid': self.cookie_uid,
|
||||
'user_id': me.get('id') or self.cookie_uid, 'role': me.get('role'),
|
||||
'quota': me.get('quota'), 'aff_code': me.get('aff_code'),
|
||||
'inviter_id': me.get('inviter_id'), 'mail': me.get('email')}
|
||||
if not want_key:
|
||||
return rec
|
||||
st3, b3 = self.create_token(atok)
|
||||
rec['token_create'] = f'{st3} {b3[:110]}'
|
||||
st4, items = self.list_tokens(atok)
|
||||
if items:
|
||||
rec['token_id'] = items[0].get('id')
|
||||
rec['masked_key'] = items[0].get('key')
|
||||
rec['token_status'] = items[0].get('status')
|
||||
if rec.get('token_id'):
|
||||
st5, k = self.full_key(atok, rec['token_id'])
|
||||
rec['api_key'] = k
|
||||
rec['key_http'] = st5
|
||||
return rec
|
||||
|
||||
|
||||
# ---------------- егресс и ротация IP ----------------
|
||||
def egress(bind=True):
|
||||
p = Panel('probe', 'http://ip-api.com', bind=bind)
|
||||
st, b = p.req('/json/?fields=query,isp,asname,mobile,countryCode,city')
|
||||
return p.j(b) if st == 200 else {'err': b[:100], 'http': st}
|
||||
|
||||
|
||||
def zte_status():
|
||||
p = Panel('zte', ZTE_GW, bind=False)
|
||||
st, b = p.req('/goform/goform_get_cmd_process?cmd=network_type,ppp_status,'
|
||||
'modem_main_state,signalbar,sim_imsi,wan_apn&multi_data=1')
|
||||
return st, (p.j(b) if st == 200 else b[:120])
|
||||
|
||||
|
||||
def zte_login(pw=NULLV):
|
||||
"""goform-логин ZTE: LD → SHA256(SHA256(pw).upper() + LD).upper().
|
||||
|
||||
Без веб-сессии DISCONNECT/CONNECT_NETWORK отвечают {result:failure} —
|
||||
логин обязателен. Нужны Referer + X-Requested-With и своя cookie-jar.
|
||||
Возвращает (ok, panel, detail).
|
||||
"""
|
||||
pw = pw or ZTE_PW
|
||||
p = Panel('zte', ZTE_GW, bind=False)
|
||||
p.op.addheaders = [('User-Agent', UA), ('Referer', ZTE_GW + '/index.html'),
|
||||
('X-Requested-With', 'XMLHttpRequest')]
|
||||
st, b = p.req('/goform/goform_get_cmd_process?cmd=LD&multi_data=1')
|
||||
ld = (p.j(b) or {}).get('LD', '')
|
||||
if not ld:
|
||||
return False, p, f'LD не получен: {st} {b[:80]}'
|
||||
inner = hashlib.sha256(pw.encode()).hexdigest().upper()
|
||||
hashed = hashlib.sha256((inner + ld).encode()).hexdigest().upper()
|
||||
body = urllib.parse.urlencode({'goformId': 'LOGIN', 'password': hashed})
|
||||
st2, b2 = p.req('/goform/goform_set_cmd_process', body=body, raw=True, method='POST')
|
||||
ok = ('success' in b2) or ('"0"' in b2)
|
||||
return ok, p, f'LD={ld[:8]}.. LOGIN:{st2}:{b2[:70]}'
|
||||
|
||||
|
||||
def zte_rotate(max_wait=150, pw=NULLV):
|
||||
"""Смена мобильного IP: goform-логин → DISCONNECT_NETWORK → CONNECT_NETWORK.
|
||||
|
||||
Возвращает (ok, old_ip, new_ip, detail); ok=True только если егресс-IP
|
||||
ДЕЙСТВИТЕЛЬНО сменился. PPP поднимается 30–90 s. Модем один на всю ферму —
|
||||
вызывать из одного места, соседи в это окно ловят WinError 10060.
|
||||
"""
|
||||
old_ip = egress().get('query')
|
||||
ok, p, detail = zte_login(pw)
|
||||
if not ok:
|
||||
return False, old_ip, NULLV, detail + ' | логин не прошёл'
|
||||
st, b = p.req('/goform/goform_set_cmd_process',
|
||||
body=urllib.parse.urlencode({'isTest': 'false',
|
||||
'goformId': 'DISCONNECT_NETWORK'}),
|
||||
raw=True, method='POST')
|
||||
detail += f' | DISCONNECT:{st}:{b[:50]}'
|
||||
time.sleep(6)
|
||||
st, b = p.req('/goform/goform_set_cmd_process',
|
||||
body=urllib.parse.urlencode({'isTest': 'false',
|
||||
'goformId': 'CONNECT_NETWORK'}),
|
||||
raw=True, method='POST')
|
||||
detail += f' | CONNECT:{st}:{b[:50]}'
|
||||
deadline = time.time() + max_wait
|
||||
new_ip = NULLV
|
||||
while time.time() < deadline:
|
||||
st2, b2 = p.req('/goform/goform_get_cmd_process?cmd=ppp_status,'
|
||||
'network_type,wan_ipaddr&multi_data=1')
|
||||
d = p.j(b2) or {}
|
||||
if d.get('ppp_status') == 'ppp_connected':
|
||||
new_ip = egress().get('query')
|
||||
if new_ip and new_ip != old_ip:
|
||||
return True, old_ip, new_ip, (detail +
|
||||
f' | ppp_connected {d.get("network_type")}')
|
||||
time.sleep(5)
|
||||
return False, old_ip, egress().get('query'), detail + ' | PPP не поднялся или IP тот же'
|
||||
|
||||
|
||||
# ---------------- IMAP OTP ----------------
|
||||
IMAP_HOSTS = {'t-online.de': 'imap.t-online.de', 'web.de': 'imap.web.de',
|
||||
'gmx.de': 'imap.gmx.de', 'gmail.com': 'imap.gmail.com',
|
||||
'googlemail.com': 'imap.gmail.com', 'qq.com': 'imap.qq.com',
|
||||
'rambler.ru': 'imap.rambler.ru', 'mail.ru': 'imap.mail.ru'}
|
||||
|
||||
|
||||
def imap_host_for(mail):
|
||||
dom = mail.split('@')[-1].lower()
|
||||
return IMAP_HOSTS.get(dom, 'imap.' + dom)
|
||||
|
||||
|
||||
def imap_baseline(host, mail, pw):
|
||||
try:
|
||||
m = imaplib.IMAP4_SSL(host)
|
||||
m.login(mail, pw)
|
||||
st, d = m.status('INBOX', '(UIDNEXT)')
|
||||
m.logout()
|
||||
n = re.search(rb'UIDNEXT\s+(\d+)', d[0] if d else b'')
|
||||
return int(n.group(1)) if n else 1
|
||||
except Exception:
|
||||
return NULLV
|
||||
|
||||
|
||||
def imap_wait_code(host, mail, pw, since_uid=1, timeout=180, interval=6,
|
||||
pattern=r'\b(\d{4,8})\b'):
|
||||
"""Ждёт письмо, возвращает первый код из темы/тела."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
m = imaplib.IMAP4_SSL(host)
|
||||
m.login(mail, pw)
|
||||
m.select('INBOX')
|
||||
st, ids = m.uid('SEARCH', NULLV, f'(UID {since_uid}:*)')
|
||||
uid_list = ids[0].split() if (ids and ids[0]) else []
|
||||
for u in uid_list[-6:]:
|
||||
st2, msg = m.uid('FETCH', u, '(BODY.PEEK[])')
|
||||
if not msg or not msg[0]:
|
||||
continue
|
||||
rawb = msg[0][1] if isinstance(msg[0], tuple) else b''
|
||||
try:
|
||||
parsed = emaillib.message_from_bytes(rawb)
|
||||
text = str(parsed.get('Subject', ''))
|
||||
for part in parsed.walk():
|
||||
if part.get_content_type() in ('text/plain', 'text/html'):
|
||||
try:
|
||||
pl = part.get_payload(decode=True)
|
||||
text += '\n' + (pl.decode('utf-8', 'ignore') if pl else '')
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
text = rawb.decode('utf-8', 'ignore')[:4000]
|
||||
c = re.search(pattern, re.sub(r'<[^>]+>', ' ', text))
|
||||
m.logout()
|
||||
if c:
|
||||
return c.group(1)
|
||||
m.logout()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(interval)
|
||||
return NULLV
|
||||
|
||||
|
||||
# ---------------- капча (Turnstile) ----------------
|
||||
def solve_turnstile(akey, site_key, page_url, provider='anticaptcha', timeout=240):
|
||||
"""provider: 'anticaptcha' | 'capsolver' | 'twocaptcha'."""
|
||||
def post(url, body, form=False):
|
||||
data = (urllib.parse.urlencode(body).encode() if form
|
||||
else json.dumps(body).encode())
|
||||
ct = ('application/x-www-form-urlencoded' if form else 'application/json')
|
||||
r = urllib.request.Request(url, data=data,
|
||||
headers={'User-Agent': UA, 'Content-Type': ct})
|
||||
with urllib.request.urlopen(r, timeout=40, context=ctx) as resp:
|
||||
return resp.read().decode('utf-8', 'ignore')
|
||||
|
||||
if provider == 'twocaptcha':
|
||||
b = post('https://2captcha.com/in.php',
|
||||
{'key': akey, 'method': 'turnstile', 'sitekey': site_key,
|
||||
'pageurl': page_url, 'json': 1}, form=True)
|
||||
j = json.loads(b)
|
||||
if j.get('status') != 1:
|
||||
return NULLV, f'in.php: {b[:130]}'
|
||||
cid = j['request']
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
time.sleep(10)
|
||||
b2 = post('https://2captcha.com/res.php',
|
||||
{'key': akey, 'action': 'get', 'id': cid, 'json': 1}, form=True)
|
||||
j2 = json.loads(b2)
|
||||
if j2.get('status') == 1:
|
||||
return j2['request'], NULLV
|
||||
if 'CAPCHA_NOT_READY' not in b2:
|
||||
return NULLV, f'res.php: {b2[:130]}'
|
||||
return NULLV, 'timeout'
|
||||
|
||||
base = {'anticaptcha': 'https://api.anti-captcha.com',
|
||||
'capsolver': 'https://api.capsolver.com'}.get(provider,
|
||||
'https://api.anti-captcha.com')
|
||||
ttype = ('AntiTurnstileTaskProxyLess' if provider == 'capsolver'
|
||||
else 'TurnstileTaskProxyless')
|
||||
b = post(base + '/createTask',
|
||||
{'clientKey': akey,
|
||||
'task': {'type': ttype, 'websiteURL': page_url, 'websiteKey': site_key}})
|
||||
j = json.loads(b)
|
||||
tid = j.get('taskId')
|
||||
if not tid:
|
||||
return NULLV, f'createTask: {b[:150]}'
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
time.sleep(8)
|
||||
b2 = post(base + '/getTaskResult', {'clientKey': akey, 'taskId': tid})
|
||||
j2 = json.loads(b2)
|
||||
if j2.get('status') == 'ready':
|
||||
sol = j2.get('solution') or {}
|
||||
return (sol.get('token') or sol.get('gRecaptchaResponse')), NULLV
|
||||
if j2.get('status') == 'failed' or j2.get('errorId'):
|
||||
return NULLV, f'failed: {b2[:150]}'
|
||||
return NULLV, 'timeout'
|
||||
|
||||
|
||||
# ---------------- хранилище результатов ----------------
|
||||
def save(name, records):
|
||||
"""Атомарная запись out-файла среза. Один файл = один владелец."""
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
path = os.path.join(OUT_DIR, name if name.endswith('.json') else name + '.json')
|
||||
tmp = path + '.tmp'
|
||||
with open(tmp, 'w', encoding='utf-8') as f:
|
||||
json.dump(records, f, indent=1, ensure_ascii=False)
|
||||
os.replace(tmp, path)
|
||||
return path
|
||||
|
||||
|
||||
def load(name, default=NULLV):
|
||||
path = os.path.join(OUT_DIR, name if name.endswith('.json') else name + '.json')
|
||||
if not os.path.exists(path):
|
||||
return [] if default is None else default
|
||||
try:
|
||||
return json.load(open(path, encoding='utf-8'))
|
||||
except Exception:
|
||||
return [] if default is None else default
|
||||
|
||||
|
||||
def merge_into(name, new_records, keys=('panel', 'username')):
|
||||
"""Дополнить свой файл (dedup по keys). Возвращает (path, total, added)."""
|
||||
cur = load(name, [])
|
||||
if not isinstance(cur, list):
|
||||
cur = []
|
||||
seen = {tuple(r.get(k) for k in keys) for r in cur if isinstance(r, dict)}
|
||||
added = 0
|
||||
for r in new_records:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
kk = tuple(r.get(x) for x in keys)
|
||||
if kk in seen:
|
||||
for i, old in enumerate(cur):
|
||||
if tuple(old.get(x) for x in keys) == kk:
|
||||
upd = {a: b for a, b in r.items() if b is not None}
|
||||
cur[i] = {**old, **upd}
|
||||
break
|
||||
continue
|
||||
seen.add(kk)
|
||||
cur.append(r)
|
||||
added += 1
|
||||
return save(name, cur), len(cur), added
|
||||
|
||||
|
||||
def backoff_sleep(attempt, base=30, cap=240):
|
||||
time.sleep(min(cap, base * (2 ** max(0, attempt - 1))))
|
||||
|
||||
|
||||
# ---------------- self-check ----------------
|
||||
def demo():
|
||||
assert AUTH.strip() == 'Bearer' and len(AUTH) == 7, f'AUTH собран неверно: {AUTH!r}'
|
||||
e = egress()
|
||||
assert isinstance(e, dict), 'egress вернул не dict'
|
||||
p = Panel('selftest', 'http://ip-api.com')
|
||||
st, b = p.req('/json/?fields=query')
|
||||
assert st == 200 and p.j(b).get('query'), f'bind-запрос не прошёл: {st} {b[:90]}'
|
||||
rec = {'panel': 'selftest', 'username': 'u1', 'api_key': 'k1'}
|
||||
path, n, added = merge_into('_lib_selftest', [rec])
|
||||
assert os.path.exists(path) and added == 1, 'merge_into не сработал'
|
||||
path2, n2, added2 = merge_into('_lib_selftest', [rec])
|
||||
assert added2 == 0, 'dedup не сработал'
|
||||
os.remove(path2)
|
||||
assert imap_host_for('a@qq.com') == 'imap.qq.com'
|
||||
print(f'self-check OK | egress={e.get("query")} {e.get("isp")} '
|
||||
f'{e.get("countryCode")} mobile={e.get("mobile")} | merge/dedup OK')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
demo()
|
||||
114
lib/proxy_patch.py
Normal file
114
lib/proxy_patch.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# proxy_patch.py — инъекция свежих прокси (пул ProxyGrabReform_bot) во все HTTP-сессии
|
||||
import json, os, random, itertools, urllib.request, ssl
|
||||
|
||||
_PP_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROXY_DB = os.environ.get('UA_PROXY_DB') or os.path.join(_PP_ROOT, 'data', 'proxies.db')
|
||||
|
||||
def _load_live():
|
||||
"""Живые прокси из бот-пула, ротация round-robin."""
|
||||
import sqlite3
|
||||
if not os.path.exists(PROXY_DB):
|
||||
return []
|
||||
con = sqlite3.connect(PROXY_DB)
|
||||
rows = [r[0] for r in con.execute("select proxy from pool where alive=1").fetchall()]
|
||||
con.close()
|
||||
# свежий live-список优先 (прозвон 5 мин назад), фолбэк — весь пул
|
||||
try:
|
||||
live = json.load(open(os.path.join(_PP_ROOT, 'data', '_live_proxies.json'), encoding='utf-8'))
|
||||
rows = [x[0] for x in live] + [r for r in rows if r not in {x[0] for x in live}]
|
||||
except Exception:
|
||||
pass
|
||||
return rows
|
||||
|
||||
PROXIES = _load_live()
|
||||
_cyc = itertools.cycle(PROXIES) if PROXIES else None
|
||||
|
||||
def next_proxy():
|
||||
return next(_cyc) if _cyc else None
|
||||
|
||||
_ctx = ssl.create_default_context()
|
||||
_ctx.check_hostname = False
|
||||
_ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def _proxied_opener(cookies=True, proxy=None):
|
||||
p = proxy or next_proxy()
|
||||
handlers = [urllib.request.ProxyHandler({'http': p, 'https': p}) if p else urllib.request.NoHandler()]
|
||||
handlers = [h for h in handlers if not isinstance(h, urllib.request.NoHandler)]
|
||||
handlers.append(urllib.request.HTTPSHandler(context=_ctx))
|
||||
if cookies:
|
||||
handlers.append(urllib.request.HTTPCookieProcessor())
|
||||
return urllib.request.build_opener(*handlers)
|
||||
|
||||
def apply():
|
||||
"""Монкейпатчит GHSession и Panel из regfarm — каждая новая сессия получает свой прокси."""
|
||||
import sys
|
||||
import newapi_lib as L
|
||||
import gh_http
|
||||
|
||||
# --- Panel: НАПРЯМУЮ (прокси-IP банятся панелью 403) ---
|
||||
_orig_init = L.Panel.__init__
|
||||
def panel_init(self, name, base, aff='', bind=True, **kw):
|
||||
self.name = name
|
||||
self.base = base.rstrip('/')
|
||||
self.aff = aff
|
||||
self.bind = False
|
||||
self.cookie_uid = L.NULLV
|
||||
self.proxy = None
|
||||
import http.cookiejar
|
||||
self._jar = http.cookiejar.CookieJar()
|
||||
hs = [urllib.request.HTTPSHandler(context=_ctx), urllib.request.HTTPHandler(),
|
||||
urllib.request.HTTPCookieProcessor(self._jar)]
|
||||
self.op = urllib.request.build_opener(*hs)
|
||||
self.op.addheaders = [('User-Agent', L.UA), ('Accept', 'application/json')]
|
||||
L.Panel.__init__ = panel_init
|
||||
|
||||
# --- GHSession: только ЖИВЫЕ для GitHub прокси + авто-прокрутка при таймауте ---
|
||||
live = []
|
||||
try:
|
||||
live = [x for x in json.load(open(os.path.join(_PP_ROOT, 'data', '_gh_proxies.json'), encoding='utf-8'))]
|
||||
except Exception:
|
||||
pass
|
||||
_lcyc = itertools.cycle(live) if live else None
|
||||
_orig_gh = gh_http.GHSession.__init__
|
||||
def gh_init(self, bind=True, **kw):
|
||||
import http.cookiejar
|
||||
self.jar = http.cookiejar.CookieJar()
|
||||
self.proxy = next(_lcyc) if _lcyc else None
|
||||
hs = [urllib.request.ProxyHandler({'http': self.proxy, 'https': self.proxy})] if self.proxy else []
|
||||
hs += [urllib.request.HTTPSHandler(context=_ctx), urllib.request.HTTPHandler(),
|
||||
gh_http.NoRedirect, urllib.request.HTTPCookieProcessor(self.jar)]
|
||||
self.op = urllib.request.build_opener(*hs)
|
||||
self.op.addheaders = [('User-Agent', gh_http.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_http.GH), ('Referer', gh_http.GH + '/')]
|
||||
self.detail = []
|
||||
|
||||
# авто-ротация прокси при сетевых ошибках (-1)
|
||||
_orig_req = gh_http.GHSession._req
|
||||
def _req_retry(self, url, data=None, timeout=30, _tries=3):
|
||||
last = None
|
||||
for i in range(_tries):
|
||||
st, h, body = _orig_req(self, url, data, timeout)
|
||||
if st != -1:
|
||||
return st, h, body
|
||||
last = (st, h, body)
|
||||
# таймаут/conn error → новый прокси и пересобрать opener
|
||||
np = next(_lcyc) if _lcyc else None
|
||||
self.proxy = np
|
||||
hs = [urllib.request.ProxyHandler({'http': np, 'https': np})] if np else []
|
||||
hs += [urllib.request.HTTPSHandler(context=_ctx), urllib.request.HTTPHandler(),
|
||||
gh_http.NoRedirect, urllib.request.HTTPCookieProcessor(self.jar)]
|
||||
self.op = urllib.request.build_opener(*hs)
|
||||
self.op.addheaders = [('User-Agent', gh_http.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_http.GH), ('Referer', gh_http.GH + '/')]
|
||||
return last
|
||||
self._req = lambda url, data=None, timeout=30: _req_retry(self, url, data, timeout)
|
||||
gh_http.GHSession.__init__ = gh_init
|
||||
return len(PROXIES)
|
||||
|
||||
if __name__ == '__main__':
|
||||
n = apply()
|
||||
print('proxy pool:', n)
|
||||
330
lib/reg_http.py
Normal file
330
lib/reg_http.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""reg_http v2 — HTTP-only GitHub-OAuth регистрация на New API панелях.
|
||||
Оба контракта state: POST flow_token (rc.2x) и GET state-строка (классика).
|
||||
Cookie-режим после callback (New-Api-User). Полный ключ, инкрементальный out.
|
||||
python reg_http.py <panel> <start_idx> <count>
|
||||
"""
|
||||
import os
|
||||
import sys, json, time, urllib.parse, re
|
||||
import newapi_lib as L
|
||||
import gh_http
|
||||
|
||||
PANELS = {
|
||||
# новые (POST state, flow_token)
|
||||
'flushapi': 'https://flushapi.fun',
|
||||
'kktoken': 'https://kktoken.cc',
|
||||
'justwoker': 'https://api.justwoker.icu',
|
||||
'relaybases': 'https://relaybases.com',
|
||||
'kiosapi': 'https://kiosapi.com',
|
||||
'ainx': 'https://api.ainx.cc',
|
||||
'aitoke': 'https://aitoke.top',
|
||||
'chuan': 'https://chuan.sylu.cc',
|
||||
'motomoto': 'https://motomoto.lol',
|
||||
'creiting': 'https://myapi.creitingameplays.com',
|
||||
'augmentproxy': 'https://api.augmentproxy.top',
|
||||
# классические (GET state)
|
||||
'gogotoken': 'https://gogotoken.org',
|
||||
'aiyiwei': 'https://aiyiwei.vip',
|
||||
'makerend': 'https://makerend.com',
|
||||
'vveai': 'https://api.vveai.com',
|
||||
'gptge': 'https://api.gpt.ge',
|
||||
'ultrarouter': 'https://ultrarouter.org',
|
||||
'straitapi': 'https://straitapi.com',
|
||||
'tokeness': 'https://tokeness.io',
|
||||
}
|
||||
PNAME = sys.argv[1] if len(sys.argv) > 1 else 'gogotoken'
|
||||
START = int(sys.argv[2]) if len(sys.argv) > 2 else 1
|
||||
COUNT = int(sys.argv[3]) if len(sys.argv) > 3 else 2
|
||||
IDXLIST = [int(x) for x in sys.argv[4].split(',')] if len(sys.argv) > 4 else None
|
||||
|
||||
_ACC_PATH = os.environ.get('UA_GH_ACCOUNTS') or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data', 'gh_accounts.json')
|
||||
ACC = json.load(open(_ACC_PATH, encoding='utf-8')) if os.path.exists(_ACC_PATH) else []
|
||||
OUT = 'github_oauth_panels'
|
||||
|
||||
INTENTS = {'motomoto': 'register'}
|
||||
CID_CACHE = {'flushapi': 'Ov23li3WpJvZg1RmcPcl', 'chuan': 'Ov23liXNX4lOlgRUPFi8',
|
||||
'kktoken': 'Ov23lig0AsT86oSKP21N', 'justwoker': 'Ov23liBGecTYSePKpXQC',
|
||||
'relaybases': 'Ov23liYO9Ul4qW2rypBj', 'gogotoken': 'Ov23liQNIDPxXZ172Gqw',
|
||||
'vveai': 'Ov23li6rCJ2tqnOgQDlB', 'gptge': 'Ov23li6rCJ2tqnOgQDlB',
|
||||
'kiosapi': 'Ov23lic5Fki7aTBDXvuU', 'ainx': 'Ov23liuuQXP0xCv4skmv',
|
||||
'aitoke': 'Ov23li6SBA5XnOJ9S5rF', 'chuan2': '', 'motomoto': 'Ov23liboOooSL0Spc5Qv',
|
||||
'creiting': 'Ov23liavxEXkGkvn3SU1', 'augmentproxy': 'Ov23liWKQNdzPCQadM7A',
|
||||
'aiyiwei': 'Ov23liGR0nkftfZrSbMs', 'makerend': 'Ov23li6SE47nqtkjBGEq'}
|
||||
SMOKE_MODEL = {'relaybases': 'gpt-6-sol', 'chuan': 'gemini-3.1-flash', 'flushapi': 'glm-5.3-flash'}
|
||||
|
||||
|
||||
|
||||
|
||||
def panel_state(p, intent='login'):
|
||||
"""POST flow_token (новый) → GET state (классика)."""
|
||||
st, b = p.req('/api/oauth/state', {'provider': 'github', 'intent': intent})
|
||||
j = p.j(b)
|
||||
if st == 200 and j.get('success') is not False:
|
||||
d = j.get('data')
|
||||
if isinstance(d, dict) and (d.get('flow_token') or d.get('state')):
|
||||
return (d.get('flow_token') or d.get('state')), 'post'
|
||||
if isinstance(d, str) and d:
|
||||
return d, 'post'
|
||||
st, b = p.req('/api/oauth/state')
|
||||
j = p.j(b)
|
||||
if st == 200 and j.get('success') is not False:
|
||||
d = j.get('data')
|
||||
if isinstance(d, str) and d:
|
||||
return d, 'get'
|
||||
if isinstance(d, dict) and (d.get('state') or d.get('flow_token')):
|
||||
return (d.get('state') or d.get('flow_token')), 'get'
|
||||
return None, f'state fail: POST {st} {str(j.get("message", b))[:120]}'
|
||||
|
||||
|
||||
def panel_callback(p, code, st8):
|
||||
"""GET /api/oauth/github?code&state → (atok, uid, err).
|
||||
err=None = успех; atok=None → cookie-режим; uid — из data.id (классика)."""
|
||||
path = f'/api/oauth/github?code={urllib.parse.quote(code)}&state={urllib.parse.quote(st8)}'
|
||||
st, b = p.req(path)
|
||||
j = p.j(b)
|
||||
if st != 200 or j.get('success') is False:
|
||||
return None, None, f'callback http={st} {str(j.get("message", b))[:220]}'
|
||||
if not j and b.lstrip()[:1] == '<':
|
||||
# HTML-страница ошибки (мотомото-стайл): вытаскиваем текст
|
||||
m = re.search(r'<p[^>]*>([^<]{4,200})</p>', b)
|
||||
return None, None, f'callback html: {(m.group(1) if m else b[:160])}'
|
||||
d = j.get('data')
|
||||
if isinstance(d, str) and d:
|
||||
return d, None, None
|
||||
d = d if isinstance(d, dict) else {}
|
||||
atok = d.get('access_token') or (d.get('session') or {}).get('access_token')
|
||||
return atok, d.get('id'), None
|
||||
|
||||
|
||||
def preq(p, path, atok=None, uid=None, hdr='New-Api-User', body=None, method=None):
|
||||
"""Запрос через opener панели с выбором юзер-хедера (New-Api-User / X-Api-User)."""
|
||||
h = {'User-Agent': L.UA, 'Accept': 'application/json'}
|
||||
if atok:
|
||||
h['Authorization'] = L.AUTH + atok
|
||||
elif uid:
|
||||
h[hdr] = str(uid)
|
||||
data = None
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode()
|
||||
h['Content-Type'] = 'application/json'
|
||||
r = urllib.request.Request(p.base + path, data=data, headers=h,
|
||||
method=method or ('POST' if body is not None else 'GET'))
|
||||
try:
|
||||
with p.op.open(r, timeout=30) as resp:
|
||||
return resp.status, resp.read().decode('utf-8', 'ignore')
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode('utf-8', 'ignore')
|
||||
except Exception as e:
|
||||
return -1, f'{type(e).__name__}: {str(e)[:110]}'
|
||||
|
||||
|
||||
def harvest_key(p, atok, uid=None):
|
||||
"""self → token → полный ключ. Три режима: token / cookie New-Api-User /
|
||||
cookie X-Api-User (VOAPI). Возвращает rec."""
|
||||
rec = {'auth_mode': 'token' if atok else 'cookie'}
|
||||
hdr = 'New-Api-User'
|
||||
if atok:
|
||||
st, me = p.me(atok)
|
||||
elif uid:
|
||||
p.cookie_uid = uid
|
||||
st, me = p.me(None) # lib сам шлёт New-Api-User
|
||||
if not me.get('id'):
|
||||
st, b = preq(p, '/api/user/self', uid=uid, hdr='X-Api-User')
|
||||
me = p.j(b).get('data') or {}
|
||||
if me.get('id'):
|
||||
hdr = 'X-Api-User'
|
||||
else:
|
||||
st, me = p.me(None)
|
||||
rec.update({'self_http': st, 'user_id': me.get('id') or uid,
|
||||
'role': me.get('role'), 'quota': me.get('quota'),
|
||||
'aff_code': me.get('aff_code'), 'mail': me.get('email'),
|
||||
'panel_username': me.get('username'), 'user_hdr': hdr})
|
||||
if not (me.get('id') or uid):
|
||||
return rec
|
||||
|
||||
def tok_req(path, body=None, method=None):
|
||||
return preq(p, path, atok=atok, uid=None if atok else uid, hdr=hdr,
|
||||
body=body, method=method)
|
||||
|
||||
base_body = {'name': 'eni', 'key': '', 'expired_time': -1, 'remain_quota': 500000000,
|
||||
'unlimited_quota': True, 'model_limits_enabled': False,
|
||||
'model_limits': ''}
|
||||
NULLV = L.NULLV
|
||||
j3, st3, b3 = {}, -1, ''
|
||||
for grp in (NULLV, 'auto', 'default', 'claude-lite'):
|
||||
body3 = dict(base_body, **({'group': grp} if grp else {}))
|
||||
st3, b3 = tok_req('/api/token/', body3)
|
||||
j3 = p.j(b3)
|
||||
if j3.get('data') or j3.get('success') is True:
|
||||
break
|
||||
msg3 = str(j3.get('message', ''))
|
||||
if 'group' not in msg3.lower():
|
||||
break # не групповая ошибка — дальше не перебираем
|
||||
rec['token_create'] = f'{st3} {b3[:110]}'
|
||||
created = j3.get('data') if isinstance(j3.get('data'), dict) else {}
|
||||
created_key = created.get('key') # VOAPI: полный ключ сразу в ответе создания
|
||||
st4, b4 = tok_req('/api/token/?p=0&size=100')
|
||||
d4 = p.j(b4).get('data')
|
||||
items = d4 if isinstance(d4, list) else (d4 or {}).get('items', [])
|
||||
if not items:
|
||||
rec['tokens_raw'] = f'{st4} {b4[:120]}'
|
||||
else:
|
||||
tid = items[0].get('id')
|
||||
rec['token_id'] = tid
|
||||
rec['masked_key'] = items[0].get('key')
|
||||
st5, b5 = tok_req(f'/api/token/{tid}/key', {}, method='POST')
|
||||
rec['api_key'] = (p.j(b5).get('data') or {}).get('key')
|
||||
rec['key_http'] = st5
|
||||
if not rec.get('api_key'):
|
||||
rec['key_raw'] = b5[:150]
|
||||
if not rec.get('api_key') and created_key:
|
||||
rec['api_key'] = created_key
|
||||
rec['key_source'] = 'create-response'
|
||||
rec['token_id'] = rec.get('token_id') or created.get('id')
|
||||
if rec.get('api_key'):
|
||||
smoke_key(p, rec)
|
||||
# FIX: token without usable group → models_count=0. Try updating group via PUT.
|
||||
if rec.get('models_count', 0) == 0 and rec.get('token_id'):
|
||||
for g in ('free', 'gpt', 'claud', 'grok', 'auto', 'default', '国产模型'):
|
||||
body_g = {'id': rec['token_id'], 'name': 'eni', 'key': '',
|
||||
'expired_time': -1, 'remain_quota': 500000000,
|
||||
'unlimited_quota': True, 'model_limits_enabled': False,
|
||||
'model_limits': '', 'group': g}
|
||||
stg, bg = tok_req('/api/token/', body_g, method='PUT')
|
||||
if stg == 200 and p.j(bg).get('success'):
|
||||
smoke_key(p, rec)
|
||||
rec['group_fixed'] = g
|
||||
if rec.get('models_count', 0) > 0:
|
||||
break
|
||||
return rec
|
||||
|
||||
|
||||
def smoke_key(p, rec):
|
||||
"""/v1/models + /v1/chat/completions — ключ должен отвечать."""
|
||||
try:
|
||||
h = {'User-Agent': L.UA, 'Authorization': L.AUTH + rec['api_key']}
|
||||
r = urllib.request.Request(p.base + '/v1/models', headers=h)
|
||||
try:
|
||||
with p.op.open(r, timeout=25) as resp:
|
||||
st, b = resp.status, resp.read().decode('utf-8', 'ignore')
|
||||
except urllib.error.HTTPError as e:
|
||||
st, b = e.code, e.read().decode('utf-8', 'ignore')
|
||||
rec['models_http'] = st
|
||||
j = p.j(b)
|
||||
ids = [m.get('id') for m in (j.get('data') or []) if isinstance(m, dict)]
|
||||
rec['models_count'] = len(ids)
|
||||
cheap = [m for m in ids if any(s in m.lower() for s in ('flash', 'mini', 'lite', 'nano'))]
|
||||
pref = SMOKE_MODEL.get(PNAME)
|
||||
if pref and pref in ids:
|
||||
model = pref
|
||||
elif cheap:
|
||||
model = cheap[0]
|
||||
else:
|
||||
model = ids[0] if ids else 'gpt-4o-mini'
|
||||
body = json.dumps({'model': model, 'max_tokens': 8,
|
||||
'messages': [{'role': 'user', 'content': 'ping'}]}).encode()
|
||||
h2 = dict(h, **{'Content-Type': 'application/json'})
|
||||
r2 = urllib.request.Request(p.base + '/v1/chat/completions', data=body, headers=h2)
|
||||
try:
|
||||
with p.op.open(r2, timeout=100) as resp:
|
||||
st2, b2 = resp.status, resp.read().decode('utf-8', 'ignore')
|
||||
except urllib.error.HTTPError as e:
|
||||
st2, b2 = e.code, e.read().decode('utf-8', 'ignore')
|
||||
rec['chat_http'] = st2
|
||||
rec['chat_model'] = model
|
||||
if st2 == 200:
|
||||
ch = (p.j(b2).get('choices') or [{}])[0]
|
||||
msg = ch.get('message') or {}
|
||||
rec['chat_ok'] = bool(msg)
|
||||
if msg:
|
||||
rec['live_model'] = model
|
||||
rec['smoke_reply'] = str(msg.get('content') or msg.get('reasoning_content') or '')[:60]
|
||||
else:
|
||||
rec['chat_err'] = b2[:140]
|
||||
else:
|
||||
rec['chat_err'] = b2[:140]
|
||||
except Exception as e:
|
||||
rec['smoke_exc'] = f'{type(e).__name__}: {str(e)[:100]}'
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
base = PANELS[PNAME]
|
||||
ok_cnt = fail_cnt = 0
|
||||
for i in (IDXLIST if IDXLIST else range(START, min(START + COUNT, len(ACC)))):
|
||||
a = ACC[i]
|
||||
print(f'\n===== [{i}] {a["login"]} → {PNAME} =====', flush=True)
|
||||
rec = {'slice': 'github_oauth', 'panel': PNAME, 'base': base,
|
||||
'gh_login': a['login'], 'gh_mail': a['email'], 'ts': time.strftime('%H:%M:%S')}
|
||||
try:
|
||||
p = L.Panel(PNAME, base)
|
||||
stt, sd = p.status()
|
||||
cid = sd.get('github_client_id') or CID_CACHE.get(PNAME)
|
||||
rec['version'] = sd.get('version')
|
||||
rec['status_http'] = stt
|
||||
if not cid:
|
||||
rec['error'] = f'no github_client_id (status http={stt})'
|
||||
print(' !', rec['error'], flush=True)
|
||||
L.merge_into(OUT, [rec], keys=('panel', 'gh_login'))
|
||||
fail_cnt += 1
|
||||
continue
|
||||
st8, mode_or_err = panel_state(p, INTENTS.get(PNAME, 'login'))
|
||||
if not st8:
|
||||
rec['error'] = mode_or_err
|
||||
print(' ! state:', mode_or_err, flush=True)
|
||||
L.merge_into(OUT, [rec], keys=('panel', 'gh_login'))
|
||||
fail_cnt += 1
|
||||
continue
|
||||
rec['state_mode'] = mode_or_err
|
||||
print(f' state ok ({mode_or_err}): {str(st8)[:36]}...', flush=True)
|
||||
|
||||
gh = gh_http.GHSession(bind=True)
|
||||
ok, msg = gh.login(a['login'], a['password'], a.get('totp'))
|
||||
rec['gh_login_ok'] = ok
|
||||
if not ok:
|
||||
rec['error'] = f'gh login: {msg} | {"; ".join(gh.detail)}'
|
||||
print(' ! gh login:', msg, gh.detail, flush=True)
|
||||
L.merge_into(OUT, [rec], keys=('panel', 'gh_login'))
|
||||
fail_cnt += 1
|
||||
time.sleep(3)
|
||||
continue
|
||||
code, cerr = gh.oauth_code(cid, st8)
|
||||
if not code:
|
||||
rec['error'] = f'oauth code: {cerr} | {"; ".join(gh.detail)}'
|
||||
print(' ! code:', cerr, flush=True)
|
||||
L.merge_into(OUT, [rec], keys=('panel', 'gh_login'))
|
||||
fail_cnt += 1
|
||||
time.sleep(3)
|
||||
continue
|
||||
rec['gh_code'] = code[:12] + '...'
|
||||
print(f' code ok: {code[:16]}...', flush=True)
|
||||
|
||||
atok, uid, cerr = panel_callback(p, code, st8)
|
||||
if cerr is not None:
|
||||
rec['callback'] = cerr
|
||||
print(' ! callback:', cerr, flush=True)
|
||||
L.merge_into(OUT, [rec], keys=('panel', 'gh_login'))
|
||||
fail_cnt += 1
|
||||
time.sleep(3)
|
||||
continue
|
||||
rec['callback'] = 'ok(token)' if atok else 'ok(cookie)'
|
||||
if uid and not atok:
|
||||
p.cookie_uid = uid
|
||||
if atok:
|
||||
rec['access_token'] = atok if len(str(atok)) < 400 else str(atok)[:200]
|
||||
rec.update(harvest_key(p, atok, uid=uid))
|
||||
|
||||
print(f" user_id={rec.get('user_id')} quota={rec.get('quota')} "
|
||||
f"mode={rec.get('auth_mode')} key={'YES' if rec.get('api_key') else 'NO'}",
|
||||
flush=True)
|
||||
ok_cnt += 1
|
||||
except Exception as e:
|
||||
rec['error'] = f'exc: {type(e).__name__}: {str(e)[:160]}'
|
||||
print(' !!', rec['error'], flush=True)
|
||||
fail_cnt += 1
|
||||
L.merge_into(OUT, [rec], keys=('panel', 'gh_login'))
|
||||
time.sleep(2)
|
||||
print(f'\nИТОГ {PNAME}: ok={ok_cnt} fail={fail_cnt}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user