Spaces:
Runtime error
Runtime error
| ; | |
| 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}`); | |
| }); | |