optimize: shuffle-drain pool (no repeat chains), executor for fetchers, DEVNULL pipes, verify=False validator, IP fallbacks, +/- hop topbar, timestamps, countdown, rotation counter, blacklist bad proxies

Made-with: Cursor
This commit is contained in:
Dr Jones
2026-04-14 17:21:10 -07:00
parent 4bb626498f
commit 45798464e4
16 changed files with 8208 additions and 7935 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import json
import logging
from typing import Callable
@@ -8,6 +9,33 @@ import httpx
log = logging.getLogger(__name__)
# Secondary fallback endpoints for IP resolution
_IP_FALLBACKS = [
"https://api.ipify.org?format=json",
"https://httpbin.org/ip",
"https://ifconfig.me/ip",
]
def _parse_ip(text: str) -> str | None:
"""Parse an IP string from JSON or plain-text response body."""
body = text.strip()
if not body:
return None
if body.startswith("{"):
try:
d = json.loads(body)
ip = d.get("ip") or d.get("origin") or d.get("query")
return str(ip).split(",")[0].strip() if ip else None
except Exception:
return None
# Plain-text: must look like an IP (short, no HTML)
if len(body) < 50 and "<" not in body:
candidate = body.split()[0].strip()
if candidate.count(".") >= 3 or ":" in candidate:
return candidate
return None
async def validate_proxies(
proxy_urls: list[str],
@@ -38,9 +66,14 @@ async def validate_proxies(
async def _check_one(proxy_url: str, check_url: str, timeout_seconds: float) -> bool:
t = httpx.Timeout(timeout_seconds, connect=min(8.0, timeout_seconds))
try:
t = httpx.Timeout(timeout_seconds, connect=min(8.0, timeout_seconds))
async with httpx.AsyncClient(proxy=proxy_url, timeout=t, verify=True, follow_redirects=True) as c:
async with httpx.AsyncClient(
proxy=proxy_url,
timeout=t,
verify=False, # many proxies have self-signed or no TLS
follow_redirects=True,
) as c:
r = await c.get(check_url)
return r.status_code == 200 and len(r.content) > 0
except Exception:
@@ -48,49 +81,55 @@ async def _check_one(proxy_url: str, check_url: str, timeout_seconds: float) ->
async def check_chain_exit_ip(
listen_proxy: str, check_url: str, timeout_seconds: float
listen_proxy: str,
check_url: str,
timeout_seconds: float,
) -> str | None:
"""Query the IP-check URL through the local chain proxy.
Returns the exit IP string on success, or None on failure."""
Returns the exit IP string on success, or None on failure.
Tries the configured URL first, then falls back to alternatives."""
urls = [check_url] + [u for u in _IP_FALLBACKS if u != check_url]
t = httpx.Timeout(timeout_seconds, connect=min(10.0, timeout_seconds))
try:
t = httpx.Timeout(timeout_seconds, connect=min(10.0, timeout_seconds))
async with httpx.AsyncClient(proxy=listen_proxy, timeout=t, verify=True, follow_redirects=True) as c:
r = await c.get(check_url)
if r.status_code != 200:
return None
body = r.text.strip()
# ipify returns {"ip":"x.x.x.x"} in json mode
if body.startswith("{"):
import json
async with httpx.AsyncClient(
proxy=listen_proxy,
timeout=t,
verify=False,
follow_redirects=True,
) as c:
for url in urls:
try:
return json.loads(body).get("ip") or json.loads(body).get("origin")
r = await c.get(url)
if r.status_code == 200:
ip = _parse_ip(r.text)
if ip:
return ip
except Exception:
return None
# plain text mode
if body and len(body) < 50:
return body
return None
continue
except Exception:
return None
pass
return None
async def get_direct_ip(check_url: str, timeout_seconds: float = 15.0) -> str | None:
"""Get our own exit IP without the proxy chain (so we can compare)."""
urls = [check_url] + [u for u in _IP_FALLBACKS if u != check_url]
t = httpx.Timeout(timeout_seconds)
try:
t = httpx.Timeout(timeout_seconds)
async with httpx.AsyncClient(timeout=t, verify=True, follow_redirects=True) as c:
r = await c.get(check_url)
if r.status_code != 200:
return None
body = r.text.strip()
if body.startswith("{"):
import json
async with httpx.AsyncClient(
timeout=t,
verify=False,
follow_redirects=True,
) as c:
for url in urls:
try:
return json.loads(body).get("ip") or json.loads(body).get("origin")
r = await c.get(url)
if r.status_code == 200:
ip = _parse_ip(r.text)
if ip:
return ip
except Exception:
return None
if body and len(body) < 50:
return body
return None
continue
except Exception:
return None
pass
return None