// Reverse proxy with built-in password auth for HermesFace Spaces.
// Routes: /v1/* → gateway:8642 (API key gated), /* → dashboard:7861 (password gated).
// Uses POST-based login (NO client crypto — works in HF iframes).
// Drop into scripts/proxy.js. No npm install needed — Node built-in http module only.
const http = require('http');
const crypto = require('crypto');
const DASHBOARD = { host: '127.0.0.1', port: 7861 };
const GATEWAY = { host: '127.0.0.1', port: 8642 };
const PASSWORD = process.env.HERMES_WEBUI_PASSWORD || 'gogiantswin';
const AUTH_HASH = crypto.createHash('sha256').update(PASSWORD).digest('hex').slice(0, 16);
function isAuthenticated(req) {
const cookies = (req.headers.cookie || '').split(';').map(c => c.trim());
for (const c of cookies) {
const [name, val] = c.split('=');
if (name === 'hf_auth' && val === AUTH_HASH) return true;
}
return false;
}
function getTarget(url) {
return url.startsWith('/v1/') || url === '/v1' ? GATEWAY : DASHBOARD;
}
function serveLogin(res, error) {
const errHtml = error ? '
Wrong password
' : '';
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
HermesFace — Login
🔱 HermesFace
Enter password to continue
`);
}
function parseBody(req, cb) {
let body = '';
req.on('data', chunk => { body += chunk; if (body.length > 1024) req.destroy(); });
req.on('end', () => {
const params = {};
body.split('&').forEach(p => { const [k,v] = p.split('='); params[k] = decodeURIComponent(v || ''); });
cb(params);
});
}
const server = http.createServer((req, res) => {
// Handle login POST
if (req.method === 'POST' && req.url === '/_login') {
parseBody(req, (params) => {
if (params.pw === PASSWORD) {
res.writeHead(302, { 'Location': '/', 'Set-Cookie': `hf_auth=${AUTH_HASH}; Path=/; Max-Age=86400; SameSite=Lax` });
res.end();
} else {
serveLogin(res, true);
}
});
return;
}
const target = getTarget(req.url);
// Auth check for dashboard routes (NOT /v1)
if (target === DASHBOARD && !isAuthenticated(req)) {
return serveLogin(res, false);
}
// Rewrite Host header so dashboard accepts forwarded requests
req.headers.host = `${target.host}:${target.port}`;
const proxyReq = http.request({
hostname: target.host,
port: target.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(`[proxy] ${req.method} ${req.url} → ${target.host}:${target.port} ERROR: ${err.message}`);
try { res.writeHead(502); res.end('Bad Gateway'); } catch (_) {}
});
req.pipe(proxyReq);
});
// WebSocket upgrade
server.on('upgrade', (req, socket, head) => {
const target = getTarget(req.url);
if (target === DASHBOARD && !isAuthenticated(req)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
req.headers.host = `${target.host}:${target.port}`;
const proxyWs = http.request({ hostname: target.host, port: target.port, path: req.url, method: 'GET', headers: req.headers });
proxyWs.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
Object.entries(proxyRes.headers).map(([k,v]) => `${k}: ${v}`).join('\r\n') + '\r\n\r\n');
socket.pipe(proxySocket).pipe(socket);
});
proxyWs.on('error', () => { socket.destroy(); });
proxyWs.end();
});
const PORT = process.env.PORT || 7860;
server.listen(PORT, '0.0.0.0', () => {
console.log(`[proxy] 0.0.0.0:${PORT} — /v1/* → gateway:8642 | /* → dashboard:7861 (password-protected)`);
});