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

176
providers/gh_device_flow.py Normal file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""gh_device_auth — device-flow авторизация gh CLI через уже залогиненную GHSession.
1. gh auth login --web печатает one-time code XXXX-XXXX и поллит github.com/login/oauth/access_token
2. Мы: GHSession.login(account) -> GET /login/device -> POST user_code -> authorize-confirm
3. gh получает gho_-токен (scopes: repo,read:org,gist) и пишет в свой keyring/hosts.yml
4. gh auth token -> токен; сохраняем в .gh_push_tokens.json
Usage: python gh_device_auth.py
"""
import json
import os
import re
import subprocess
import sys
import threading
import time
sys.path.insert(0, 'C:/Users/User/tmp/beat_toolkit')
from gh_session import GHSession, find_form, form_fields
POOL = 'C:/Users/User/tmp/gh_accounts.json'
BANK = 'C:/Users/User/tmp/beat_autoreg/accounts.jsonl'
OUT = 'C:/Users/User/tmp/beat_autoreg/.gh_push_tokens.json'
GH = 'https://github.com'
def pick_account():
pool = {a['login']: a for a in json.load(open(POOL, encoding='utf-8'))}
recs = [json.loads(l) for l in open(BANK, encoding='utf-8') if l.strip()]
cands = [r['gh_login'] for r in recs if r.get('access_token') and r['gh_login'] in pool]
cands = list(dict.fromkeys(cands))[::-1]
for login in cands:
return pool[login]
return None
def approve_device(s, code):
"""Device-flow: select_account -> user-code-N посимвольно -> confirmation -> consent."""
# 1. select_account
st, h, body = s.get(GH + '/login/device/select_account')
if st != 200:
return False, f'select_account http={st}'
form = find_form(body, 'select_account')
fields = form_fields(form) if form else {}
st2, h2, body2 = s.post_form(GH + '/login/device/select_account', fields)
loc2 = h2.get('Location', '')
if st2 in (301, 302) and loc2:
u = loc2 if loc2.startswith('http') else GH + loc2
st2, h2, body2 = s.get(u)
# 2. confirmation form: user-code-0..8 посимвольно (включая дефис)
cform = find_form(body2, '/login/device/confirmation')
if not cform:
return False, f'no confirmation form (http={st2}, len={len(body2)})'
fields = form_fields(cform)
chars = list(code) # 'ABCF-A8AA' -> 9 символов
for i, ch in enumerate(chars):
fields[f'user-code-{i}'] = ch
if 'commit' in fields and not fields['commit']:
fields['commit'] = 'Continue'
st3, h3, body3 = s.post_form(GH + '/login/device/confirmation', fields)
loc3 = h3.get('Location', '')
if st3 in (301, 302) and loc3:
u = loc3 if loc3.startswith('http') else GH + loc3
st3, h3, body3 = s.get(u)
if 'Device activated' in body3 or 'Congratulations' in body3:
return True, 'activated (no consent)'
open('C:/Users/User/tmp/beat_autoreg/consent_debug.html', 'w', encoding='utf-8').write(body3)
# 3. consent страница gh CLI OAuth app: форма с authenticity_token + Authorize
m = re.search(r'name="authenticity_token"[^>]*value="([^"]+)"', body3)
if not m:
return False, f'no consent token (http={st3}, len={len(body3)})'
aform = None
for mm in re.finditer(r'<form[^>]*>.*?</form>', body3, re.S):
if 'authenticity_token' in mm.group(0):
aform = mm.group(0)
break
action = GH + '/login/oauth/authorize'
if aform:
am = re.search(r'action="([^"]+)"', aform)
if am:
action = am.group(1)
if not action.startswith('http'):
action = GH + action
fields = form_fields(aform)
else:
fields = {'authenticity_token': m.group(1)}
fields['authenticity_token'] = m.group(1)
fields['authorize'] = '1' # КРИТИЧНО: кнопка Authorize (0 = Deny = access_denied)
st4, h4, body4 = s.post_form(action, fields)
loc4 = h4.get('Location', '')
if st4 in (301, 302) and loc4:
u = loc4 if loc4.startswith('http') else GH + loc4
st4, h4, body4 = s.get(u)
if 'Device activated' in body4 or 'Congratulations' in body4 or st4 == 200:
return True, f'consent ok http={st4}'
return False, f'consent unclear http={st4} len={len(body4)}'
def main():
if len(sys.argv) > 1:
pool = {a['login']: a for a in json.load(open(POOL, encoding='utf-8'))}
acc = pool.get(sys.argv[1])
else:
acc = pick_account()
if not acc:
print('NO_ACCOUNT')
return
login = acc['login']
print('account:', login)
# 1. GHSession login
s = GHSession()
ok, msg = s.login(login, acc['password'], acc.get('totp'))
print('gh login:', ok, msg)
if not ok:
print('LOGIN_FAIL')
return
# 2. gh auth login --web в фоне (pty через subprocess + pipes)
env = dict(os.environ)
env['GH_PROMPT_DISABLED'] = '1'
proc = subprocess.Popen(
['gh', 'auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, encoding='utf-8', errors='replace', env=env)
code = None
buf = ''
deadline = time.time() + 60
while time.time() < deadline:
line = proc.stdout.readline()
if not line:
time.sleep(0.2)
continue
buf += line
print('gh:', line.strip(), flush=True)
m = re.search(r'([A-Z0-9]{4}-[A-Z0-9]{4})', line)
if m:
code = m.group(1)
break
if proc.poll() is not None:
break
if not code:
print('NO_CODE from gh. buf:', buf[-300:])
proc.kill()
return
print('device code:', code)
# Enter в gh (откроет "браузер" — упадёт молча, поллинг продолжится)
try:
proc.stdin.write('\n')
proc.stdin.flush()
except Exception:
pass
# 3. approve в GHSession
ok2, info = approve_device(s, code)
print('approve:', ok2, info)
if not ok2:
proc.kill()
return
# 4. ждать завершения gh (поллинг access_token)
try:
out, _ = proc.communicate(timeout=60)
print('gh finished:', (out or '')[-300:])
except subprocess.TimeoutExpired:
proc.kill()
print('gh timeout after approve')
# 5. gh auth token
r = subprocess.run(['gh', 'auth', 'token'], capture_output=True, text=True, timeout=30)
tok = r.stdout.strip()
if tok.startswith(('gho_', 'ghu_', 'ghp_')):
json.dump([[tok, login]], open(OUT, 'w'))
print('TOKEN_OK len', len(tok), '-> ', OUT)
else:
print('TOKEN_FAIL:', r.stderr[:200])
if __name__ == '__main__':
main()