171 lines
5.7 KiB
Python
Executable File
171 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""GitHub expanded DashScope search — queries not covered by eternal parsers.
|
|
Random token per request, rotate on 401/403/429, max 5 pages per query.
|
|
Extracts sk-[0-9a-f]{32}, dedupes, inserts into keys.db prov='gh_expanded' status='NEW'.
|
|
"""
|
|
import os, re, sys, time, hashlib, sqlite3, random, requests
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from threading import Lock
|
|
|
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
|
DB_PATH = os.path.join(BASE, "data", "keys.db")
|
|
TOK_PATH = os.path.join(BASE, "gh_tokens.txt")
|
|
|
|
RE_KEY = re.compile(r'sk-[0-9a-f]{32}')
|
|
RE_PLACEHOLDER = re.compile(
|
|
r'sk-(xxx|your|example|\*+|None|placeholder|0{8,}|f{8,})', re.IGNORECASE)
|
|
|
|
QUERIES = [
|
|
'"sk-" "dashscope" "qwen" language:python',
|
|
'"sk-" "dashscope" extension:json',
|
|
'"sk-" "dashscope" extension:properties',
|
|
'"sk-" "dashscope" extension:toml',
|
|
'"sk-" "dashscope" extension:ts',
|
|
'"sk-" "dashscope" extension:env',
|
|
'"sk-" "qwen" "dashscope" extension:yml',
|
|
'sk- dashscope filename:.env.example',
|
|
]
|
|
|
|
MAX_PAGES = 5
|
|
HDRS_BASE = {"Accept": "application/vnd.github.v3+json", "User-Agent": "keyhunter/expanded"}
|
|
|
|
tokens = []
|
|
dead_tokens = set()
|
|
tok_lock = Lock()
|
|
db_lock = Lock()
|
|
|
|
|
|
def rand_token():
|
|
with tok_lock:
|
|
alive = [t for t in tokens if t not in dead_tokens]
|
|
if not alive:
|
|
dead_tokens.clear()
|
|
alive = tokens[:]
|
|
return random.choice(alive)
|
|
|
|
|
|
def kill_token(t):
|
|
with tok_lock:
|
|
dead_tokens.add(t)
|
|
|
|
|
|
def gh_get(url, params, tries=6):
|
|
"""GET with random token, rotate on 401/403/429. Returns Response or None."""
|
|
for _ in range(tries):
|
|
tok = rand_token()
|
|
h = dict(HDRS_BASE)
|
|
h["Authorization"] = f"token {tok}"
|
|
try:
|
|
r = requests.get(url, headers=h, params=params, timeout=20)
|
|
except Exception:
|
|
continue
|
|
if r.status_code in (401, 403, 429):
|
|
kill_token(tok)
|
|
time.sleep(0.3)
|
|
continue
|
|
return r
|
|
return None
|
|
|
|
def fetch_raw(repo, path):
|
|
# public raw content — no token needed; avoids killing tokens on private-repo 403s
|
|
url = f"https://raw.githubusercontent.com/{repo}/HEAD/{path}"
|
|
for _ in range(2):
|
|
try:
|
|
r = requests.get(url, headers={"User-Agent": "keyhunter/expanded"}, timeout=20)
|
|
if r.status_code == 200:
|
|
return repo, path, r.text
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def search_page(query, page):
|
|
r = gh_get("https://api.github.com/search/code",
|
|
{"q": query, "per_page": 100, "page": page})
|
|
if r is None or r.status_code != 200:
|
|
return set()
|
|
data = r.json()
|
|
return {(i["repository"]["full_name"], i["path"]) for i in data.get("items", [])}
|
|
|
|
|
|
def extract_keys(text):
|
|
out = set()
|
|
for m in RE_KEY.finditer(text):
|
|
k = m.group(0)
|
|
if RE_PLACEHOLDER.search(k):
|
|
continue
|
|
out.add(k)
|
|
return out
|
|
|
|
|
|
def process_query(query, db, cur, seen, stats):
|
|
print(f"\n[QUERY] {query}", flush=True)
|
|
files = set()
|
|
for page in range(1, MAX_PAGES + 1):
|
|
got = search_page(query, page)
|
|
files |= got
|
|
print(f" page {page}: {len(got)} files (cum {len(files)})", flush=True)
|
|
if len(got) < 100:
|
|
break
|
|
time.sleep(0.4)
|
|
|
|
files = [(r, p) for (r, p) in files]
|
|
new_keys = []
|
|
with ThreadPoolExecutor(max_workers=3) as pool:
|
|
futs = {pool.submit(fetch_raw, r, p): (r, p) for r, p in files}
|
|
for f in as_completed(futs):
|
|
res = f.result()
|
|
if res is None:
|
|
continue
|
|
repo, path, content = res
|
|
for k in extract_keys(content):
|
|
with db_lock:
|
|
if k in seen:
|
|
continue
|
|
seen.add(k)
|
|
h = hashlib.sha256(k.encode()).hexdigest()
|
|
cur.execute("SELECT 1 FROM keys WHERE hash=?", (h,))
|
|
if cur.fetchone():
|
|
continue
|
|
now = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
|
|
cur.execute(
|
|
"INSERT INTO keys (hash,val,prov,status,plan,price,remaining,found,repo,first_seen,last_seen)"
|
|
" VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
|
(h, k, 'gh_expanded', 'NEW', 'standard', '', '',
|
|
now, f"github:{repo}/{path}", now, now))
|
|
db.commit()
|
|
new_keys.append((k, repo, path))
|
|
for k, r, p in new_keys:
|
|
print(f" [+] NEW {k} | {r}/{p}", flush=True)
|
|
stats['files'] += len(files)
|
|
stats['new'] += len(new_keys)
|
|
print(f" => files {len(files)}, new keys {len(new_keys)}", flush=True)
|
|
|
|
|
|
def main():
|
|
with open(TOK_PATH) as f:
|
|
tokens.extend(l.strip() for l in f if l.strip() and not l.startswith("#"))
|
|
print(f"tokens: {len(tokens)}")
|
|
if not tokens:
|
|
return 1
|
|
|
|
db = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
cur = db.cursor()
|
|
seen = set(m[0] for m in cur.execute(
|
|
"SELECT val FROM keys WHERE val LIKE 'sk-%'").fetchall())
|
|
print(f"existing sk- vals loaded for dedupe: {len(seen)}")
|
|
|
|
stats = {'files': 0, 'new': 0}
|
|
for q in QUERIES:
|
|
try:
|
|
process_query(q, db, cur, seen, stats)
|
|
except Exception as e:
|
|
print(f" !! query failed: {e}", flush=True)
|
|
|
|
db.close()
|
|
print(f"\nDONE. files fetched: {stats['files']}, new keys inserted: {stats['new']}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|