grok2api / app /dataplane /reverse /transport /websocket.py
FUCAT's picture
Deploy grok2api to HF Spaces (Docker)
7e55e53
Raw
History Blame Contribute Delete
4.86 kB
"""WebSocket transport with proxy and SOCKS support."""
import ssl
from typing import Any, Awaitable, Callable, Mapping, Optional
from urllib.parse import urlparse
import aiohttp
import certifi
from aiohttp_socks import ProxyConnector
from app.platform.logging.logger import logger
from app.platform.config.snapshot import get_config
from app.control.proxy.models import ProxyLease
def _ssl_ctx() -> ssl.SSLContext:
ctx = ssl.create_default_context()
ctx.load_verify_locations(certifi.where())
return ctx
def _normalize_socks(proxy_url: str) -> tuple[str, Optional[bool]]:
scheme = urlparse(proxy_url).scheme.lower()
rdns: Optional[bool] = None
base = scheme
if scheme == "socks":
base, rdns = "socks5", True
elif scheme == "socks5h":
base, rdns = "socks5", True
elif scheme == "socks4a":
base, rdns = "socks4", True
if base != scheme:
proxy_url = proxy_url.replace(f"{scheme}://", f"{base}://", 1)
return proxy_url, rdns
def _build_connector(
proxy_url: str,
ssl_ctx: ssl.SSLContext,
) -> tuple[aiohttp.BaseConnector, str | None]:
if not proxy_url:
return aiohttp.TCPConnector(ssl=ssl_ctx), None
scheme = urlparse(proxy_url).scheme.lower()
if scheme.startswith("socks"):
normalized, rdns = _normalize_socks(proxy_url)
kwargs: dict = {"ssl": ssl_ctx}
if rdns is not None:
kwargs["rdns"] = rdns
logger.debug("websocket connector selected: proxy_type=socks proxy_url={}", proxy_url)
return ProxyConnector.from_url(normalized, **kwargs), None
logger.debug("websocket connector selected: proxy_type=http proxy_url={}", proxy_url)
return aiohttp.TCPConnector(ssl=ssl_ctx), proxy_url
class WebSocketConnection:
"""Wraps aiohttp WebSocketResponse with lifecycle cleanup."""
def __init__(
self,
session: aiohttp.ClientSession,
ws: aiohttp.ClientWebSocketResponse,
*,
on_close: Callable[[], Awaitable[None]] | None = None,
) -> None:
self.session = session
self.ws = ws
self._on_close = on_close
async def close(self) -> None:
if not self.ws.closed:
await self.ws.close()
await self.session.close()
if self._on_close:
await self._on_close()
self._on_close = None
async def __aenter__(self) -> aiohttp.ClientWebSocketResponse:
return self.ws
async def __aexit__(self, *_: Any) -> None:
await self.close()
class WebSocketClient:
"""Establish WebSocket connections through optional proxy."""
def __init__(self, proxy: str | None = None) -> None:
self._proxy_override = proxy
self._ssl = _ssl_ctx()
async def connect(
self,
url: str,
headers: Optional[Mapping[str, str]] = None,
timeout: Optional[float] = None,
ws_kwargs: Optional[Mapping[str, Any]] = None,
*,
lease: ProxyLease | None = None,
on_close: Callable[[], Awaitable[None]] | None = None,
) -> WebSocketConnection:
proxy_url = self._proxy_override or (lease.proxy_url if lease else "")
connector, http_proxy = _build_connector(proxy_url, self._ssl)
cfg = get_config()
total_s = float(timeout) if timeout is not None else cfg.get_float("voice.timeout", 120.0)
client_timeout = aiohttp.ClientTimeout(total=total_s)
session = aiohttp.ClientSession(connector=connector, timeout=client_timeout)
try:
extra: dict[str, Any] = dict(ws_kwargs or {})
skip_ssl = cfg.get_bool("proxy.egress.skip_ssl_verify", False) and bool(proxy_url)
if skip_ssl and urlparse(proxy_url).scheme.lower() == "https":
proxy_ssl = ssl.create_default_context()
proxy_ssl.check_hostname = False
proxy_ssl.verify_mode = ssl.CERT_NONE
try:
ws = await session.ws_connect(
url, headers=headers, proxy=http_proxy,
ssl=self._ssl, proxy_ssl=proxy_ssl, **extra,
)
except TypeError:
ws = await session.ws_connect(
url, headers=headers, proxy=http_proxy,
ssl=self._ssl, **extra,
)
else:
ws = await session.ws_connect(
url, headers=headers, proxy=http_proxy,
ssl=self._ssl, **extra,
)
return WebSocketConnection(session, ws, on_close=on_close)
except Exception:
await session.close()
raise
__all__ = ["WebSocketClient", "WebSocketConnection"]