Files
chatballs/apps/backend/hub_platform/integrations/proxy.py
T
Andrey 4b112ae3f8 feat(integrations): SOCKS5-прокси + единый opener (DRY)
Раньше ProxyHandler-логика дублировалась в openrouter/checks/transports и
поддерживала только http/https. Telegram/SOCKS-сервисы требуют SOCKS5 — теперь
единый build_opener поддерживает http/https/socks5/socks5h во всех интеграциях.

- integrations/proxy.py (новый): build_opener(proxy_url) — http/https через
  stdlib ProxyHandler; socks5/socks5h через кастомный _SocksProxyHandler (PySocks,
  изолированный от остального процесса: переопределённый .connect, не глобальный
  monkeypatch socket). socks5h → DNS через прокси (rdns=True). Без PySocks при
  SOCKS-схеме — ValueError с явным сообщением.
- openrouter.py / checks.py / transports/base.py: убрано дублирование ProxyHandler,
  все используют build_opener (chat, embeddings, check_*, MAX/Telegram transport).
- requirements.txt: PySocks>=1.7.
- IntegrationForm.tsx: placeholder упоминает socks5://.
- tests.py: build_opener socks-схема строит handler; http → ProxyHandler; без
  PySocks → ValueError; check_*/OpenRouter прокидывают proxy_url (вкл socks5).
2026-07-10 04:19:03 +03:00

108 lines
4.2 KiB
Python

"""Единый HTTP-opener с поддержкой прокси для всех интеграций (ADR-HUB-0020).
Поддерживаемые схемы proxy_url:
- http://[user:pass@]host:port
- https://[user:pass@]host:port
- socks5://[user:pass@]host:port (DNS резолвится локально)
- socks5h://[user:pass@]host:port (DNS через прокси, rdns=True)
HTTP/HTTPS работают через stdlib ProxyHandler. SOCKS5 требует PySocks (`socks`):
если зависимость отсутствует — SOCKS-схема даёт ValueError, чтобы владелец видел
явную ошибку, а не молчаливый обход прокси.
"""
from __future__ import annotations
import http.client
import ssl
import urllib.parse
import urllib.request
SOCKS_SCHEMES = ("socks5", "socks5h")
def build_opener(proxy_url: str):
"""urllib opener, проксирующий http/https/socks5 запросы. Пустой proxy_url → без прокси."""
if not proxy_url:
return urllib.request.build_opener()
scheme = urllib.parse.urlparse(proxy_url).scheme.lower()
if scheme in SOCKS_SCHEMES:
return urllib.request.build_opener(_SocksProxyHandler(proxy_url))
return urllib.request.build_opener(urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url}))
class _SocksProxyHandler(urllib.request.HTTPHandler, urllib.request.HTTPSHandler):
"""urllib handler, устанавливающий TCP-соединение через SOCKS5 (PySocks).
PySocks патчит socket.create_connection глобально; мы вместо этого строим
http.client-соединение с переопределённым .connect(), который туннелирует
через SOCKS — это изолирует прокси от остального процесса.
"""
def __init__(self, proxy_url: str):
super().__init__()
self._socks = _import_socks()
parsed = urllib.parse.urlparse(proxy_url)
self._proxy_type = self._socks.PROXY_TYPE_SOCKS5
self._proxy_host = parsed.hostname or ""
self._proxy_port = parsed.port or 1080
self._username = parsed.username or None
self._password = parsed.password or None
# socks5h → DNS резолвится на стороне прокси (rdns=True).
self._rdns = parsed.scheme.lower() == "socks5h"
def http_open(self, req):
return self.do_open(self._make_http_conn, req)
def https_open(self, req):
return self.do_open(self._make_https_conn, req)
def _make_http_conn(self, req, timeout=30):
host = req.host
port = req.port or 80
return _SocksHTTPConnection(host, port, self, timeout=timeout)
def _make_https_conn(self, req, timeout=30):
host = req.host
port = req.port or 443
return _SocksHTTPSConnection(host, port, self, timeout=timeout, context=ssl.create_default_context())
def open_socket(self, host: str, port: int):
return self._socks.create_connection(
(host, port),
proxy_type=self._proxy_type,
proxy_addr=self._proxy_host,
proxy_port=self._proxy_port,
proxy_username=self._username,
proxy_password=self._password,
rdns=self._rdns,
)
def _import_socks():
try:
import socks
return socks
except ImportError as error:
raise ValueError("Для SOCKS-прокси нужна зависимость PySocks (pip install PySocks)") from error
class _SocksHTTPConnection(http.client.HTTPConnection):
def __init__(self, host, port, handler, timeout=30):
super().__init__(host, port, timeout=timeout)
self._handler = handler
def connect(self):
self.sock = self._handler.open_socket(self.host, self.port)
class _SocksHTTPSConnection(http.client.HTTPSConnection):
def __init__(self, host, port, handler, timeout=30, context=None):
super().__init__(host, port, timeout=timeout, context=context)
self._handler = handler
def connect(self):
sock = self._handler.open_socket(self.host, self.port)
self.sock = self._context.wrap_socket(sock, server_hostname=self.host)