Files
hack/tools/scripts/llm-key-hunter/proxy_pool.py
T

336 lines
14 KiB
Python

#!/usr/bin/env python3
"""Persistent self-healing proxy pool — favors DURABLE proxies.
Free public proxies die within minutes. Instead of trusting whatever works
right now, this daemon:
1. scrapes fresh candidates from many public lists every REFRESH_CYCLES,
2. re-tests EVERY known proxy against the REAL target (api.github.com) every
TEST_INTERVAL seconds,
3. tracks per-proxy uptime, consecutive successes, and survival time,
4. exposes two files for hunters:
github_live.txt — all proxies that passed the last round
github_stable.txt — durable proxies: survived MIN_ROUNDS rounds with
>= UPTIME_MIN uptime and a recent success
State persists in pool_state.json so durability accrues across restarts.
Run it once and leave it running (or schedule it). Logs to proxy_pool.log.
Usage:
python3 proxy_pool.py --once # one scrape + test round, then exit
python3 proxy_pool.py # run forever
python3 proxy_pool.py --workers 200
"""
import argparse, json, os, re, socket, subprocess, time, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from datetime import datetime, timezone
HERE = Path(__file__).resolve().parent
OUT = HERE / "results" / "proxies"
OUT.mkdir(parents=True, exist_ok=True)
STATE = OUT / "pool_state.json"
LIVE = OUT / "github_live.txt"
STABLE = OUT / "github_stable.txt"
VETERAN = OUT / "github_veteran.txt"
LOG = OUT / "proxy_pool.log"
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36"
# The actual things we need the proxy for: reaching GitHub API AND raw content.
# A proxy must reach BOTH to be considered "stable" (some proxies MITM or only
# allow certain hosts). 200/401/403/429/404 all mean "the proxy reached GitHub".
TARGETS = [
"https://api.github.com/zen",
"https://raw.githubusercontent.com/torvalds/linux/master/README",
]
PASS_CODES = {200, 401, 403, 429, 404}
# A proxy must pass at least this many of the TARGETS to count as a live round.
MIN_TARGETS_PASS = 2
SOURCES = [
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt",
"https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks5.txt",
"https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt",
"https://raw.githubusercontent.com/monosans/proxy-list/main/proxies_anonymous/http.txt",
"https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt",
"https://raw.githubusercontent.com/clarketm/proxy-list/master/proxy-list-raw.txt",
"https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/http.txt",
"https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/https.txt",
"https://raw.githubusercontent.com/roosterkid/openproxylist/main/HTTPS_RAW.txt",
"https://raw.githubusercontent.com/mmpx12/proxy-list/master/http.txt",
"https://raw.githubusercontent.com/mmpx12/proxy-list/master/https.txt",
"https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/protocols/http/data.txt",
"https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/countries/CN/data.txt",
"https://raw.githubusercontent.com/zloi-user/hideip.me/main/http.txt",
"https://raw.githubusercontent.com/zloi-user/hideip.me/main/https.txt",
"https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt",
"https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt",
"https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt",
"https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/http.txt",
"https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/https.txt",
"https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/http/http.txt",
"https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/https/https.txt",
"https://raw.githubusercontent.com/yemixzy/proxy-list/main/proxies/http.txt",
"https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt",
"https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/https.txt",
"https://raw.githubusercontent.com/proxy4parsing/proxy-list/main/http.txt",
"https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/http_proxies.txt",
]
# Durability thresholds
MIN_ROUNDS = 3 # must have been tested at least this many rounds
UPTIME_MIN = 0.50 # and succeed at least this fraction of the time
STREAK_MIN = 2 # and be alive on the last 2 consecutive rounds
# Veteran tier: the truly durable proxies that survive over time.
VET_MIN_ROUNDS = 12 # at least 12 rounds of history
VET_MIN_AGE_H = 1.0 # survived at least 1 wall-clock hour
VET_UPTIME_MIN = 0.70 # 70%+ uptime
VET_STREAK_MIN = 3 # alive the last 3 rounds
TEST_TIMEOUT = 5
MAX_AGE_FAILS = 8 # forget a NEW proxy after this many consecutive failures
VET_MAX_FAILS = 20 # give veterans a longer leash (they sometimes blip)
RECHECK_INTERVAL = 60 # re-test all live/known proxies every N seconds
SCRAPE_INTERVAL = 1800 # re-scrape sources every N seconds
IPPORT_RE = re.compile(rb"\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{2,5})\b")
DIRECT = urllib.request.build_opener()
def log(msg):
line = f"[{datetime.now().strftime('%H:%M:%S')}] {msg}"
print(line, flush=True)
with LOG.open("a") as f:
f.write(line + "\n")
def scrape():
found = set()
for url in SOURCES:
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with DIRECT.open(req, timeout=20) as r:
data = r.read()
for m in IPPORT_RE.finditer(data):
ip = m.group(1).decode()
port = int(m.group(2))
octets = [int(x) for x in ip.split(".")]
if all(0 <= o <= 255 for o in octets) and 1 <= port <= 65535:
found.add(f"{ip}:{port}")
except Exception:
continue
return found
def test_proxy(pp, timeout=TEST_TIMEOUT):
"""Return (pp, ok, latency_ms, code_or_err). Uses curl because Python's
ProxyHandler falsely reports success when the tunnel hangs mid-transfer.
A proxy is OK only if it reaches MIN_TARGETS_PASS of TARGETS."""
scheme = "socks5" if pp.endswith("|s5") else "http"
addr = pp.split("|")[0]
proxy_url = f"{scheme}://{addr}"
t0 = time.time()
codes = []
passed = 0
for target in TARGETS:
try:
out = subprocess.run(
["curl", "-s", "-m", str(timeout), "-x", proxy_url,
"-o", "/dev/null", "-w", "%{http_code}",
"-A", UA, target],
capture_output=True, text=True, timeout=timeout + 3)
code = out.stdout.strip()
codes.append(code)
if code.isdigit() and int(code) in PASS_CODES:
passed += 1
else:
# one failed target is enough to fail fast
break
except subprocess.TimeoutExpired:
codes.append("timeout")
break
except Exception as e:
codes.append(type(e).__name__)
break
lat = int((time.time() - t0) * 1000)
ok = passed >= MIN_TARGETS_PASS
return pp, ok, lat, ",".join(codes)
def load_state():
if STATE.exists():
try:
return json.loads(STATE.read_text())
except Exception:
return {}
return {}
def save_state(state):
tmp = STATE.with_suffix(".tmp")
tmp.write_text(json.dumps(state, ensure_ascii=False))
os.replace(tmp, STATE)
def update(state, results, round_id):
now = time.time()
live = []
for pp, ok, lat, code in results:
e = state.setdefault(pp, {"first_seen": now, "successes": 0,
"fails": 0, "streak": 0, "rounds": 0,
"last_ok": 0, "last_code": "",
"latencies": []})
e["rounds"] += 1
e["last_code"] = code
if ok:
e["successes"] += 1
e["streak"] += 1
e["fails"] = 0
e["last_ok"] = now
e["latencies"].append(lat)
e["latencies"] = e["latencies"][-20:]
live.append(pp)
else:
e["fails"] += 1
e["streak"] = 0
# evict dead weight; veterans get a longer leash (they blip sometimes)
evicted = 0
for pp in list(state.keys()):
e = state[pp]
is_vet = (e["rounds"] >= VET_MIN_ROUNDS
and (e["successes"] / max(1, e["successes"] + e["fails"])) >= VET_UPTIME_MIN)
limit = VET_MAX_FAILS if is_vet else MAX_AGE_FAILS
if e["fails"] >= limit:
del state[pp]
evicted += 1
return live, evicted
def _avg_lat(e):
return sum(e["latencies"]) / len(e["latencies"]) if e["latencies"] else 9999
def write_lists(state, live):
live_set = set(live)
stable = []
veterans = []
now = time.time()
for pp, e in state.items():
if pp not in live_set:
continue
total = e["successes"] + e["fails"]
uptime = e["successes"] / total if total else 0
age_h = (now - e["first_seen"]) / 3600
avg_lat = _avg_lat(e)
if (e["rounds"] >= MIN_ROUNDS and uptime >= UPTIME_MIN
and e["streak"] >= STREAK_MIN):
stable.append((pp, avg_lat, uptime, age_h, e["rounds"]))
if (e["rounds"] >= VET_MIN_ROUNDS and age_h >= VET_MIN_AGE_H
and uptime >= VET_UPTIME_MIN and e["streak"] >= VET_STREAK_MIN):
veterans.append((pp, avg_lat, uptime, age_h, e["rounds"]))
# write live (sorted by latency if known)
live_sorted = sorted(live, key=lambda p: _avg_lat(state.get(p, {})))
LIVE.write_text("\n".join(f"http://{pp.split('|')[0]}" for pp in live_sorted) + "\n")
stable_sorted = sorted(stable, key=lambda x: x[1])
STABLE.write_text("\n".join(
f"http://{pp.split('|')[0]}\t{lat:.0f}ms\tup={up:.0%}\tage={age:.1f}h\trounds={r}"
for pp, lat, up, age, r in stable_sorted) + "\n")
VETERAN.write_text("\n".join(
f"http://{pp.split('|')[0]}\t{lat:.0f}ms\tup={up:.0%}\tage={age:.1f}h\trounds={r}"
for pp, lat, up, age, r in sorted(veterans, key=lambda x: x[1])
) + ("\n" if veterans else ""))
return len(stable_sorted), len(veterans)
def run_round(state, workers, round_id, do_scrape, max_new=5000, max_test=10000):
new_now = []
if do_scrape:
fresh = scrape()
for p in fresh:
if p not in state:
state[p] = {"first_seen": time.time(), "successes": 0,
"fails": 0, "streak": 0, "rounds": 0,
"last_ok": 0, "last_code": "", "latencies": []}
new_now.append(p)
log(f"scraped {len(fresh)} candidates, {len(new_now)} new, {len(state)} total")
# Always re-test known proxies first; add a bounded batch of untested new ones.
known = [p for p, e in state.items() if e["rounds"] > 0]
untested = [p for p, e in state.items() if e["rounds"] == 0]
targets = known + untested[:max_new]
if len(targets) > max_test:
# keep all known, trim newest if still over
targets = targets[:max_test]
log(f"round {round_id}: testing {len(targets)} proxies "
f"({len(known)} known, {len(targets)-len(known)} new; workers={workers})")
results = []
t0 = time.time()
with ThreadPoolExecutor(max_workers=workers) as ex:
futs = [ex.submit(test_proxy, p) for p in targets]
for f in as_completed(futs):
results.append(f.result())
live, evicted = update(state, results, round_id)
n_stable, n_vet = write_lists(state, live)
save_state(state)
log(f"round {round_id} done in {time.time()-t0:.0f}s: {len(live)} live, "
f"{n_stable} stable, {n_vet} veterans, {evicted} evicted, "
f"{max(0,len(untested)-max_new)} untested queued")
return n_stable, n_vet
def show_status():
state = load_state()
now = time.time()
rows = []
for pp, e in state.items():
total = e["successes"] + e["fails"]
up = e["successes"] / total if total else 0
age_h = (now - e["first_seen"]) / 3600
rows.append((pp, _avg_lat(e), up, age_h, e["rounds"], e["streak"],
e.get("last_code", "")))
live = [r for r in rows if r[5] > 0]
stable = [r for r in live if r[4] >= MIN_ROUNDS and r[2] >= UPTIME_MIN and r[5] >= STREAK_MIN]
vets = [r for r in live if r[4] >= VET_MIN_ROUNDS and r[3] >= VET_MIN_AGE_H
and r[2] >= VET_UPTIME_MIN and r[5] >= VET_STREAK_MIN]
print(f"tracked: {len(rows)} | live now: {len(live)} | stable: {len(stable)} | veterans: {len(vets)}")
if vets:
print("\n=== VETERANS (durable) ===")
for pp, lat, up, age, rnd, st, code in sorted(vets, key=lambda x: x[1]):
print(f" http://{pp.split('|')[0]}\t{lat:.0f}ms\tup={up:.0%}\tage={age:.1f}h\trounds={rnd}\tstreak={st}")
if stable:
print("\n=== STABLE ===")
for pp, lat, up, age, rnd, st, code in sorted(stable, key=lambda x: x[1])[:15]:
print(f" http://{pp.split('|')[0]}\t{lat:.0f}ms\tup={up:.0%}\tage={age:.1f}h\trounds={rnd}\tstreak={st}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--once", action="store_true")
ap.add_argument("--status", action="store_true", help="print current pool status and exit")
ap.add_argument("--workers", type=int, default=300)
args = ap.parse_args()
if args.status:
show_status()
return
state = load_state()
log(f"loaded {len(state)} known proxies from state")
round_id = 0
last_scrape = 0
while True:
round_id += 1
do_scrape = (time.time() - last_scrape) > SCRAPE_INTERVAL or last_scrape == 0
if do_scrape:
last_scrape = time.time()
try:
run_round(state, args.workers, round_id, do_scrape)
except Exception as e:
log(f"round error: {type(e).__name__}: {e}")
if args.once:
break
time.sleep(RECHECK_INTERVAL)
if __name__ == "__main__":
main()