diff --git a/pyproject.toml b/pyproject.toml index ea1cc94..857fa38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "asyncpg>=0.31.0", "email-validator>=2.3.0", "fastapi>=0.135.1", + "httpx-socks[asyncio]>=0.11.0", "httpx[socks]>=0.28.1", "pydantic>=2.12.5", "pydantic-settings>=2.13.1", diff --git a/src/proxy_pool/plugins/builtin/checkers/http_anonymity.py b/src/proxy_pool/plugins/builtin/checkers/http_anonymity.py new file mode 100644 index 0000000..1ab209c --- /dev/null +++ b/src/proxy_pool/plugins/builtin/checkers/http_anonymity.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import httpx + +from proxy_pool.config import Settings +from proxy_pool.plugins.protocols import CheckContext, CheckResult + + +class HttpAnonymityChecker: + name = "http_anonymity" + stage = 2 + priority = 0 + timeout = 15.0 + + def __init__(self, judge_url: str, timeout: float = 15.0) -> None: + self._judge_url = judge_url + self.timeout = timeout + + async def check( + self, + proxy_ip: str, + proxy_port: int, + proxy_protocol: str, + context: CheckContext, + ) -> CheckResult: + if proxy_protocol in ("socks4", "socks5"): + proxy_url = f"{proxy_protocol}://{proxy_ip}:{proxy_port}" + try: + from httpx_socks import AsyncProxyTransport + + transport = AsyncProxyTransport.from_url(proxy_url) + client_kwargs = {"transport": transport, "timeout": self.timeout} + except ImportError: + return CheckResult( + passed=False, + detail="httpx-socks not installed, cannot check SOCKS proxies", + ) + else: + proxy_url = f"http://{proxy_ip}:{proxy_port}" + client_kwargs = { + "proxy": proxy_url, + "timeout": self.timeout, + "verify": False, + } + + try: + async with httpx.AsyncClient(**client_kwargs) as client: + response = await client.get(self._judge_url) + response.raise_for_status() + except httpx.TimeoutException: + return CheckResult( + passed=False, + detail=f"HTTP request through proxy timed out after {self.timeout}s", + ) + except httpx.ProxyError as err: + return CheckResult( + passed=False, + detail=f"Proxy error: {err}", + ) + except httpx.HTTPError as err: + return CheckResult( + passed=False, + detail=f"HTTP error: {err}", + ) + except Exception as err: + return CheckResult( + passed=False, + detail=f"Connection error: {err}", + ) + + latency = context.elapsed_ms() - (context.tcp_latency_ms or 0) + context.http_latency_ms = latency + + try: + data = response.json() + exit_ip = data.get("origin") or data.get("ip") + except Exception: + exit_ip = response.text.strip() + + if exit_ip: + context.exit_ip = exit_ip + + if exit_ip and exit_ip != proxy_ip: + context.anonymity_level = "elite" + elif exit_ip and exit_ip == proxy_ip: + context.anonymity_level = "anonymous" + else: + context.anonymity_level = "transparent" + + return CheckResult( + passed=True, + detail=f"Exit IP: {exit_ip}", + latency_ms=latency, + metadata={"exit_ip": exit_ip}, + ) + + def should_skip(self, proxy_protocol: str) -> bool: + return False # We handle all protocols now + + +def create_plugin(settings: Settings) -> HttpAnonymityChecker: + return HttpAnonymityChecker( + judge_url=settings.proxy.judge_url, + timeout=settings.proxy.check_http_timeout, + ) diff --git a/uv.lock b/uv.lock index 4108148..70a9e0e 100644 --- a/uv.lock +++ b/uv.lock @@ -459,6 +459,21 @@ socks = [ { name = "socksio" }, ] +[[package]] +name = "httpx-socks" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpcore" }, + { name = "httpx" }, + { name = "python-socks" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/b4/1a5a0f67207a117ca554677ccde6f30679d4b5c10a1bf838b93c7644f468/httpx_socks-0.11.0.tar.gz", hash = "sha256:2e2de097d87dfc228dd36e3c5ae0588c836e48159f5996b33cef540497af9b32", size = 117971, upload-time = "2025-12-05T05:46:41.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/2a/78b08da3f2c8eb4dd31420d0a38ed4fd4cce272dbe6a8a0d154c0300002b/httpx_socks-0.11.0-py3-none-any.whl", hash = "sha256:8c28ad569ccf681b45437ea8465203cbc082206659b6f623e4ea509b1eb4e8a7", size = 13308, upload-time = "2025-12-05T05:46:40.193Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -693,6 +708,7 @@ dependencies = [ { name = "email-validator" }, { name = "fastapi" }, { name = "httpx", extra = ["socks"] }, + { name = "httpx-socks" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "redis", extra = ["hiredis"] }, @@ -721,6 +737,7 @@ requires-dist = [ { name = "email-validator", specifier = ">=2.3.0" }, { name = "fastapi", specifier = ">=0.135.1" }, { name = "httpx", extras = ["socks"], specifier = ">=0.28.1" }, + { name = "httpx-socks", extras = ["asyncio"], specifier = ">=0.11.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic-settings", specifier = ">=2.13.1" }, { name = "redis", extras = ["hiredis"], specifier = ">=5.3.1" }, @@ -908,6 +925,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "python-socks" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/0b/cd77011c1bc01b76404f7aba07fca18aca02a19c7626e329b40201217624/python_socks-2.8.1.tar.gz", hash = "sha256:698daa9616d46dddaffe65b87db222f2902177a2d2b2c0b9a9361df607ab3687", size = 38909, upload-time = "2026-02-16T05:24:00.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/fe/9a58cb6eec633ff6afae150ca53c16f8cc8b65862ccb3d088051efdfceb7/python_socks-2.8.1-py3-none-any.whl", hash = "sha256:28232739c4988064e725cdbcd15be194743dd23f1c910f784163365b9d7be035", size = 55087, upload-time = "2026-02-16T05:23:59.147Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -996,6 +1022,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "socksio" version = "1.0.0"