/** * hermes-gateway — minimaler Bearer-Token Reverse-Proxy vor hermes-proxy. * * WARUM: hermes-proxy selbst hat KEINE Authentifizierung — genau wie ARIAs * Proxy (siehe /root/ARIA-AGENT/docker-compose.yml, Service "proxy"). Bei * ARIA ist das ok, weil der Proxy NIE einen Netzwerk-Port bekommt, nur * Docker-intern von aria-brain erreichbar ist. * * Hermes Agent laeuft aber auf einer ANDEREN Maschine (Stefans Wunsch) und * muss den Proxy ueber Netzwerk erreichen -> der Port muss also offen sein. * Ohne Auth koennte dann JEDER der den Port erreicht (falsches Netz-Segment, * offenes WLAN, Portscan) Stefans Claude-Max-Subscription fremdnutzen — * die eigentliche "Auth" ist ja nur die eingeloggte Claude-CLI-Session im * Volume, kein Provider-seitiger API-Key-Check. * * Dieses Gateway sitzt davor: prueft den Authorization-Header gegen ein * Shared Secret (ENV TOKEN), leitet nur bei Match an hermes-proxy weiter. * Alles andere -> 401, kein Request geht durch. * * Praktischer Nebeneffekt: OpenAI-kompatible Clients (Hermes Agent * eingeschlossen) schicken ihren konfigurierten `api_key` sowieso schon als * "Authorization: Bearer "-Header. In Hermes' config.yaml also * einfach `api_key: ""` (= derselbe Wert wie hier in TOKEN) setzen — * kein Extra-Code auf Hermes-Seite noetig. */ const http = require("http"); const crypto = require("crypto"); const TOKEN = process.env.TOKEN; const UPSTREAM_HOST = process.env.UPSTREAM_HOST || "hermes-proxy"; const UPSTREAM_PORT = parseInt(process.env.UPSTREAM_PORT || "3456", 10); const LISTEN_PORT = parseInt(process.env.LISTEN_PORT || "8080", 10); if (!TOKEN || TOKEN.length < 16) { console.error( "FATAL: TOKEN env fehlt oder ist zu kurz (< 16 Zeichen). " + "Ohne ordentliches Shared Secret startet das Gateway bewusst nicht " + "— sonst haengt hier ein offener Claude-Proxy im Netz." ); process.exit(1); } function safeEqual(a, b) { const ab = Buffer.from(a, "utf8"); const bb = Buffer.from(b, "utf8"); if (ab.length !== bb.length) return false; return crypto.timingSafeEqual(ab, bb); } function unauthorized(res) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: { message: "unauthorized", type: "invalid_api_key" } })); } const server = http.createServer((req, res) => { const auth = req.headers["authorization"] || ""; const expected = `Bearer ${TOKEN}`; if (!safeEqual(auth, expected)) { console.log(`[hermes-gateway] 401 ${req.method} ${req.url} von ${req.socket.remoteAddress}`); return unauthorized(res); } const proxyReq = http.request( { hostname: UPSTREAM_HOST, port: UPSTREAM_PORT, path: req.url, method: req.method, headers: req.headers, }, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); } ); proxyReq.on("error", (err) => { console.error(`[hermes-gateway] upstream error: ${err.message}`); if (!res.headersSent) { res.writeHead(502, { "Content-Type": "application/json" }); } res.end(JSON.stringify({ error: { message: "bad_gateway", detail: String(err.message) } })); }); req.pipe(proxyReq); }); server.listen(LISTEN_PORT, "0.0.0.0", () => { console.log(`[hermes-gateway] listening on 0.0.0.0:${LISTEN_PORT} -> http://${UPSTREAM_HOST}:${UPSTREAM_PORT}`); });