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