97 lines
3.1 KiB
Python
97 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Callable
|
|
|
|
import httpx
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
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:
|
|
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:
|
|
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."""
|
|
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
|
|
try:
|
|
return json.loads(body).get("ip") or json.loads(body).get("origin")
|
|
except Exception:
|
|
return None
|
|
# plain text mode
|
|
if body and len(body) < 50:
|
|
return body
|
|
return None
|
|
except Exception:
|
|
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)."""
|
|
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
|
|
try:
|
|
return json.loads(body).get("ip") or json.loads(body).get("origin")
|
|
except Exception:
|
|
return None
|
|
if body and len(body) < 50:
|
|
return body
|
|
return None
|
|
except Exception:
|
|
return None
|