PropNode Deploy commited on
Commit
8a43cd2
·
1 Parent(s): 0434636

Deploy PropNode ZeroGPU gateway (2026-11-01 13:26:08 UTC)

Browse files
Files changed (1) hide show
  1. app.py +91 -94
app.py CHANGED
@@ -3,6 +3,10 @@
3
  - Keeps the real codebase private in GitHub (`Gini-Eddie/propnode-admin`).
4
  - Displays a public decoy Gradio interface at `/` to regular Hugging Face visitors.
5
  - Unlocks the real PropNode Admin portal only via `/gate/<GATEWAY_KEY>` or authorized origins.
 
 
 
 
6
  """
7
  import hashlib
8
  import hmac
@@ -32,9 +36,8 @@ if not hasattr(huggingface_hub, "HfFolder"):
32
  import gradio as gr
33
  import spaces
34
  import torch
35
- import uvicorn
36
- from fastapi import FastAPI
37
- from fastapi.responses import RedirectResponse
38
 
39
  logging.basicConfig(level=logging.INFO)
40
  logger = logging.getLogger("propnode.gateway")
@@ -102,7 +105,8 @@ def _load_private_admin_app():
102
  check=False,
103
  )
104
  if result.returncode != 0:
105
- logger.error("Failed to clone private repository (code %s): %s", result.returncode, result.stderr)
 
106
  return None
107
 
108
  sys.path.insert(0, str(RUNTIME_DIR))
@@ -118,36 +122,7 @@ def _load_private_admin_app():
118
  admin_app = _load_private_admin_app()
119
 
120
  # ---------------------------------------------------------------------------
121
- # 3. Public Decoy + Secret Gate FastAPI App (Mounted at `/` for Gradio 5)
122
- # ---------------------------------------------------------------------------
123
- public_app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
124
-
125
-
126
- @public_app.get("/gate/{key}")
127
- async def unlock_gateway(key: str):
128
- """Secret entry URL that sets an HttpOnly pass cookie and opens the admin login."""
129
- expected = _expected_key()
130
- if not expected or not hmac.compare_digest(key, expected):
131
- return RedirectResponse(url="/", status_code=302)
132
-
133
- response = RedirectResponse(url="/login.html", status_code=302)
134
- response.set_cookie(
135
- GATE_COOKIE,
136
- _gate_token(),
137
- httponly=True,
138
- secure=True,
139
- samesite="lax",
140
- path="/",
141
- max_age=86400 * 30,
142
- )
143
- return response
144
-
145
-
146
- public_app = gr.mount_gradio_app(public_app, demo, path="/")
147
-
148
-
149
- # ---------------------------------------------------------------------------
150
- # 4. Zero-Leakage ASGI Gateway Dispatcher
151
  # ---------------------------------------------------------------------------
152
  GRADIO_PREFIXES = (
153
  "/gradio_api",
@@ -163,74 +138,96 @@ GRADIO_PREFIXES = (
163
  )
164
 
165
 
166
- class GatewayRouter:
167
- def __init__(self, decoy_asgi, private_asgi):
168
- self.decoy_asgi = decoy_asgi
169
- self.private_asgi = private_asgi
170
-
171
- def _is_authorized(self, scope: dict) -> bool:
172
- if self.private_asgi is None or scope.get("type") != "http":
173
- return False
174
-
175
- path: str = scope.get("path", "")
176
- if path.startswith("/gate/") or path.startswith(GRADIO_PREFIXES):
177
- return False
178
-
179
- headers = {
180
- k.decode("latin1").lower(): v.decode("latin1")
181
- for k, v in scope.get("headers", [])
182
- }
183
-
184
- # Never expose the admin app inside the Hugging Face Space iframe
185
- if headers.get("sec-fetch-dest") == "iframe":
186
- return False
187
-
188
- # 1. Check browser gateway cookie set via /gate/<GATEWAY_KEY>
189
- raw_cookie = headers.get("cookie", "")
190
- if raw_cookie:
191
- cookie = SimpleCookie()
192
- try:
193
- cookie.load(raw_cookie)
194
- if GATE_COOKIE in cookie:
195
- val = cookie[GATE_COOKIE].value
196
- if hmac.compare_digest(val, _gate_token()):
197
- return True
198
- except Exception:
199
- pass
200
-
201
- # 2. Check explicit X-Gateway-Key header (from a reverse proxy)
202
- expected = _expected_key()
203
- header_key = headers.get("x-gateway-key", "")
204
- if expected and header_key and hmac.compare_digest(header_key, expected):
205
- return True
206
-
207
- # 3. Check allowed external domain origin on /api/ calls
208
- origin = headers.get("origin", "").rstrip("/")
209
- allowed_origins = {
210
- o.strip().rstrip("/")
211
- for o in os.environ.get("ADMIN_ALLOWED_ORIGINS", "").split(",")
212
- if o.strip() and not o.strip().endswith(".hf.space")
213
- }
214
- if origin and origin in allowed_origins and path.startswith("/api/"):
215
- return True
216
 
 
 
217
  return False
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  async def __call__(self, scope, receive, send):
220
- if self._is_authorized(scope):
221
- await self.private_asgi(scope, receive, send)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  return
223
 
224
- await self.decoy_asgi(scope, receive, send)
 
 
225
 
 
226
 
227
- app = GatewayRouter(public_app, admin_app)
228
 
 
 
 
229
  if __name__ == "__main__":
230
- # Hugging Face routes public traffic to GRADIO_SERVER_PORT (7860).
231
- # Do NOT use $PORT here: on ZeroGPU it points to an internal port (7861) already in use.
232
- port = int(os.environ.get("GRADIO_SERVER_PORT", 7860))
233
- host = os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0")
234
- uvicorn.run(app, host=host, port=port, reload=False)
 
235
 
236
- # Deployed: 2026-11-01 13:08:42 UTC
 
3
  - Keeps the real codebase private in GitHub (`Gini-Eddie/propnode-admin`).
4
  - Displays a public decoy Gradio interface at `/` to regular Hugging Face visitors.
5
  - Unlocks the real PropNode Admin portal only via `/gate/<GATEWAY_KEY>` or authorized origins.
6
+
7
+ ZeroGPU only registers @spaces.GPU functions when the app starts through
8
+ `demo.launch()`. The gateway is therefore attached as ASGI middleware on Gradio's
9
+ own server (via `app_kwargs`), instead of running a separate Uvicorn server.
10
  """
11
  import hashlib
12
  import hmac
 
36
  import gradio as gr
37
  import spaces
38
  import torch
39
+ from starlette.middleware import Middleware
40
+ from starlette.responses import RedirectResponse
 
41
 
42
  logging.basicConfig(level=logging.INFO)
43
  logger = logging.getLogger("propnode.gateway")
 
105
  check=False,
106
  )
107
  if result.returncode != 0:
108
+ # stderr may contain the token-bearing URL; do not log it verbatim.
109
+ logger.error("Failed to clone private repository (code %s).", result.returncode)
110
  return None
111
 
112
  sys.path.insert(0, str(RUNTIME_DIR))
 
122
  admin_app = _load_private_admin_app()
123
 
124
  # ---------------------------------------------------------------------------
125
+ # 3. Gateway Middleware (runs in front of Gradio's own server)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  # ---------------------------------------------------------------------------
127
  GRADIO_PREFIXES = (
128
  "/gradio_api",
 
138
  )
139
 
140
 
141
+ def _headers(scope: dict) -> dict:
142
+ return {k.decode("latin1").lower(): v.decode("latin1") for k, v in scope.get("headers", [])}
143
+
144
+
145
+ def _is_authorized(scope: dict) -> bool:
146
+ if admin_app is None or scope.get("type") != "http":
147
+ return False
148
+
149
+ path: str = scope.get("path", "")
150
+ if path.startswith(GRADIO_PREFIXES):
151
+ return False
152
+
153
+ headers = _headers(scope)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ # Never expose the admin app inside the Hugging Face Space iframe
156
+ if headers.get("sec-fetch-dest") == "iframe":
157
  return False
158
 
159
+ # 1. Browser gateway cookie set via /gate/<GATEWAY_KEY>
160
+ raw_cookie = headers.get("cookie", "")
161
+ if raw_cookie:
162
+ cookie = SimpleCookie()
163
+ try:
164
+ cookie.load(raw_cookie)
165
+ if GATE_COOKIE in cookie and hmac.compare_digest(cookie[GATE_COOKIE].value, _gate_token()):
166
+ return True
167
+ except Exception:
168
+ pass
169
+
170
+ # 2. Explicit X-Gateway-Key header (from a reverse proxy)
171
+ expected = _expected_key()
172
+ header_key = headers.get("x-gateway-key", "")
173
+ if expected and header_key and hmac.compare_digest(header_key, expected):
174
+ return True
175
+
176
+ # 3. API call from an explicitly allowed external domain (e.g. https://admin.propnode.app)
177
+ origin = headers.get("origin", "").rstrip("/")
178
+ allowed_origins = {
179
+ o.strip().rstrip("/")
180
+ for o in os.environ.get("ADMIN_ALLOWED_ORIGINS", "").split(",")
181
+ if o.strip() and not o.strip().endswith(".hf.space")
182
+ }
183
+ if origin and origin in allowed_origins and path.startswith("/api/"):
184
+ return True
185
+
186
+ return False
187
+
188
+
189
+ class GatewayMiddleware:
190
+ """Pure ASGI middleware: routes authorized traffic to PropNode Admin, the rest to Gradio."""
191
+
192
+ def __init__(self, app):
193
+ self.app = app # Gradio's ASGI app
194
+
195
  async def __call__(self, scope, receive, send):
196
+ if scope.get("type") == "http" and scope.get("path", "").startswith("/gate/"):
197
+ key = scope["path"][len("/gate/"):].strip("/")
198
+ expected = _expected_key()
199
+ if admin_app is None or not expected or not hmac.compare_digest(key, expected):
200
+ await RedirectResponse(url="/", status_code=302)(scope, receive, send)
201
+ return
202
+ response = RedirectResponse(url="/login.html", status_code=302)
203
+ response.set_cookie(
204
+ GATE_COOKIE,
205
+ _gate_token(),
206
+ httponly=True,
207
+ secure=True,
208
+ samesite="lax",
209
+ path="/",
210
+ max_age=86400 * 30,
211
+ )
212
+ await response(scope, receive, send)
213
  return
214
 
215
+ if _is_authorized(scope):
216
+ await admin_app(scope, receive, send)
217
+ return
218
 
219
+ await self.app(scope, receive, send)
220
 
 
221
 
222
+ # ---------------------------------------------------------------------------
223
+ # 4. Launch through Gradio so ZeroGPU detects the @spaces.GPU function
224
+ # ---------------------------------------------------------------------------
225
  if __name__ == "__main__":
226
+ demo.launch(
227
+ server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
228
+ server_port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)),
229
+ ssr_mode=False,
230
+ app_kwargs={"middleware": [Middleware(GatewayMiddleware)]},
231
+ )
232
 
233
+ # Deployed: 2026-11-01 13:26:08 UTC