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