'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}`); });