dbernald2's picture
add N8N_PROTOCOL=https and N8N_HOST — required for HF Spaces
a43585e verified
Raw
History Blame Contribute Delete
4.07 kB
'use strict';
const httpProxy = require('/proxy/node_modules/http-proxy');
const http = require('http');
const N8N_PORT = parseInt(process.env.N8N_PORT || '5200');
const PROXY_PORT = 5678;
const USER = process.env.NGINX_USER;
const PASS = process.env.NGINX_PASSWORD;
const WEBHOOK_RE = /^\/webhook(-test)?\//;
// Rate limiter: max 10 failed attempts per IP within 15 minutes → block for 15 minutes
const MAX_FAILURES = 10;
const WINDOW_MS = 15 * 60 * 1000;
const BLOCK_MS = 15 * 60 * 1000;
const failures = new Map(); // ip → { count, windowStart, blockedUntil }
function getClientIp(req) {
const forwarded = req.headers['x-forwarded-for'];
return (forwarded ? forwarded.split(',')[0] : req.socket.remoteAddress).trim();
}
function isBlocked(ip) {
const entry = failures.get(ip);
if (!entry) return false;
if (entry.blockedUntil && Date.now() < entry.blockedUntil) return true;
// Block expired — reset
if (entry.blockedUntil) failures.delete(ip);
return false;
}
function recordFailure(ip) {
const now = Date.now();
const entry = failures.get(ip) || { count: 0, windowStart: now, blockedUntil: null };
// Reset window if it has expired
if (now - entry.windowStart > WINDOW_MS) {
entry.count = 0;
entry.windowStart = now;
}
entry.count += 1;
if (entry.count >= MAX_FAILURES) {
entry.blockedUntil = now + BLOCK_MS;
console.warn(`[rate-limit] Blocked ${ip} after ${entry.count} failed attempts`);
}
failures.set(ip, entry);
}
function clearFailures(ip) {
failures.delete(ip);
}
// Purge stale entries every 30 minutes to prevent memory growth
setInterval(() => {
const now = Date.now();
for (const [ip, entry] of failures) {
const expired = entry.blockedUntil
? now > entry.blockedUntil
: now - entry.windowStart > WINDOW_MS;
if (expired) failures.delete(ip);
}
}, 30 * 60 * 1000);
const proxy = httpProxy.createProxyServer({
target: `http://127.0.0.1:${N8N_PORT}`,
ws: true,
});
proxy.on('error', (err, req, res) => {
if (res && res.writeHead) {
res.writeHead(502);
res.end('n8n starting up, please refresh in a moment');
}
});
function isAuthorized(req) {
if (WEBHOOK_RE.test(req.url)) return true;
const auth = req.headers['authorization'];
if (!auth || !auth.startsWith('Basic ')) return false;
const decoded = Buffer.from(auth.slice(6), 'base64').toString();
const colon = decoded.indexOf(':');
const user = decoded.slice(0, colon);
const pass = decoded.slice(colon + 1);
return user === USER && pass === PASS;
}
const server = http.createServer((req, res) => {
// Webhooks always pass through
if (WEBHOOK_RE.test(req.url)) {
delete req.headers['x-forwarded-for'];
return proxy.web(req, res);
}
const ip = getClientIp(req);
if (isBlocked(ip)) {
res.writeHead(429, { 'Content-Type': 'text/plain', 'Retry-After': '900' });
res.end('Too many failed attempts. Try again in 15 minutes.');
return;
}
if (isAuthorized(req)) {
clearFailures(ip);
delete req.headers['x-forwarded-for'];
proxy.web(req, res);
} else {
// Only count as a brute-force attempt when credentials were actually provided
// but wrong. Unauthenticated requests (no Authorization header) are just the
// browser loading the page for the first time — counting them would block users
// before the Basic Auth dialog ever appears (browsers make ~20 parallel requests).
const hasCredentials = req.headers['authorization'] && req.headers['authorization'].startsWith('Basic ');
if (hasCredentials) recordFailure(ip);
res.writeHead(401, {
'WWW-Authenticate': 'Basic realm="n8n"',
'Content-Type': 'text/plain',
});
res.end('Unauthorized');
}
});
server.on('upgrade', (req, socket, head) => {
if (WEBHOOK_RE.test(req.url) || isAuthorized(req)) {
delete req.headers['x-forwarded-for'];
proxy.ws(req, socket, head);
} else {
socket.destroy();
}
});
server.listen(PROXY_PORT, () => {
console.log(`Auth proxy ready on :${PROXY_PORT} → n8n on :${N8N_PORT}`);
});