"""Panel-side wrapper that routes outbound HTTP through the Render proxy. Why --- HuggingFace Spaces share outbound IPs that several big services (Telegram Bot API, OpenAI, Anthropic, etc.) routinely blacklist. The panel itself sits on a HF Space — to call those APIs reliably we relay through the small service in ``render_proxy/``. Strategy -------- If ``RENDER_PROXY_URL`` + ``RENDER_PROXY_TOKEN`` are configured we transparently rewrite the host of outgoing requests: * the panel still builds normal URLs (``https://api.telegram.org/...``), * we intercept just before sending and rewrite the call to go through ``https:///forward?target=`` with a bearer token. Response is returned to the caller as if direct. For per-bot calls, the bot itself uses the values from env vars ``LLM_PROXY_URL`` / ``TELEGRAM_PROXY_URL`` (set by the panel during deploy). """ from __future__ import annotations import logging from typing import Any, Optional import httpx from config import PANEL_CONFIG log = logging.getLogger("panel.proxy-client") def render_proxy_enabled() -> bool: return bool(PANEL_CONFIG.render_proxy_url and PANEL_CONFIG.render_proxy_token) def _rewrite_via_proxy(url: str) -> str: """Return ``url`` rewritten through the proxy's /forward endpoint.""" from urllib.parse import quote proxy = PANEL_CONFIG.render_proxy_url.rstrip("/") return f"{proxy}/forward?target={quote(url, safe='')}" def _auth_headers() -> dict: return {"Authorization": f"Bearer {PANEL_CONFIG.render_proxy_token}"} def proxy_aware_request( method: str, url: str, *, headers: Optional[dict] = None, json: Any = None, data: Any = None, params: Optional[dict] = None, timeout: float = 60.0, follow_redirects: bool = True, ) -> httpx.Response: """Make an HTTP request, routed through the Render proxy when set. The caller passes a real external URL. If the proxy is enabled the URL is rewritten; the request body and headers are otherwise identical to a direct call. Caller receives a normal ``httpx.Response`` and can ``.raise_for_status()`` / ``.json()`` just like usual. """ headers = dict(headers or {}) # When proxying, we drop the original Host header — the upstream # is the proxy. httpx sets Host from the URL automatically. target = _rewrite_via_proxy(url) if render_proxy_enabled() else url if render_proxy_enabled(): # Carry the original target host in a header so the proxy # can log which logical backend was hit (useful in Render logs). from urllib.parse import urlparse original_host = urlparse(url).hostname or "" headers.setdefault("X-Panel-Original-Host", original_host) headers.update(_auth_headers()) return httpx.request( method=method, url=target, headers=headers, json=json, data=data, params=params, timeout=timeout, follow_redirects=follow_redirects, ) async def proxy_aware_async_request( method: str, url: str, *, headers: Optional[dict] = None, json: Any = None, data: Any = None, params: Optional[dict] = None, timeout: float = 60.0, follow_redirects: bool = True, ) -> httpx.Response: """Async variant of :func:`proxy_aware_request`.""" headers = dict(headers or {}) target = _rewrite_via_proxy(url) if render_proxy_enabled() else url if render_proxy_enabled(): from urllib.parse import urlparse headers.setdefault("X-Panel-Original-Host", urlparse(url).hostname or "") headers.update(_auth_headers()) async with httpx.AsyncClient(timeout=timeout, follow_redirects=follow_redirects) as client: return await client.request( method=method, url=target, headers=headers, json=json, data=data, params=params, ) def bot_env_extras() -> dict[str, str]: """Return the env-var dict the panel should write into each bot's ``.env`` file when ``RENDER_PROXY_URL`` is set. The bot uses ``TELEGRAM_PROXY_URL`` and ``LLM_PROXY_URL`` to call the proxy instead of Telegram/LLM providers directly. """ if not render_proxy_enabled(): return {} proxy = PANEL_CONFIG.render_proxy_url.rstrip("/") return { "RENDER_PROXY_URL": PANEL_CONFIG.render_proxy_url, "RENDER_PROXY_TOKEN": PANEL_CONFIG.render_proxy_token, "TELEGRAM_PROXY_URL": f"{proxy}/telegram", "LLM_PROXY_URL": f"{proxy}/llm/openai", # sensible default }