Spaces:
Runtime error
Runtime error
File size: 4,069 Bytes
a43585e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | '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}`);
});
|