Anurag commited on
Commit
b835b9d
·
unverified ·
2 Parent(s): 67c54ed8f6996c

Merge branch 'main' into main

Browse files
Files changed (1) hide show
  1. health-server.js +97 -38
health-server.js CHANGED
@@ -3,7 +3,7 @@ const http = require("http");
3
  const https = require("https");
4
  const fs = require("fs");
5
  const net = require("net");
6
- const { timingSafeEqual } = require("crypto");
7
 
8
  function isTrue(value) {
9
  return /^(true|1|yes|on)$/i.test(String(value || "").trim());
@@ -22,6 +22,8 @@ const JUPYTER_PORT = Number.parseInt(process.env.JUPYTER_PORT || "8888", 10);
22
  const JUPYTER_HOST = "127.0.0.1";
23
  const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
24
  const GATEWAY_TOKEN = (process.env.GATEWAY_TOKEN || "").trim();
 
 
25
  const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
26
  // Explicit HUGGINGCLAW_JUPYTER_ENABLED=true enables Jupyter.
27
  // Otherwise DEV_MODE=true enables it unless HUGGINGCLAW_JUPYTER_ENABLED is explicitly false.
@@ -256,18 +258,58 @@ function parseCookies(req) {
256
  return Object.fromEntries(h.split(";").map(c => c.trim().split("=")).filter(p => p.length >= 2).map(([k, ...v]) => [k.trim(), decodeURIComponent(v.join("=").trim())]));
257
  }
258
 
259
- // Constant-time comparison — prevent timing attacks on token check
260
- function safeEqual(a, b) {
261
- if (typeof a !== "string" || typeof b !== "string") return false;
262
- const ba = Buffer.from(a);
263
- const bb = Buffer.from(b);
264
- if (ba.length !== bb.length) return false;
265
- return timingSafeEqual(ba, bb);
266
  }
267
 
268
- function isEnvBuilderAuthed(req) {
269
- if (!GATEWAY_TOKEN) return true; // unprotected when no token set
270
- return safeEqual(parseCookies(req).hc_env_auth || "", GATEWAY_TOKEN);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  }
272
 
273
  function readBody(req) {
@@ -279,10 +321,11 @@ function readBody(req) {
279
  });
280
  }
281
 
282
- function renderEnvBuilderLogin(error = false) {
 
283
  return `<!doctype html><html lang="en"><head>
284
  <meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
285
- <title>HuggingClaw — Env Builder</title>
286
  <style>
287
  :root{color-scheme:dark;--bg:#08080f;--panel:#12111b;--line:#26243a;--text:#f6f4ff;--muted:#7f7a9e;--bad:#fb7185}
288
  *{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);padding:24px}
@@ -290,19 +333,20 @@ function renderEnvBuilderLogin(error = false) {
290
  h1{margin:0 0 8px;font-size:1.4rem}
291
  .sub{color:var(--muted);font-size:.82rem;margin:0 0 24px}
292
  .row{display:flex;gap:8px;margin-top:16px}
293
- input{flex:1;background:#0d0c18;border:1px solid var(--line);border-radius:7px;padding:10px 12px;color:var(--text);font-size:.95rem;outline:none}
294
  input:focus{border-color:#6366f1}
295
- button{background:#fff;color:#000;border:none;border-radius:7px;padding:10px 20px;font-weight:700;font-size:.95rem;cursor:pointer;transition:opacity .15s}
296
  button:hover{opacity:.85}
297
  .err{color:var(--bad);font-size:.82rem;margin-top:10px}
298
  code{background:#232234;border:1px solid #34324c;border-radius:5px;padding:2px 6px;font-size:.88em}
299
  </style></head><body>
300
  <div class="card">
301
- <h1>⚙️ Env Builder</h1>
302
  <p class="sub">Enter your <code>GATEWAY_TOKEN</code> to continue</p>
303
- <form method="post" action="/env-builder/login">
 
304
  <div class="row">
305
- <input type="password" name="token" placeholder="GATEWAY_TOKEN" autofocus autocomplete="current-password">
306
  <button type="submit">Unlock</button>
307
  </div>
308
  ${error ? '<p class="err">Invalid token — try again</p>' : ""}
@@ -567,6 +611,7 @@ function proxyHTTP(req, res, targetHost, targetPort, options = {}) {
567
  "x-forwarded-host": req.headers.host,
568
  "x-forwarded-proto": "https",
569
  "x-forwarded-prefix": options.publicPrefix || "",
 
570
  };
571
 
572
  const canReplayRequest = req.method === "GET" || req.method === "HEAD";
@@ -678,24 +723,37 @@ const server = http.createServer(async (req, res) => {
678
  !isSameOriginNav &&
679
  !isFromHFApp;
680
 
681
- if (pathname === "/env-builder/login") {
 
 
 
 
 
 
 
 
 
 
 
 
682
  if (req.method === "POST") {
683
  const body = await readBody(req);
684
- const token = decodeURIComponent((body.match(/(?:^|&)token=([^&]*)/) || [])[1] || "").replace(/\+/g, " ");
685
- if (safeEqual(token, GATEWAY_TOKEN)) {
686
- const cookie = `hc_env_auth=${encodeURIComponent(GATEWAY_TOKEN)}; Path=/; HttpOnly; SameSite=None; Secure; Partitioned; Max-Age=86400`;
687
- res.writeHead(302, { Location: "/env-builder", "Set-Cookie": cookie, "Cache-Control": "no-store" });
 
688
  return res.end();
689
  }
690
- res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
691
- return res.end(renderEnvBuilderLogin(true));
692
  }
693
- res.writeHead(302, { Location: "/env-builder", "Cache-Control": "no-store" });
694
- return res.end();
695
  }
696
 
697
- if (pathname === "/env-builder/logout") {
698
- res.writeHead(302, { Location: "/env-builder", "Set-Cookie": "hc_env_auth=; Path=/; HttpOnly; SameSite=None; Secure; Partitioned; Max-Age=0", "Cache-Control": "no-store" });
699
  return res.end();
700
  }
701
 
@@ -704,19 +762,13 @@ const server = http.createServer(async (req, res) => {
704
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
705
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
706
  }
707
- if (!isEnvBuilderAuthed(req)) {
708
- res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
709
- return res.end(renderEnvBuilderLogin(false));
710
- }
711
- res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
712
  return res.end(renderEnvBuilder());
713
  }
714
 
715
  if (pathname === "/env-builder.js") {
716
- if (!isEnvBuilderAuthed(req)) {
717
- res.writeHead(401, { "Content-Type": "text/plain" });
718
- return res.end("Unauthorized");
719
- }
720
  try {
721
  const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
722
  res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
@@ -751,12 +803,19 @@ const server = http.createServer(async (req, res) => {
751
  res.writeHead(200, { "Content-Type": "text/html" });
752
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
753
  }
 
 
 
 
 
 
754
  return proxyHTTP(req, res, JUPYTER_HOST, JUPYTER_PORT, {
755
  publicPrefix: JUPYTER_BASE,
756
  // Jupyter is started with --ServerApp.base_url=/terminal/, so keep the
757
  // /terminal prefix when proxying. Stripping it breaks static/theme URLs.
758
  stripPrefix: "",
759
  retryWithoutPrefixOn404: false,
 
760
  });
761
  }
762
 
 
3
  const https = require("https");
4
  const fs = require("fs");
5
  const net = require("net");
6
+ const crypto = require("crypto");
7
 
8
  function isTrue(value) {
9
  return /^(true|1|yes|on)$/i.test(String(value || "").trim());
 
22
  const JUPYTER_HOST = "127.0.0.1";
23
  const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
24
  const GATEWAY_TOKEN = (process.env.GATEWAY_TOKEN || "").trim();
25
+ const SESSION_COOKIE = "hc_session";
26
+ const LOGIN_PATH = "/login";
27
  const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
28
  // Explicit HUGGINGCLAW_JUPYTER_ENABLED=true enables Jupyter.
29
  // Otherwise DEV_MODE=true enables it unless HUGGINGCLAW_JUPYTER_ENABLED is explicitly false.
 
258
  return Object.fromEntries(h.split(";").map(c => c.trim().split("=")).filter(p => p.length >= 2).map(([k, ...v]) => [k.trim(), decodeURIComponent(v.join("=").trim())]));
259
  }
260
 
261
+ // Constant-time comparison using crypto — prevent timing attacks
262
+ function timingSafeEqualString(a, b) {
263
+ if (!a || !b) return false;
264
+ const bufA = Buffer.from(a);
265
+ const bufB = Buffer.from(b);
266
+ if (bufA.length !== bufB.length) return false;
267
+ return crypto.timingSafeEqual(bufA, bufB);
268
  }
269
 
270
+ function expectedSessionValue() {
271
+ if (!GATEWAY_TOKEN) return "";
272
+ return crypto.createHmac("sha256", GATEWAY_TOKEN).update("huggingclaw-session-v1").digest("hex");
273
+ }
274
+
275
+ function isHttpsRequest(req) {
276
+ return req.headers["x-forwarded-proto"] === "https";
277
+ }
278
+
279
+ function buildSessionCookie(req) {
280
+ const secure = isHttpsRequest(req) ? "; Secure" : "";
281
+ return `${SESSION_COOKIE}=${encodeURIComponent(expectedSessionValue())}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${secure}`;
282
+ }
283
+
284
+ function getBearerToken(req) {
285
+ const match = /^Bearer\s+(.+)$/i.exec(req.headers.authorization || "");
286
+ return match ? match[1] : "";
287
+ }
288
+
289
+ function isAuthorized(req) {
290
+ if (!GATEWAY_TOKEN) return true;
291
+ return (
292
+ timingSafeEqualString(getBearerToken(req), GATEWAY_TOKEN) ||
293
+ timingSafeEqualString(parseCookies(req)[SESSION_COOKIE], expectedSessionValue())
294
+ );
295
+ }
296
+
297
+ function sanitizeNext(value) {
298
+ if (!value || typeof value !== "string") return "/";
299
+ if (!value.startsWith("/") || value.startsWith("//")) return "/";
300
+ return value;
301
+ }
302
+
303
+ function loginUrl(nextPath) {
304
+ return `${LOGIN_PATH}?next=${encodeURIComponent(sanitizeNext(nextPath))}`;
305
+ }
306
+
307
+ function requireAuth(req, res) {
308
+ if (isAuthorized(req)) return true;
309
+ const parsed = parseRequestUrl(req.url);
310
+ res.writeHead(302, { Location: loginUrl(parsed.pathname + parsed.search), "Cache-Control": "no-store" });
311
+ res.end();
312
+ return false;
313
  }
314
 
315
  function readBody(req) {
 
321
  });
322
  }
323
 
324
+ function renderLoginPage(nextPath = "/", error = false) {
325
+ const safeNext = sanitizeNext(nextPath);
326
  return `<!doctype html><html lang="en"><head>
327
  <meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
328
+ <title>HuggingClaw</title>
329
  <style>
330
  :root{color-scheme:dark;--bg:#08080f;--panel:#12111b;--line:#26243a;--text:#f6f4ff;--muted:#7f7a9e;--bad:#fb7185}
331
  *{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);padding:24px}
 
333
  h1{margin:0 0 8px;font-size:1.4rem}
334
  .sub{color:var(--muted);font-size:.82rem;margin:0 0 24px}
335
  .row{display:flex;gap:8px;margin-top:16px}
336
+ input{flex:1;background:#0d0c18;border:1px solid var(--line);border-radius:7px;padding:10px 12px;color:var(--text);font-size:.95rem;outline:none;transition:border-color .15s}
337
  input:focus{border-color:#6366f1}
338
+ button{background:#fff;color:#000;border:none;border-radius:7px;padding:10px 20px;font-weight:700;font-size:.95rem;cursor:pointer;transition:opacity .15s;white-space:nowrap}
339
  button:hover{opacity:.85}
340
  .err{color:var(--bad);font-size:.82rem;margin-top:10px}
341
  code{background:#232234;border:1px solid #34324c;border-radius:5px;padding:2px 6px;font-size:.88em}
342
  </style></head><body>
343
  <div class="card">
344
+ <h1>🦞 HuggingClaw</h1>
345
  <p class="sub">Enter your <code>GATEWAY_TOKEN</code> to continue</p>
346
+ <form method="post" action="${LOGIN_PATH}">
347
+ <input type="hidden" name="next" value="${escapeHtml(safeNext)}" />
348
  <div class="row">
349
+ <input type="password" name="token" placeholder="GATEWAY_TOKEN" autofocus autocomplete="current-password" required>
350
  <button type="submit">Unlock</button>
351
  </div>
352
  ${error ? '<p class="err">Invalid token — try again</p>' : ""}
 
611
  "x-forwarded-host": req.headers.host,
612
  "x-forwarded-proto": "https",
613
  "x-forwarded-prefix": options.publicPrefix || "",
614
+ ...(options.extraHeaders || {}),
615
  };
616
 
617
  const canReplayRequest = req.method === "GET" || req.method === "HEAD";
 
723
  !isSameOriginNav &&
724
  !isFromHFApp;
725
 
726
+ if (pathname === LOGIN_PATH) {
727
+ if (isAuthorized(req)) {
728
+ const parsed = parseRequestUrl(req.url);
729
+ const next = sanitizeNext(parsed.searchParams.get("next") || "/");
730
+ res.writeHead(302, { Location: next, "Cache-Control": "no-store" });
731
+ return res.end();
732
+ }
733
+ if (req.method === "GET") {
734
+ const parsed = parseRequestUrl(req.url);
735
+ const next = sanitizeNext(parsed.searchParams.get("next") || "/");
736
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
737
+ return res.end(renderLoginPage(next, false));
738
+ }
739
  if (req.method === "POST") {
740
  const body = await readBody(req);
741
+ const params = new URLSearchParams(body);
742
+ const submittedToken = params.get("token") || "";
743
+ const next = sanitizeNext(params.get("next") || "/");
744
+ if (!GATEWAY_TOKEN || timingSafeEqualString(submittedToken, GATEWAY_TOKEN)) {
745
+ res.writeHead(302, { Location: next, "Set-Cookie": buildSessionCookie(req), "Cache-Control": "no-store" });
746
  return res.end();
747
  }
748
+ res.writeHead(401, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
749
+ return res.end(renderLoginPage(next, true));
750
  }
751
+ res.writeHead(405, { Allow: "GET, POST" });
752
+ return res.end("Method Not Allowed");
753
  }
754
 
755
+ if (pathname === "/logout") {
756
+ res.writeHead(302, { Location: LOGIN_PATH, "Set-Cookie": `${SESSION_COOKIE}=; Path=/; HttpOnly; Max-Age=0`, "Cache-Control": "no-store" });
757
  return res.end();
758
  }
759
 
 
762
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
763
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
764
  }
765
+ if (!requireAuth(req, res)) return;
766
+ res.writeHead(200, { "Content-Type": "text/html" });
 
 
 
767
  return res.end(renderEnvBuilder());
768
  }
769
 
770
  if (pathname === "/env-builder.js") {
771
+ if (!requireAuth(req, res)) return;
 
 
 
772
  try {
773
  const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
774
  res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
 
803
  res.writeHead(200, { "Content-Type": "text/html" });
804
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
805
  }
806
+ if (!requireAuth(req, res)) return;
807
+ // Inject the Jupyter token so JupyterLab skips its own login screen.
808
+ // Mirror start.sh logic: JUPYTER_TOKEN falls back to GATEWAY_TOKEN when
809
+ // unset or still the insecure default — that's what Jupyter was started with.
810
+ const rawJupyterToken = (process.env.JUPYTER_TOKEN || "").trim();
811
+ const jToken = (!rawJupyterToken || rawJupyterToken === "huggingface") ? GATEWAY_TOKEN : rawJupyterToken;
812
  return proxyHTTP(req, res, JUPYTER_HOST, JUPYTER_PORT, {
813
  publicPrefix: JUPYTER_BASE,
814
  // Jupyter is started with --ServerApp.base_url=/terminal/, so keep the
815
  // /terminal prefix when proxying. Stripping it breaks static/theme URLs.
816
  stripPrefix: "",
817
  retryWithoutPrefixOn404: false,
818
+ extraHeaders: jToken ? { authorization: `token ${jToken}` } : {},
819
  });
820
  }
821