93 lines
3.5 KiB
JavaScript
93 lines
3.5 KiB
JavaScript
/**
|
|
* 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 <api_key>"-Header. In Hermes' config.yaml also
|
|
* einfach `api_key: "<TOKEN>"` (= 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}`);
|
|
});
|