somratpro Claude Sonnet 4.5 commited on
Commit
b05a526
·
1 Parent(s): 80dd8e5

feat: password-protect env-builder with GATEWAY_TOKEN

Browse files

- GET /env-builder — shows login form if not authed
- POST /env-builder/login — validates GATEWAY_TOKEN, sets httpOnly cookie
- GET /env-builder/logout — clears cookie
- GET /env-builder.js — 401 if not authed (prevents unauthenticated JS load)
- Constant-time token comparison to prevent timing attacks
- Cookie: HttpOnly, SameSite=Strict, 24h TTL, scoped to /env-builder
- No protection when GATEWAY_TOKEN is unset (dev mode)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. health-server.js +88 -0
health-server.js CHANGED
@@ -159,6 +159,65 @@ function escapeHtml(v) {
159
  return String(v).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  function badge(label, tone = "neutral") {
163
  return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
164
  }
@@ -443,16 +502,45 @@ const server = http.createServer(async (req, res) => {
443
  typeof req.headers.host === "string" &&
444
  req.headers.host.endsWith(".hf.space");
445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  if (pathname === "/env-builder" || pathname === "/env-builder/") {
447
  if (isDirectHfSpaceRequest) {
448
  res.writeHead(200, { "Content-Type": "text/html" });
449
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
450
  }
 
 
 
 
451
  res.writeHead(200, { "Content-Type": "text/html" });
452
  return res.end(renderEnvBuilder());
453
  }
454
 
455
  if (pathname === "/env-builder.js") {
 
 
 
 
456
  try {
457
  const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
458
  res.writeHead(200, { "Content-Type": "application/javascript" });
 
159
  return String(v).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
160
  }
161
 
162
+ function parseCookies(req) {
163
+ const h = req.headers.cookie || "";
164
+ return Object.fromEntries(h.split(";").map(c => c.trim().split("=")).filter(p => p.length >= 2).map(([k, ...v]) => [k.trim(), decodeURIComponent(v.join("=").trim())]));
165
+ }
166
+
167
+ // Constant-time comparison — prevent timing attacks on token check
168
+ function safeEqual(a, b) {
169
+ if (typeof a !== "string" || typeof b !== "string" || a.length !== b.length) return false;
170
+ let d = 0;
171
+ for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
172
+ return d === 0;
173
+ }
174
+
175
+ function isEnvBuilderAuthed(req) {
176
+ if (!GATEWAY_TOKEN) return true; // unprotected when no token set
177
+ return safeEqual(parseCookies(req).hc_env_auth || "", GATEWAY_TOKEN);
178
+ }
179
+
180
+ function readBody(req) {
181
+ return new Promise((resolve) => {
182
+ let body = "";
183
+ req.on("data", chunk => { body += chunk; if (body.length > 4096) { body = ""; req.destroy(); } });
184
+ req.on("end", () => resolve(body));
185
+ req.on("error", () => resolve(""));
186
+ });
187
+ }
188
+
189
+ function renderEnvBuilderLogin(error = false) {
190
+ return `<!doctype html><html lang="en"><head>
191
+ <meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
192
+ <title>HuggingClaw — Env Builder</title>
193
+ <style>
194
+ :root{color-scheme:dark;--bg:#08080f;--panel:#12111b;--line:#26243a;--text:#f6f4ff;--muted:#7f7a9e;--bad:#fb7185}
195
+ *{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}
196
+ .card{border:1px solid var(--line);background:var(--panel);border-radius:14px;padding:36px 32px;max-width:400px;width:100%;text-align:center}
197
+ h1{margin:0 0 8px;font-size:1.4rem}
198
+ .sub{color:var(--muted);font-size:.82rem;margin:0 0 24px}
199
+ .row{display:flex;gap:8px;margin-top:16px}
200
+ input{flex:1;background:#0d0c18;border:1px solid var(--line);border-radius:7px;padding:10px 12px;color:var(--text);font-size:.95rem;outline:none}
201
+ input:focus{border-color:#6366f1}
202
+ button{background:#fff;color:#000;border:none;border-radius:7px;padding:10px 20px;font-weight:700;font-size:.95rem;cursor:pointer;transition:opacity .15s}
203
+ button:hover{opacity:.85}
204
+ .err{color:var(--bad);font-size:.82rem;margin-top:10px}
205
+ code{background:#232234;border:1px solid #34324c;border-radius:5px;padding:2px 6px;font-size:.88em}
206
+ </style></head><body>
207
+ <div class="card">
208
+ <h1>⚙️ Env Builder</h1>
209
+ <p class="sub">Enter your <code>GATEWAY_TOKEN</code> to continue</p>
210
+ <form method="post" action="/env-builder/login">
211
+ <div class="row">
212
+ <input type="password" name="token" placeholder="GATEWAY_TOKEN" autofocus autocomplete="current-password">
213
+ <button type="submit">Unlock</button>
214
+ </div>
215
+ ${error ? '<p class="err">Invalid token — try again</p>' : ""}
216
+ </form>
217
+ </div>
218
+ </body></html>`;
219
+ }
220
+
221
  function badge(label, tone = "neutral") {
222
  return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
223
  }
 
502
  typeof req.headers.host === "string" &&
503
  req.headers.host.endsWith(".hf.space");
504
 
505
+ if (pathname === "/env-builder/login") {
506
+ if (req.method === "POST") {
507
+ const body = await readBody(req);
508
+ const token = decodeURIComponent((body.match(/(?:^|&)token=([^&]*)/) || [])[1] || "").replace(/\+/g, " ");
509
+ if (safeEqual(token, GATEWAY_TOKEN)) {
510
+ const cookie = `hc_env_auth=${encodeURIComponent(GATEWAY_TOKEN)}; Path=/env-builder; HttpOnly; SameSite=Strict; Max-Age=86400`;
511
+ res.writeHead(302, { Location: "/env-builder", "Set-Cookie": cookie, "Cache-Control": "no-store" });
512
+ return res.end();
513
+ }
514
+ res.writeHead(200, { "Content-Type": "text/html" });
515
+ return res.end(renderEnvBuilderLogin(true));
516
+ }
517
+ res.writeHead(302, { Location: "/env-builder", "Cache-Control": "no-store" });
518
+ return res.end();
519
+ }
520
+
521
+ if (pathname === "/env-builder/logout") {
522
+ res.writeHead(302, { Location: "/env-builder", "Set-Cookie": "hc_env_auth=; Path=/env-builder; HttpOnly; Max-Age=0", "Cache-Control": "no-store" });
523
+ return res.end();
524
+ }
525
+
526
  if (pathname === "/env-builder" || pathname === "/env-builder/") {
527
  if (isDirectHfSpaceRequest) {
528
  res.writeHead(200, { "Content-Type": "text/html" });
529
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
530
  }
531
+ if (!isEnvBuilderAuthed(req)) {
532
+ res.writeHead(200, { "Content-Type": "text/html" });
533
+ return res.end(renderEnvBuilderLogin(false));
534
+ }
535
  res.writeHead(200, { "Content-Type": "text/html" });
536
  return res.end(renderEnvBuilder());
537
  }
538
 
539
  if (pathname === "/env-builder.js") {
540
+ if (!isEnvBuilderAuthed(req)) {
541
+ res.writeHead(401, { "Content-Type": "text/plain" });
542
+ return res.end("Unauthorized");
543
+ }
544
  try {
545
  const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
546
  res.writeHead(200, { "Content-Type": "application/javascript" });