File size: 8,730 Bytes
32b00f5
 
 
cd2ba4f
32b00f5
8a43cd2
 
 
 
32b00f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a43cd2
 
32b00f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a43cd2
 
32b00f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a43cd2
32b00f5
cd2ba4f
 
 
 
 
 
0434636
cd2ba4f
 
 
 
 
 
fabc3f1
 
 
 
 
 
1c39ce7
fabc3f1
 
 
 
 
8a43cd2
 
 
 
 
 
 
 
 
fabc3f1
8a43cd2
 
 
32b00f5
8a43cd2
 
32b00f5
 
fabc3f1
8a43cd2
 
 
 
 
 
 
fabc3f1
 
8a43cd2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fabc3f1
 
 
 
 
 
 
 
 
 
8a43cd2
 
 
 
 
 
 
 
 
 
32b00f5
8a43cd2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32b00f5
 
8a43cd2
 
 
32b00f5
8a43cd2
32b00f5
 
8a43cd2
 
 
32b00f5
8a43cd2
 
 
 
 
 
cd2ba4f
91cdc25
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
"""Public Hugging Face ZeroGPU Gateway for PropNode Admin.

- Keeps the real codebase private in GitHub (`Gini-Eddie/propnode-admin`).
- Displays a public decoy Gradio interface at `/` to regular Hugging Face visitors.
- Unlocks the real PropNode Admin portal only via `/gate/<GATEWAY_KEY>` or authorized origins.

ZeroGPU only registers @spaces.GPU functions when the app starts through
`demo.launch()`. The gateway is therefore attached as ASGI middleware on Gradio's
own server (via `app_kwargs`), instead of running a separate Uvicorn server.
"""
import hashlib
import hmac
import logging
import os
import shutil
import subprocess
import sys
from http.cookies import SimpleCookie
from pathlib import Path

import huggingface_hub

# Compatibility shim if huggingface_hub >= 1.0 is present in the base container
if not hasattr(huggingface_hub, "HfFolder"):
    class _HfFolder:
        @classmethod
        def get_token(cls):
            return os.environ.get("HF_TOKEN")

        @classmethod
        def save_token(cls, token: str):
            pass

    huggingface_hub.HfFolder = _HfFolder

import gradio as gr
import spaces
import torch
from starlette.middleware import Middleware
from starlette.responses import RedirectResponse

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("propnode.gateway")

# ---------------------------------------------------------------------------
# 1. ZeroGPU Decoy Interface (shown to public Hugging Face visitors)
# ---------------------------------------------------------------------------
zero = torch.Tensor([0]).cuda()


@spaces.GPU
def greet(n: float) -> str:
    return f"Node telemetry ping: {zero.item() + float(n or 0):.1f} OK"


demo = gr.Interface(
    fn=greet,
    inputs=gr.Number(label="Diagnostic Signal", value=1),
    outputs=gr.Text(label="Gateway Response"),
    title="PropNode Telemetry Gateway",
    description="Internal compute node status check.",
)

# ---------------------------------------------------------------------------
# 2. Private Repository Loader
# ---------------------------------------------------------------------------
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "").strip()
GITHUB_REPO = os.environ.get("GITHUB_REPO", "Gini-Eddie/propnode-admin").strip()
GATEWAY_KEY = os.environ.get("GATEWAY_KEY", "").strip()
RUNTIME_DIR = Path("/tmp/propnode_admin_runtime")
GATE_COOKIE = "__pn_gate"


def _expected_key() -> str:
    return GATEWAY_KEY or os.environ.get("ADMIN_SECRET_KEY", "").strip()


def _gate_token() -> str:
    secret = _expected_key() or "fallback-unconfigured"
    return hashlib.sha256(f"propnode-gate:{secret}".encode()).hexdigest()


def _load_private_admin_app():
    missing = [
        name
        for name in ("GITHUB_TOKEN", "DATABASE_URL", "ADMIN_SECRET_KEY")
        if not os.environ.get(name, "").strip()
    ]
    if missing:
        logger.warning(
            "Gateway running in decoy-only mode. Configure Space Secrets to enable backend: %s",
            ", ".join(missing),
        )
        return None

    if RUNTIME_DIR.exists():
        shutil.rmtree(RUNTIME_DIR)

    repo_url = f"https://x-access-token:{GITHUB_TOKEN}@github.com/{GITHUB_REPO}.git"
    logger.info("Fetching private repository %s into runtime directory...", GITHUB_REPO)
    result = subprocess.run(
        ["git", "clone", "--depth", "1", repo_url, str(RUNTIME_DIR)],
        capture_output=True,
        text=True,
        check=False,
    )
    if result.returncode != 0:
        # stderr may contain the token-bearing URL; do not log it verbatim.
        logger.error("Failed to clone private repository (code %s).", result.returncode)
        return None

    sys.path.insert(0, str(RUNTIME_DIR))
    try:
        from backend.app.main import app as private_app
        logger.info("Private PropNode Admin backend mounted successfully.")
        return private_app
    except Exception as exc:
        logger.error("Failed to import private backend: %s", exc)
        return None


admin_app = _load_private_admin_app()

# ---------------------------------------------------------------------------
# 3. Gateway Middleware (runs in front of Gradio's own server)
# ---------------------------------------------------------------------------
GRADIO_PREFIXES = (
    "/gradio_api",
    "/queue/",
    "/config",
    "/info",
    "/theme.css",
    "/custom.css",
    "/file=",
    "/heartbeat/",
    "/manifest.json",
)


def _is_gradio_route(path: str) -> bool:
    if path.startswith(GRADIO_PREFIXES):
        return True
    if path.startswith("/assets/"):
        # Route `/assets/logo.png` and `/assets/fonts/*` to PropNode Admin;
        # route Gradio's own hashed JS/CSS bundle files in `/assets/` to Gradio.
        if path in {"/assets/logo.png", "/assets/icon-logo.png"} or path.startswith("/assets/fonts/"):
            return False
        return True
    return False


def _headers(scope: dict) -> dict:
    return {k.decode("latin1").lower(): v.decode("latin1") for k, v in scope.get("headers", [])}


def _is_authorized(scope: dict) -> bool:
    if admin_app is None or scope.get("type") != "http":
        return False

    path: str = scope.get("path", "")
    if _is_gradio_route(path):
        return False

    headers = _headers(scope)

    # Never expose the admin app inside the Hugging Face Space iframe
    if headers.get("sec-fetch-dest") == "iframe":
        return False

    # 1. Browser gateway cookie set via /gate/<GATEWAY_KEY> or active admin session cookie
    raw_cookie = headers.get("cookie", "")
    if raw_cookie:
        cookie = SimpleCookie()
        try:
            cookie.load(raw_cookie)
            if GATE_COOKIE in cookie and hmac.compare_digest(cookie[GATE_COOKIE].value, _gate_token()):
                return True
            if path.startswith("/api/") and "propnode_admin_session" in cookie:
                return True
        except Exception:
            pass

    # 2. Explicit X-Gateway-Key header (from a reverse proxy)
    expected = _expected_key()
    header_key = headers.get("x-gateway-key", "")
    if expected and header_key and hmac.compare_digest(header_key, expected):
        return True

    # 3. API call from an explicitly allowed external domain (e.g. https://admin.propnode.app)
    allowed_origins = {
        o.strip().rstrip("/")
        for o in os.environ.get("ADMIN_ALLOWED_ORIGINS", "").split(",")
        if o.strip() and not o.strip().endswith(".hf.space")
    }
    if path.startswith("/api/") and allowed_origins:
        origin = headers.get("origin", "").rstrip("/")
        referer = headers.get("referer", "")
        forwarded_host = headers.get("x-forwarded-host", "").strip()
        if origin and origin in allowed_origins:
            return True
        if any(referer.startswith(o + "/") or referer == o for o in allowed_origins):
            return True
        if forwarded_host and f"https://{forwarded_host}" in allowed_origins:
            return True

    return False


class GatewayMiddleware:
    """Pure ASGI middleware: routes authorized traffic to PropNode Admin, the rest to Gradio."""

    def __init__(self, app):
        self.app = app  # Gradio's ASGI app

    async def __call__(self, scope, receive, send):
        if scope.get("type") == "http" and scope.get("path", "").startswith("/gate/"):
            key = scope["path"][len("/gate/"):].strip("/")
            expected = _expected_key()
            if admin_app is None or not expected or not hmac.compare_digest(key, expected):
                await RedirectResponse(url="/", status_code=302)(scope, receive, send)
                return
            response = RedirectResponse(url="/login.html", status_code=302)
            response.set_cookie(
                GATE_COOKIE,
                _gate_token(),
                httponly=True,
                secure=True,
                samesite="lax",
                path="/",
                max_age=86400 * 30,
            )
            await response(scope, receive, send)
            return

        if _is_authorized(scope):
            await admin_app(scope, receive, send)
            return

        await self.app(scope, receive, send)


# ---------------------------------------------------------------------------
# 4. Launch through Gradio so ZeroGPU detects the @spaces.GPU function
# ---------------------------------------------------------------------------
if __name__ == "__main__":
    demo.launch(
        server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
        server_port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)),
        ssr_mode=False,
        app_kwargs={"middleware": [Middleware(GatewayMiddleware)]},
    )

# Deployed: 2026-11-08 21:24:00 UTC