somratpro commited on
Commit
e04b24a
·
1 Parent(s): 08d8db1

Protect Hermes app route with gateway token

Browse files
Files changed (2) hide show
  1. README.md +17 -0
  2. health-server.js +47 -8
README.md CHANGED
@@ -46,6 +46,23 @@ HuggingMess runs [Nous Research Hermes Agent](https://github.com/NousResearch/he
46
  | `CLOUDFLARE_WORKERS_TOKEN` | Optional | Auto-creates a Worker proxy for Telegram Bot API traffic |
47
  | `UPTIMEROBOT_API_KEY` | Optional | Auto-creates a monitor for `/health` |
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  ## LLM Providers
50
 
51
  HuggingMess supports two configuration styles:
 
46
  | `CLOUDFLARE_WORKERS_TOKEN` | Optional | Auto-creates a Worker proxy for Telegram Bot API traffic |
47
  | `UPTIMEROBOT_API_KEY` | Optional | Auto-creates a monitor for `/health` |
48
 
49
+ ## Access Control
50
+
51
+ Hermes' built-in dashboard is local-first and does not provide its own public auth layer. HuggingMess adds wrapper-level auth for the exposed Space routes.
52
+
53
+ Set this Space secret:
54
+
55
+ ```text
56
+ GATEWAY_TOKEN=your-strong-password-or-token
57
+ ```
58
+
59
+ Then:
60
+
61
+ - Opening `/app/` asks for browser Basic Auth.
62
+ - Use any username.
63
+ - Use `GATEWAY_TOKEN` as the password.
64
+ - API routes under `/v1/*` accept `Authorization: Bearer <GATEWAY_TOKEN>`.
65
+
66
  ## LLM Providers
67
 
68
  HuggingMess supports two configuration styles:
health-server.js CHANGED
@@ -12,6 +12,7 @@ const GATEWAY_HOST = "127.0.0.1";
12
  const startTime = Date.now();
13
  const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
14
  const APP_BASE = "/app";
 
15
 
16
  const SYNC_STATUS_FILE = "/tmp/huggingmess-sync-status.json";
17
  const UPTIMEROBOT_STATUS_FILE = "/tmp/huggingmess-uptimerobot-status.json";
@@ -38,6 +39,41 @@ function readJson(path, fallback = null) {
38
  return fallback;
39
  }
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  function proxyRequest(req, res, targetPort, rewritePath = (path) => path) {
42
  const parsed = new URL(req.url, "http://localhost");
43
  const targetPath = rewritePath(parsed.pathname) + parsed.search;
@@ -120,7 +156,7 @@ function badge(label, state) {
120
 
121
  function renderDashboard(data) {
122
  const syncStatus = String(data.backup?.status || "unknown").toUpperCase();
123
- const dashboardLink = data.dashboard ? `<a class="button" href="${APP_BASE}/">Open Hermes App</a>` : "";
124
  const apiLink = data.gateway ? `<a class="button secondary" href="/v1/models">API Models</a>` : "";
125
  const keepAlive = data.uptimerobot?.configured
126
  ? `UptimeRobot is monitoring <code>${data.uptimerobot.url}</code>.`
@@ -216,6 +252,7 @@ const server = http.createServer(async (req, res) => {
216
  }
217
 
218
  if (path === APP_BASE || path.startsWith(`${APP_BASE}/`)) {
 
219
  proxyRequest(req, res, DASHBOARD_PORT, (p) => p.replace(/^\/app/, "") || "/");
220
  return;
221
  }
@@ -227,6 +264,7 @@ const server = http.createServer(async (req, res) => {
227
  path.startsWith("/dashboard-plugins/") ||
228
  path.startsWith("/ds-assets/")
229
  ) {
 
230
  proxyRequest(req, res, DASHBOARD_PORT);
231
  return;
232
  }
@@ -252,13 +290,14 @@ const server = http.createServer(async (req, res) => {
252
  }
253
 
254
  if (path === "/v1" || path.startsWith("/v1/")) {
255
- if (API_SERVER_KEY) {
256
- const expected = `Bearer ${API_SERVER_KEY}`;
257
- if (req.headers.authorization !== expected) {
258
- res.writeHead(401, { "content-type": "application/json" });
259
- res.end(JSON.stringify({ error: "unauthorized", message: "Use Authorization: Bearer <GATEWAY_TOKEN>." }));
260
- return;
261
- }
 
262
  }
263
  proxyRequest(req, res, GATEWAY_PORT);
264
  return;
 
12
  const startTime = Date.now();
13
  const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
14
  const APP_BASE = "/app";
15
+ const AUTH_REALM = "HuggingMess";
16
 
17
  const SYNC_STATUS_FILE = "/tmp/huggingmess-sync-status.json";
18
  const UPTIMEROBOT_STATUS_FILE = "/tmp/huggingmess-uptimerobot-status.json";
 
39
  return fallback;
40
  }
41
 
42
+ function getBearerToken(req) {
43
+ const value = req.headers.authorization || "";
44
+ const match = /^Bearer\s+(.+)$/i.exec(value);
45
+ return match ? match[1] : "";
46
+ }
47
+
48
+ function getBasicPassword(req) {
49
+ const value = req.headers.authorization || "";
50
+ const match = /^Basic\s+(.+)$/i.exec(value);
51
+ if (!match) return "";
52
+ try {
53
+ const decoded = Buffer.from(match[1], "base64").toString("utf8");
54
+ const separator = decoded.indexOf(":");
55
+ return separator >= 0 ? decoded.slice(separator + 1) : "";
56
+ } catch {
57
+ return "";
58
+ }
59
+ }
60
+
61
+ function isAuthorized(req) {
62
+ if (!API_SERVER_KEY) return true;
63
+ return getBearerToken(req) === API_SERVER_KEY || getBasicPassword(req) === API_SERVER_KEY;
64
+ }
65
+
66
+ function requireAuth(req, res) {
67
+ if (isAuthorized(req)) return true;
68
+ res.writeHead(401, {
69
+ "content-type": "text/plain; charset=utf-8",
70
+ "www-authenticate": `Basic realm="${AUTH_REALM}", charset="UTF-8"`,
71
+ "cache-control": "no-store",
72
+ });
73
+ res.end("Authentication required. Use any username and your GATEWAY_TOKEN as the password.");
74
+ return false;
75
+ }
76
+
77
  function proxyRequest(req, res, targetPort, rewritePath = (path) => path) {
78
  const parsed = new URL(req.url, "http://localhost");
79
  const targetPath = rewritePath(parsed.pathname) + parsed.search;
 
156
 
157
  function renderDashboard(data) {
158
  const syncStatus = String(data.backup?.status || "unknown").toUpperCase();
159
+ const dashboardLink = data.dashboard ? `<a class="button" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Hermes App</a>` : "";
160
  const apiLink = data.gateway ? `<a class="button secondary" href="/v1/models">API Models</a>` : "";
161
  const keepAlive = data.uptimerobot?.configured
162
  ? `UptimeRobot is monitoring <code>${data.uptimerobot.url}</code>.`
 
252
  }
253
 
254
  if (path === APP_BASE || path.startsWith(`${APP_BASE}/`)) {
255
+ if (!requireAuth(req, res)) return;
256
  proxyRequest(req, res, DASHBOARD_PORT, (p) => p.replace(/^\/app/, "") || "/");
257
  return;
258
  }
 
264
  path.startsWith("/dashboard-plugins/") ||
265
  path.startsWith("/ds-assets/")
266
  ) {
267
+ if (!requireAuth(req, res)) return;
268
  proxyRequest(req, res, DASHBOARD_PORT);
269
  return;
270
  }
 
290
  }
291
 
292
  if (path === "/v1" || path.startsWith("/v1/")) {
293
+ if (!isAuthorized(req)) {
294
+ res.writeHead(401, {
295
+ "content-type": "application/json",
296
+ "www-authenticate": `Bearer realm="${AUTH_REALM}"`,
297
+ "cache-control": "no-store",
298
+ });
299
+ res.end(JSON.stringify({ error: "unauthorized", message: "Use Authorization: Bearer <GATEWAY_TOKEN>." }));
300
+ return;
301
  }
302
  proxyRequest(req, res, GATEWAY_PORT);
303
  return;