Spaces:
Running
Running
Protect Hermes app route with gateway token
Browse files- README.md +17 -0
- 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 (
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 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;
|