Files

305 lines
12 KiB
Python

"""
harvest_existing.py — OAuth harvest from EXISTING GitHub accounts
Uses Playwright + ZTE proxy + TOTP 2FA + recovery codes + checkup dismissal
Targets: GoRouter ($70), Tabi AI ($120, broken), CodeBuddy (250cr, no GH button)
Usage: python harvest_existing.py <start_idx> <count>
Reads: C:/Users/User/tmp/github_accounts.json (or C:/Users/User/Desktop/farm/gh-farm-og/github_accounts.json)
Writes: tokens.json (GoRouter API tokens)
Key features:
- TOTP 2FA via pyotp
- Recovery code fallback for device verification
- "More options" → "tomorrow" checkup dismissal
- ZTE 4G proxy rotation (12 ports)
- 46% GoRouter success rate
"""
import asyncio, json, random, time, re, sys, os
from pathlib import Path
from urllib.parse import urlparse, parse_qs
import pyotp
from playwright.async_api import async_playwright
ACCOUNTS_IN = Path("C:/Users/User/tmp/github_accounts.json")
TOKENS_OUT = Path("C:/Users/User/Desktop/farm/gh-farm-og/tokens.json")
LOG_FILE = Path("C:/Users/User/Desktop/farm/gh-farm-og/harvest_log.txt")
PROXY_BASE = "http://192.168.0.4"
PROXY_PORTS = [8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 8091]
PLATFORMS = [
{"id": "tabiai", "name": "Tabi AI", "url": "https://tabitoken.com/sign-up?aff=sn7K", "reward": "$120"},
{"id": "gorouter", "name": "GoRouter", "url": "https://gorouter.app/sign-up?aff=ivjz", "reward": "$70"},
{"id": "codebuddy", "name": "CodeBuddy", "url": "https://www.codebuddy.ai/profile/plans-usage", "reward": "250cr"},
]
def log(msg: str):
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
print(line)
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(line + "\n")
def get_proxy(idx: int) -> str:
return f"{PROXY_BASE}:{PROXY_PORTS[idx % len(PROXY_PORTS)]}"
async def login_github(page, account: dict) -> bool:
"""Login to GitHub with 2FA + recovery code + checkup dismissal"""
email = account["email"]
password = [REDACTED]"password"]
seed = account.get("seed_2fa", "")
await page.goto("https://github.com/login", wait_until="domcontentloaded", timeout=30000)
await asyncio.sleep(2)
content = await page.content()
if len(content) < 2000:
log(f" [-] Cloudflare block")
return False
await page.fill('input[name="login"]', email)
await page.fill('input[name="password"]', password)
await page.locator('input[type="submit"]').click()
await asyncio.sleep(4)
url = page.url
content = await page.content()
if "two-factor" in url.lower() or "2fa" in content.lower():
if not seed:
return False
totp = pyotp.TOTP(seed)
code = totp.now()
await page.fill('input[name="app_otp"]', code)
await asyncio.sleep(1)
for btn_text in ["Verify", "Submit", "Confirm"]:
try:
btn = page.locator(f"button:has-text('{btn_text}')").first
if await btn.is_visible(timeout=1000):
await btn.click()
break
except: continue
await asyncio.sleep(5)
url = page.url
content = await page.content()
# Device verification → recovery code
if "device" in content.lower() and "verify" in content.lower():
recovery_codes = account.get("recovery_codes", "")
if not recovery_codes:
return False
for alt_sel in ["a:has-text('recovery')", "a:has-text('another way')"]:
try:
el = page.locator(alt_sel).first
if await el.is_visible(timeout=2000):
await el.click()
await asyncio.sleep(2)
break
except: continue
codes = [c.strip() for c in recovery_codes.split(",") if c.strip()]
if codes:
await page.fill('input[type="text"]', codes[0])
await asyncio.sleep(1)
for btn_text in ["Verify", "Submit", "Confirm"]:
try:
btn = page.locator(f"button:has-text('{btn_text}')").first
if await btn.is_visible(timeout=1000):
await btn.click()
await asyncio.sleep(5)
break
except: continue
url = page.url
content = await page.content()
if "dashboard" in url.lower() or ("github.com" in url and "login" not in url):
if "sign out" in content.lower() or "signout" in content.lower():
return True
if "password" in content.lower() and "incorrect" in content.lower():
return False
if "account" in content.lower() and ("suspended" in content.lower() or "disabled" in content.lower() or "locked" in content.lower()):
log(f" [-] Account suspended/locked")
return False
return False
async def dismiss_checkup(page):
"""Dismiss GitHub 2FA checkup: More options → tomorrow"""
if "two_factor_checkup" not in page.url:
return
for skip_sel in ["button:has-text('tomorrow')", "button:has-text('Verify your 2FA')", "button:has-text('More options')"]:
try:
el = page.locator(skip_sel).first
if await el.is_visible(timeout=2000):
await el.click()
if "More options" in skip_sel:
await asyncio.sleep(2)
for sub in ["button:has-text('tomorrow')", "button:has-text('Skip')"]:
try:
sub_el = page.locator(sub).first
if await sub_el.is_visible(timeout=2000):
await sub_el.click()
break
except: continue
log(f" [+] Checkup dismissed via {skip_sel[:40]}")
await asyncio.sleep(3)
return
except: continue
async def harvest_platform(page, platform: dict, account: dict) -> dict | None:
"""Go to platform, click Sign in with GitHub, authorize, extract token"""
log(f" [{platform['name']}] OAuth...")
try:
await page.goto(platform["url"], wait_until="domcontentloaded", timeout=60000)
await asyncio.sleep(3)
clicked = False
for sel in ["a[href*='github']", "button:has-text('GitHub')", "button:has-text('github')",
"a:has-text('GitHub')", "a:has-text('Sign in')", "button:has-text('Sign in')"]:
try:
btn = page.locator(sel).first
if await btn.is_visible(timeout=2000):
await btn.click()
clicked = True
break
except: continue
if not clicked:
links = await page.evaluate("() => [...document.querySelectorAll('a')].filter(a => a.href.includes('github.com/login/oauth')).map(a => a.href)")
if links:
await page.goto(links[0], wait_until="domcontentloaded")
clicked = True
if not clicked:
return None
await asyncio.sleep(4)
# Authorize
if "github.com" in page.url and ("authorize" in page.url.lower() or "oauth" in page.url.lower()):
auth_clicked = False
for sel in ["button[name='authorize']", "button:has-text('Authorize')", "button:has-text('authorize')",
"#js-oauth-authorize-btn", "button[type='submit']", "button.btn-primary"]:
try:
el = page.locator(sel).first
if await el.is_visible(timeout=2000):
await el.click()
auth_clicked = True
await asyncio.sleep(5)
break
except: continue
if not auth_clicked:
result = await page.evaluate("""
() => { const btns = [...document.querySelectorAll('button')];
const auth = btns.find(b => b.textContent.includes('Authorize'));
if (auth) { auth.click(); return true; }
const submit = document.querySelector('button[type="submit"]');
if (submit) { submit.click(); return true; }
return false; }""")
if result:
await asyncio.sleep(5)
await asyncio.sleep(5)
# Dismiss checkup if it appears after authorize
await dismiss_checkup(page)
# Extract token
url = page.url
token = None
parsed = urlparse(url)
for param in ["token", "access_token", "accessToken", "session", "key", "code"]:
for qs in [parse_qs(parsed.query), parse_qs(parsed.fragment)]:
if param in qs:
token = [REDACTED]][0]
break
if token: break
if not token:
try:
ls = await page.evaluate("() => JSON.parse(JSON.stringify(localStorage))")
for k in ["token", "accessToken", "access_token", "authToken", "session"]:
if k in ls:
token = ls[k]
break
except: pass
if token:
log(f" [+] TOKEN: {token[:50]}...")
return {
"platform": platform["id"], "name": platform["name"],
"reward": platform["reward"], "token": token,
"github_email": account["email"],
"github_username": account.get("username", ""),
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
}
return None
except Exception as e:
log(f" [-] Error: {str(e)[:80]}")
return None
async def process_account(account: dict, idx: int) -> list[dict]:
proxy = get_proxy(idx)
results = []
log(f"\n{'='*50}")
log(f"[{idx+1}] {account.get('username','?')} | {account['email']}")
log(f" Proxy: {proxy}")
try:
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=False, proxy={"server": proxy},
args=["--no-first-run", "--no-default-browser-check"],
)
page = await browser.new_page()
try:
if not await login_github(page, account):
await browser.close()
return results
await dismiss_checkup(page)
for platform in PLATFORMS:
result = await harvest_platform(page, platform, account)
if result:
results.append(result)
finally:
await browser.close()
except Exception as e:
log(f" [-] Browser error: {str(e)[:80]}")
return results
async def main():
if not ACCOUNTS_IN.exists():
log(f"FATAL: {ACCOUNTS_IN} not found")
return
accounts = json.loads(ACCOUNTS_IN.read_text(encoding="utf-8"))
log(f"Loaded {len(accounts)} GitHub accounts")
tokens = json.loads(TOKENS_OUT.read_text(encoding="utf-8")) if TOKENS_OUT.exists() else []
start_idx = int(sys.argv[1]) if len(sys.argv) > 1 else 0
count = int(sys.argv[2]) if len(sys.argv) > 2 else len(accounts)
end_idx = min(start_idx + count, len(accounts))
log(f"Processing: {start_idx} → {end_idx-1} ({end_idx - start_idx} accounts)")
for i in range(start_idx, end_idx):
try:
results = await process_account(accounts[i], i)
if results:
tokens.extend(results)
TOKENS_OUT.write_text(json.dumps(tokens, ensure_ascii=False, indent=2), encoding="utf-8")
log(f" [+] Saved {len(results)} tokens (total: {len(tokens)})")
except Exception as e:
log(f" [!] Fatal: {str(e)[:80]}")
if i < end_idx - 1:
delay = random.uniform(15, 30)
log(f" [zZz] {delay:.0f}s delay...")
await asyncio.sleep(delay)
log(f"\n=== DONE: {len(tokens)} tokens total ===")
if __name__ == "__main__":
asyncio.run(main())