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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user