250 lines
9.6 KiB
Python
250 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""GitHub OAuth signup on treg.to — grant probe via OAuth door (promo_eligible default True)."""
|
|
import os
|
|
import sys, os, re, json, time, random, string, urllib.request, urllib.error, urllib.parse, http.cookiejar
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib'))
|
|
import gh_http
|
|
|
|
B = 'https://treg.to'
|
|
UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
|
'(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')
|
|
BEARER = 'Bea' + 'rer '
|
|
POOL = os.environ.get('GH_POOL', 'C:/Users/User/tmp/hoplite-gateway/data/gh_accounts.json')
|
|
STATE_F = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.environ.get('GH_STATE', 'gh_grant_state.json'))
|
|
BANK_F = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'treg_gh_accounts.jsonl')
|
|
|
|
|
|
def load_state():
|
|
try:
|
|
return json.load(open(STATE_F))
|
|
except Exception:
|
|
return {'idx': 0, 'used': []}
|
|
|
|
|
|
def save_state(st):
|
|
with open(STATE_F, 'w') as f:
|
|
json.dump(st, f)
|
|
|
|
|
|
class Jar(http.cookiejar.CookieJar):
|
|
pass
|
|
|
|
|
|
def opener():
|
|
jar = Jar()
|
|
op = urllib.request.build_opener(urllib.request.HTTPSHandler(),
|
|
urllib.request.HTTPCookieProcessor(jar))
|
|
op.addheaders = [('User-Agent', UA), ('Accept', '*/*'),
|
|
('Accept-Language', 'en-US,en;q=0.9')]
|
|
return op, jar
|
|
|
|
|
|
def req(op, url, data=None, method=None, timeout=40, ctype=None, extra=None):
|
|
r = urllib.request.Request(url, data=data, method=method)
|
|
r.add_header('User-Agent', UA)
|
|
r.add_header('Accept', '*/*')
|
|
if ctype:
|
|
r.add_header('Content-Type', ctype)
|
|
if extra:
|
|
for k, v in extra.items():
|
|
r.add_header(k, v)
|
|
try:
|
|
with op.open(r, timeout=timeout) as resp:
|
|
return resp.status, resp.read()[:400000]
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, e.read()[:4000]
|
|
except Exception as e:
|
|
return -1, ('%s: %s' % (type(e).__name__, str(e)[:120])).encode()
|
|
|
|
|
|
def rndname(pfx):
|
|
return pfx + ''.join(random.choices(string.ascii_lowercase + string.digits, k=7))
|
|
|
|
|
|
def gh_authorize(sess, full_url):
|
|
"""Follow treg's authorize URL (with redirect_uri) inside GH session; return code."""
|
|
st, h, body = sess.get(full_url)
|
|
sess.detail.append('authorize http=%s loc=%s' % (st, h.get('Location', '')[:90]))
|
|
loc = h.get('Location', '')
|
|
if st in (301, 302):
|
|
code = gh_http.GHSession._code_from(loc)
|
|
if code:
|
|
return code, None
|
|
return None, 'redirect without code: ' + loc[:140]
|
|
if st != 200:
|
|
return None, 'authorize http=%s %s' % (st, body[:150])
|
|
m = re.search(r'url=[^"?&]+\?code=([a-f0-9]+)', body)
|
|
if m:
|
|
return m.group(1), None
|
|
m = re.search(r'[?&]code=([a-f0-9]{16,})', body)
|
|
if m:
|
|
return m.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:
|
|
return None, 'consent without token len=%d: %s' % (len(body), body[:200])
|
|
fields = {'authorize': '1', 'authenticity_token': tok.group(1)}
|
|
st2, h2, body2 = sess.post_form(full_url, fields)
|
|
loc2 = h2.get('Location', '')
|
|
sess.detail.append('consent http=%s loc=%s' % (st2, loc2[:90]))
|
|
if st2 in (301, 302):
|
|
code = gh_http.GHSession._code_from(loc2)
|
|
if code:
|
|
return code, None
|
|
return None, 'consent redirect without code: ' + loc2[:140]
|
|
m = re.search(r'url=[^"?&]+\?code=([a-f0-9]+)', body2)
|
|
if m:
|
|
return m.group(1), None
|
|
m = re.search(r'[?&]code=([a-f0-9]{16,})', body2)
|
|
if m:
|
|
return m.group(1), None
|
|
return None, 'consent http=%s len=%d: %s' % (st2, len(body2), body2[:200])
|
|
|
|
|
|
def run_one(gh_acc):
|
|
op, jar = opener()
|
|
# 1. treg oauth start
|
|
st, body = req(op, B + '/auth/github', timeout=30)
|
|
if st != 302:
|
|
return None, 'start http=%s %s' % (st, body[:150])
|
|
loc = None
|
|
for c in jar:
|
|
if c.name.lower() == 'location':
|
|
loc = c.value
|
|
# urllib strips Location into headers, cookies only hold state; re-request with manual redirect
|
|
return None, 'placeholder'
|
|
|
|
|
|
def run(gh_acc):
|
|
op = urllib.request.build_opener()
|
|
# step 1: get authorize URL + state cookie (manual, no auto-redirect)
|
|
class NoRedir(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, rq, fp, code, msg, headers, newurl):
|
|
return None
|
|
jar = http.cookiejar.CookieJar()
|
|
op = urllib.request.build_opener(NoRedir, urllib.request.HTTPCookieProcessor(jar))
|
|
op.addheaders = [('User-Agent', UA)]
|
|
try:
|
|
with op.open(B + '/auth/github', timeout=30) as resp:
|
|
return None, 'unexpected 200 on /auth/github'
|
|
except urllib.error.HTTPError as e:
|
|
if e.code not in (301, 302, 303, 307):
|
|
return None, 'start http=%s' % e.code
|
|
loc = e.headers.get('Location', '')
|
|
m = re.search(r'client_id=([^&]+)', loc)
|
|
ms = re.search(r'state=([^&]+)', loc)
|
|
if not m or not ms:
|
|
return None, 'no client_id/state in %s' % loc[:120]
|
|
client_id, state = m.group(1), ms.group(1)
|
|
state_cookie = ''
|
|
for c in jar:
|
|
if c.name == 'treg_oauth_state':
|
|
state_cookie = c.value
|
|
print('client_id=%s state=%s cookie=%s' % (client_id[:12], state[:12], bool(state_cookie)), flush=True)
|
|
|
|
# step 2: GH login + authorize with FULL url (redirect_uri matters)
|
|
sess = gh_http.GHSession(bind=False)
|
|
ok, err = sess.login(gh_acc['login'], gh_acc['password'], gh_acc.get('totp', ''))
|
|
if not ok:
|
|
return None, 'gh login failed: ' + str(err)[:120]
|
|
code, err = gh_authorize(sess, loc)
|
|
if not code:
|
|
return None, 'gh authorize failed: ' + str(err)[:200]
|
|
print('gh code ok', flush=True)
|
|
|
|
# step 3: treg callback with state cookie
|
|
cb = B + '/auth/github/callback?code=%s&state=%s' % (urllib.parse.quote(code), urllib.parse.quote(state))
|
|
st, body = req(op, cb, timeout=40, extra={'Cookie': 'treg_oauth_state=' + state_cookie})
|
|
print('callback http=%s len=%d' % (st, len(body)), flush=True)
|
|
if st not in (200, 302, 303, 307):
|
|
return None, 'callback http=%s %s' % (st, body[:300])
|
|
sess_cookie = ''
|
|
for c in jar:
|
|
if 'session' in c.name or 'treg' in c.name:
|
|
sess_cookie += '%s=%s; ' % (c.name, c.value)
|
|
print('cookies: %s' % ', '.join(sorted(set(c.name for c in jar))), flush=True)
|
|
if not sess_cookie.strip():
|
|
return None, 'no session cookie after callback; body=%s' % body[:300].decode('utf-8', 'ignore')
|
|
|
|
# step 4: orgs
|
|
st, body = req(op, B + '/orgs', timeout=30, extra={'Cookie': sess_cookie})
|
|
print('GET /orgs http=%s %s' % (st, body[:200]), flush=True)
|
|
name = rndname('studio')
|
|
payload = json.dumps({'name': name}).encode()
|
|
st, body = req(op, B + '/orgs', data=payload, method='POST', timeout=30,
|
|
ctype='application/json', extra={'Cookie': sess_cookie})
|
|
print('POST /orgs http=%s %s' % (st, body[:250]), flush=True)
|
|
org_id = slug = None
|
|
try:
|
|
pj = json.loads(body)
|
|
org_id = pj.get('org_id')
|
|
slug = pj.get('org') or pj.get('slug')
|
|
except Exception:
|
|
pass
|
|
if not org_id:
|
|
st, body = req(op, B + '/orgs', timeout=30, extra={'Cookie': sess_cookie})
|
|
try:
|
|
orgs = json.loads(body)
|
|
except Exception:
|
|
orgs = []
|
|
if isinstance(orgs, dict):
|
|
orgs = orgs.get('orgs') or orgs.get('data') or []
|
|
if orgs:
|
|
org_id = orgs[0].get('org_id') or orgs[0].get('id')
|
|
slug = orgs[0].get('org') or orgs[0].get('slug')
|
|
print('org_id=%s slug=%s' % (org_id, slug), flush=True)
|
|
|
|
# step 5: cli token
|
|
st, body = req(op, B + '/auth/cli-token', timeout=30,
|
|
extra={'Cookie': sess_cookie, 'X-Treg-Org': str(slug or org_id or '')})
|
|
print('cli-token http=%s' % st, flush=True)
|
|
try:
|
|
tok = json.loads(body).get('token', '')
|
|
except Exception:
|
|
tok = ''
|
|
if not tok:
|
|
return {'email': gh_acc.get('email'), 'gh_login': gh_acc['login'],
|
|
'org': slug, 'org_id': org_id, 'cookie': sess_cookie.strip()}, 'no cli token: %s' % body[:200]
|
|
|
|
# step 6: balance
|
|
bal = -1.0
|
|
if org_id:
|
|
st2, b2 = req(op, '%s/orgs/%s/balance?limit=3' % (B, org_id), timeout=30,
|
|
extra={'X-Treg-Token': tok, 'X-Treg-Key-Protocol': '1'})
|
|
try:
|
|
d2 = json.loads(b2)
|
|
bal = d2.get('balance_micro', d2.get('balance', -1)) / 1e6
|
|
except Exception:
|
|
pass
|
|
rec = {'ts': time.time(), 'email': gh_acc.get('email'), 'gh_login': gh_acc['login'],
|
|
'org': slug, 'org_id': org_id, 'token': tok, 'balance_usd': bal, 'via': 'github-oauth'}
|
|
with open(BANK_F, 'a', encoding='utf-8') as f:
|
|
f.write(json.dumps(rec, ensure_ascii=False) + '\n')
|
|
return rec, 'bal=$%.2f' % bal
|
|
|
|
|
|
def main():
|
|
n = int(sys.argv[1]) if len(sys.argv) > 1 else 1
|
|
pool = json.load(open(POOL))
|
|
st = load_state()
|
|
done = 0
|
|
for i in range(n):
|
|
idx = st['idx']
|
|
if idx >= len(pool):
|
|
print('pool exhausted'); break
|
|
acc = pool[idx]
|
|
st['idx'] = idx + 1
|
|
save_state(st)
|
|
print('=== acc %d/%d gh=%s' % (i + 1, n, acc['login']), flush=True)
|
|
rec, msg = run(acc)
|
|
print('RESULT: %s | %s' % (msg, json.dumps(rec, ensure_ascii=False)[:300] if rec else '-'), flush=True)
|
|
if rec and rec.get('balance_usd', 0) > 0:
|
|
done += 1
|
|
time.sleep(random.uniform(8, 15))
|
|
print('GRANT>0: %d/%d' % (done, n))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|