grok2api / app /dataplane /reverse /transport /websocket.py
looknicemm1's picture
Deploy grok2api (jiujiu532) to HF Space: port 7860, bucket persistence at /app/data
48d895c verified
Raw
History Blame Contribute Delete
5 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"]