File size: 4,655 Bytes
bb56382
971fa87
 
 
 
 
bb56382
971fa87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb56382
971fa87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb56382
971fa87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""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://<proxy>/forward?target=<encoded original URL>`` 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
    }