Files
proxy-god/proxy_chain_manager/validator.py

136 lines
4.0 KiB
Python

from __future__ import annotations
import asyncio
import json
import logging
from typing import Callable
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],
check_url: str,
concurrency: int,
timeout_seconds: float,
on_progress: Callable[[int, int], None] | None = None,
) -> list[str]:
sem = asyncio.Semaphore(max(1, concurrency))
ok: list[str] = []
lock = asyncio.Lock()
total = len(proxy_urls)
done = 0
async def one(u: str) -> None:
nonlocal done
async with sem:
good = await _check_one(u, check_url, timeout_seconds)
async with lock:
done += 1
if good:
ok.append(u)
if on_progress:
on_progress(done, total)
await asyncio.gather(*(one(u) for u in proxy_urls))
return ok
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:
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:
return False
async def check_chain_exit_ip(
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.
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:
async with httpx.AsyncClient(
proxy=listen_proxy,
timeout=t,
verify=False,
follow_redirects=True,
) as c:
for url in urls:
try:
r = await c.get(url)
if r.status_code == 200:
ip = _parse_ip(r.text)
if ip:
return ip
except Exception:
continue
except Exception:
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:
async with httpx.AsyncClient(
timeout=t,
verify=False,
follow_redirects=True,
) as c:
for url in urls:
try:
r = await c.get(url)
if r.status_code == 200:
ip = _parse_ip(r.text)
if ip:
return ip
except Exception:
continue
except Exception:
pass
return None