305 lines
12 KiB
Python
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()) |