anurag008w commited on
Commit
35e67fe
·
1 Parent(s): 6113d21

Implement space privacy detection and redirection

Browse files

Added functionality to detect space privacy via HF API at startup and cache the result. Updated request handling to redirect users based on space privacy status.

Files changed (1) hide show
  1. health-server.js +102 -2
health-server.js CHANGED
@@ -1,5 +1,6 @@
1
  // Single public entrypoint for HF Spaces: dashboard + reverse proxy to OpenClaw + JupyterLab.
2
  const http = require("http");
 
3
  const fs = require("fs");
4
  const net = require("net");
5
 
@@ -52,6 +53,50 @@ function deriveHfSpaceUrl() {
52
  return "";
53
  }
54
  const HF_SPACE_URL = deriveHfSpaceUrl();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
56
  "/tmp/huggingclaw-cloudflare-keepalive-status.json";
57
 
@@ -137,9 +182,24 @@ function renderDashboard(data) {
137
  tile({ title: "Model", value: `<code>${escapeHtml(LLM_MODEL)}</code>`, detail: "Primary LLM configured", tone: "neutral" }),
138
  tile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public port ${PORT}`, tone: "neutral" }),
139
  tile({ title: "Telegram", value: badge(TELEGRAM_ENABLED ? "Enabled" : "Disabled", TELEGRAM_ENABLED ? "ok" : "neutral"), detail: TELEGRAM_ENABLED ? "Bot channel active" : "Not configured", tone: TELEGRAM_ENABLED ? "ok" : "neutral" }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  tile({ title: "Backup", value: badge(syncStatus.toUpperCase(), syncTone), detail: escapeHtml(data.sync?.message || "No status yet"), tone: syncTone, meta: data.sync?.timestamp ? `<span class="local-time" data-iso="${data.sync.timestamp}"></span>` : "" }),
141
  tile({ title: "Keep Awake", value: badge(kaConf ? "CF Cron" : kaStatus.toUpperCase(), kaTone), detail: kaConf ? `Pinging <code>${escapeHtml(data.keepalive?.targetUrl || "/health")}</code>` : process.env.CLOUDFLARE_WORKERS_TOKEN ? "Worker pending or failed" : "Not configured", tone: kaTone }),
142
- ];
143
 
144
  if (JUPYTER_ENABLED) {
145
  tiles.push(tile({ title: "Terminal", value: badge(data.jupyterReady ? "Online" : "Starting…", data.jupyterReady ? "ok" : "warn"), detail: `JupyterLab at <a href="${JUPYTER_BASE}/" style="color:inherit">${JUPYTER_BASE}/</a>`, tone: data.jupyterReady ? "ok" : "warn" }));
@@ -198,11 +258,12 @@ function renderDashboard(data) {
198
  const inEmbeddedApp = (() => { try { return window.top !== window.self; } catch { return true; } })();
199
  const isDirectHfSpaceHost = /\.hf\.space$/i.test(window.location.hostname);
200
  const HF_SPACE_URL = ${JSON.stringify(HF_SPACE_URL)};
 
201
  // ── Private Space Guard ──
202
  // Direct .hf.space access outside the HF App iframe has no valid session cookie
203
  // for private spaces — HF CDN returns 404 before the request reaches the container.
204
  // Redirect users to huggingface.co/spaces/... which authenticates them properly.
205
- if (isDirectHfSpaceHost && !inEmbeddedApp && HF_SPACE_URL) {
206
  const notice = document.createElement('div');
207
  notice.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#08080f;color:#f6f4ff;font-family:sans-serif;flex-direction:column;gap:16px;z-index:9999';
208
  notice.innerHTML = '<span style="font-size:1.1rem">🔒 Private Space &mdash; Redirecting&hellip;</span><a href="' + HF_SPACE_URL + '" style="color:#a5b4fc;font-size:.85rem">Click here if not redirected</a>';
@@ -379,7 +440,23 @@ const server = http.createServer(async (req, res) => {
379
  return res.end("SPACE_ID not configured.");
380
  }
381
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  if (pathname === "/env-builder" || pathname === "/env-builder/") {
 
 
 
 
383
  res.writeHead(200, { "Content-Type": "text/html" });
384
  return res.end(renderEnvBuilder());
385
  }
@@ -396,6 +473,10 @@ const server = http.createServer(async (req, res) => {
396
  }
397
 
398
  if (pathname === "/" || pathname === "/dashboard") {
 
 
 
 
399
  const [gatewayReady, jupyterReady] = await Promise.all([
400
  probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
401
  JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/login`) : Promise.resolve(false),
@@ -410,6 +491,10 @@ const server = http.createServer(async (req, res) => {
410
  res.writeHead(404, { "Content-Type": "application/json" });
411
  return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Set DEV_MODE=true to enable /terminal/." }));
412
  }
 
 
 
 
413
  return proxyHTTP(req, res, JUPYTER_HOST, JUPYTER_PORT, {
414
  publicPrefix: JUPYTER_BASE,
415
  // Jupyter is started with --ServerApp.base_url=/terminal/, so keep the
@@ -422,6 +507,10 @@ const server = http.createServer(async (req, res) => {
422
  // OpenClaw Control UI mounted under /app. Retry without the mount prefix on
423
  // 404 so deployments keep working across OpenClaw basePath behavior changes.
424
  if (pathname === APP_BASE || pathname.startsWith(APP_BASE + "/")) {
 
 
 
 
425
  return proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT, {
426
  publicPrefix: APP_BASE,
427
  stripPrefix: APP_BASE,
@@ -429,7 +518,18 @@ const server = http.createServer(async (req, res) => {
429
  });
430
  }
431
 
 
 
 
 
 
 
 
432
  // OpenClaw gateway API/static fallback (everything else)
 
 
 
 
433
  proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT);
434
  });
435
 
 
1
  // Single public entrypoint for HF Spaces: dashboard + reverse proxy to OpenClaw + JupyterLab.
2
  const http = require("http");
3
+ const https = require("https");
4
  const fs = require("fs");
5
  const net = require("net");
6
 
 
53
  return "";
54
  }
55
  const HF_SPACE_URL = deriveHfSpaceUrl();
56
+
57
+ // Auto-detect space privacy via HF API at startup.
58
+ // Caches result so every request doesn't hit the API.
59
+ let SPACE_IS_PRIVATE = false;
60
+ async function detectSpacePrivacy() {
61
+ if (!SPACE_ID) return;
62
+ try {
63
+ const token = (process.env.HF_TOKEN || "").trim();
64
+ const reqOptions = {
65
+ hostname: "huggingface.co",
66
+ path: `/api/spaces/${SPACE_ID}`,
67
+ method: "GET",
68
+ headers: Object.assign(
69
+ { "User-Agent": "HuggingClaw/health-server" },
70
+ token ? { Authorization: `Bearer ${token}` } : {}
71
+ ),
72
+ };
73
+ await new Promise((resolve) => {
74
+ const r = https.request(reqOptions, (res) => {
75
+ let body = "";
76
+ res.on("data", (chunk) => { body += chunk; });
77
+ res.on("end", () => {
78
+ try {
79
+ if (res.statusCode === 200) {
80
+ const data = JSON.parse(body);
81
+ SPACE_IS_PRIVATE = data.private === true;
82
+ } else if (res.statusCode === 404 && !token) {
83
+ // 404 with no token usually means private space
84
+ SPACE_IS_PRIVATE = true;
85
+ }
86
+ } catch {}
87
+ resolve();
88
+ });
89
+ });
90
+ r.on("error", resolve);
91
+ r.setTimeout(5000, () => { r.destroy(); resolve(); });
92
+ r.end();
93
+ });
94
+ console.log(`[health-server] Space privacy detected: ${SPACE_IS_PRIVATE ? "private" : "public"}`);
95
+ } catch {
96
+ // Network error — default to false (safe)
97
+ }
98
+ }
99
+ detectSpacePrivacy();
100
  const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
101
  "/tmp/huggingclaw-cloudflare-keepalive-status.json";
102
 
 
182
  tile({ title: "Model", value: `<code>${escapeHtml(LLM_MODEL)}</code>`, detail: "Primary LLM configured", tone: "neutral" }),
183
  tile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public port ${PORT}`, tone: "neutral" }),
184
  tile({ title: "Telegram", value: badge(TELEGRAM_ENABLED ? "Enabled" : "Disabled", TELEGRAM_ENABLED ? "ok" : "neutral"), detail: TELEGRAM_ENABLED ? "Bot channel active" : "Not configured", tone: TELEGRAM_ENABLED ? "ok" : "neutral" }),
185
+ ];
186
+
187
+ if (WHATSAPP_ENABLED) {
188
+ const wa = data.whatsapp || {};
189
+ const waTone = wa.connected ? "ok" : wa.pairing ? "warn" : "neutral";
190
+ const waLabel = wa.connected ? "Connected" : wa.pairing ? "Pairing…" : wa.configured ? "Waiting" : "Disabled";
191
+ const waDetail = wa.connected
192
+ ? "WhatsApp channel active"
193
+ : wa.pairing
194
+ ? "Scan QR code in Control UI → WhatsApp"
195
+ : "WhatsApp not yet connected";
196
+ tiles.push(tile({ title: "WhatsApp", value: badge(waLabel, waTone), detail: waDetail, tone: waTone }));
197
+ }
198
+
199
+ tiles.push(
200
  tile({ title: "Backup", value: badge(syncStatus.toUpperCase(), syncTone), detail: escapeHtml(data.sync?.message || "No status yet"), tone: syncTone, meta: data.sync?.timestamp ? `<span class="local-time" data-iso="${data.sync.timestamp}"></span>` : "" }),
201
  tile({ title: "Keep Awake", value: badge(kaConf ? "CF Cron" : kaStatus.toUpperCase(), kaTone), detail: kaConf ? `Pinging <code>${escapeHtml(data.keepalive?.targetUrl || "/health")}</code>` : process.env.CLOUDFLARE_WORKERS_TOKEN ? "Worker pending or failed" : "Not configured", tone: kaTone }),
202
+ );
203
 
204
  if (JUPYTER_ENABLED) {
205
  tiles.push(tile({ title: "Terminal", value: badge(data.jupyterReady ? "Online" : "Starting…", data.jupyterReady ? "ok" : "warn"), detail: `JupyterLab at <a href="${JUPYTER_BASE}/" style="color:inherit">${JUPYTER_BASE}/</a>`, tone: data.jupyterReady ? "ok" : "warn" }));
 
258
  const inEmbeddedApp = (() => { try { return window.top !== window.self; } catch { return true; } })();
259
  const isDirectHfSpaceHost = /\.hf\.space$/i.test(window.location.hostname);
260
  const HF_SPACE_URL = ${JSON.stringify(HF_SPACE_URL)};
261
+ const SPACE_IS_PRIVATE = ${JSON.stringify(SPACE_IS_PRIVATE)};
262
  // ── Private Space Guard ──
263
  // Direct .hf.space access outside the HF App iframe has no valid session cookie
264
  // for private spaces — HF CDN returns 404 before the request reaches the container.
265
  // Redirect users to huggingface.co/spaces/... which authenticates them properly.
266
+ if (SPACE_IS_PRIVATE && isDirectHfSpaceHost && !inEmbeddedApp && HF_SPACE_URL) {
267
  const notice = document.createElement('div');
268
  notice.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#08080f;color:#f6f4ff;font-family:sans-serif;flex-direction:column;gap:16px;z-index:9999';
269
  notice.innerHTML = '<span style="font-size:1.1rem">🔒 Private Space &mdash; Redirecting&hellip;</span><a href="' + HF_SPACE_URL + '" style="color:#a5b4fc;font-size:.85rem">Click here if not redirected</a>';
 
440
  return res.end("SPACE_ID not configured.");
441
  }
442
 
443
+ // ── Private Space Guard (server-side) ──
444
+ // Triggers automatically when SPACE_IS_PRIVATE=true (detected via HF API at startup).
445
+ // Only intercepts browser navigation (Accept: text/html) — API calls, assets,
446
+ // and WebSocket upgrades pass through untouched.
447
+ // /health and /status are always exempt so uptime monitors keep working.
448
+ const isHtmlRequest = (req.headers.accept || "").includes("text/html");
449
+ const isDirectHfSpaceRequest = SPACE_IS_PRIVATE &&
450
+ HF_SPACE_URL &&
451
+ isHtmlRequest &&
452
+ typeof req.headers.host === "string" &&
453
+ req.headers.host.endsWith(".hf.space");
454
+
455
  if (pathname === "/env-builder" || pathname === "/env-builder/") {
456
+ if (isDirectHfSpaceRequest) {
457
+ res.writeHead(200, { "Content-Type": "text/html" });
458
+ return res.end(renderPrivateRedirect(HF_SPACE_URL));
459
+ }
460
  res.writeHead(200, { "Content-Type": "text/html" });
461
  return res.end(renderEnvBuilder());
462
  }
 
473
  }
474
 
475
  if (pathname === "/" || pathname === "/dashboard") {
476
+ if (isDirectHfSpaceRequest) {
477
+ res.writeHead(200, { "Content-Type": "text/html" });
478
+ return res.end(renderPrivateRedirect(HF_SPACE_URL));
479
+ }
480
  const [gatewayReady, jupyterReady] = await Promise.all([
481
  probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
482
  JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/login`) : Promise.resolve(false),
 
491
  res.writeHead(404, { "Content-Type": "application/json" });
492
  return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Set DEV_MODE=true to enable /terminal/." }));
493
  }
494
+ if (isDirectHfSpaceRequest) {
495
+ res.writeHead(200, { "Content-Type": "text/html" });
496
+ return res.end(renderPrivateRedirect(HF_SPACE_URL));
497
+ }
498
  return proxyHTTP(req, res, JUPYTER_HOST, JUPYTER_PORT, {
499
  publicPrefix: JUPYTER_BASE,
500
  // Jupyter is started with --ServerApp.base_url=/terminal/, so keep the
 
507
  // OpenClaw Control UI mounted under /app. Retry without the mount prefix on
508
  // 404 so deployments keep working across OpenClaw basePath behavior changes.
509
  if (pathname === APP_BASE || pathname.startsWith(APP_BASE + "/")) {
510
+ if (isDirectHfSpaceRequest) {
511
+ res.writeHead(200, { "Content-Type": "text/html" });
512
+ return res.end(renderPrivateRedirect(HF_SPACE_URL));
513
+ }
514
  return proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT, {
515
  publicPrefix: APP_BASE,
516
  stripPrefix: APP_BASE,
 
518
  });
519
  }
520
 
521
+ // Favicon — serve a minimal inline SVG so browsers don't proxy to the gateway
522
+ if (pathname === "/favicon.ico" || pathname === "/favicon.svg") {
523
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🦞</text></svg>';
524
+ res.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=86400" });
525
+ return res.end(svg);
526
+ }
527
+
528
  // OpenClaw gateway API/static fallback (everything else)
529
+ if (isDirectHfSpaceRequest) {
530
+ res.writeHead(200, { "Content-Type": "text/html" });
531
+ return res.end(renderPrivateRedirect(HF_SPACE_URL));
532
+ }
533
  proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT);
534
  });
535