feat: add HTTP anonymity checker with SOCKS support
This commit is contained in:
parent
20b8d0a917
commit
328caec770
@ -14,6 +14,7 @@ dependencies = [
|
|||||||
"asyncpg>=0.31.0",
|
"asyncpg>=0.31.0",
|
||||||
"email-validator>=2.3.0",
|
"email-validator>=2.3.0",
|
||||||
"fastapi>=0.135.1",
|
"fastapi>=0.135.1",
|
||||||
|
"httpx-socks[asyncio]>=0.11.0",
|
||||||
"httpx[socks]>=0.28.1",
|
"httpx[socks]>=0.28.1",
|
||||||
"pydantic>=2.12.5",
|
"pydantic>=2.12.5",
|
||||||
"pydantic-settings>=2.13.1",
|
"pydantic-settings>=2.13.1",
|
||||||
|
|||||||
105
src/proxy_pool/plugins/builtin/checkers/http_anonymity.py
Normal file
105
src/proxy_pool/plugins/builtin/checkers/http_anonymity.py
Normal file
@ -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,
|
||||||
|
)
|
||||||
35
uv.lock
generated
35
uv.lock
generated
@ -459,6 +459,21 @@ socks = [
|
|||||||
{ name = "socksio" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.11"
|
version = "3.11"
|
||||||
@ -693,6 +708,7 @@ dependencies = [
|
|||||||
{ name = "email-validator" },
|
{ name = "email-validator" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "httpx", extra = ["socks"] },
|
{ name = "httpx", extra = ["socks"] },
|
||||||
|
{ name = "httpx-socks" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "redis", extra = ["hiredis"] },
|
{ name = "redis", extra = ["hiredis"] },
|
||||||
@ -721,6 +737,7 @@ requires-dist = [
|
|||||||
{ name = "email-validator", specifier = ">=2.3.0" },
|
{ name = "email-validator", specifier = ">=2.3.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.135.1" },
|
{ name = "fastapi", specifier = ">=0.135.1" },
|
||||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.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", specifier = ">=2.12.5" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.13.1" },
|
{ name = "pydantic-settings", specifier = ">=2.13.1" },
|
||||||
{ name = "redis", extras = ["hiredis"], specifier = ">=5.3.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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "socksio"
|
name = "socksio"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user