Add rate limit to basic auth middleware (#5504) * feat: add rate limiting to basic auth flow * fix: round up retry-after duration * feat: enhance point consume logic * fix: move unauthorized webpage reading inside response function * refactor: move getIpAddress to express-common * fix: check for rate limit before checking creds * fix: use correct rate limit pattern in /recover-step2 * feat: handle CF forwarded IP header in rate limit, whitelist and access logger * feat: add individual config toggles for forwarded headers * feat: enhance IP address retrieval to include forwarded IP for access logging * chore: clean-up diff * fix: don't consume points for missing credentials * feat: log rate limited method and URL Co-authored-by: Copilot <copilot@github.com> * feat: make rate limiter points configurable Co-authored-by: Copilot <copilot@github.com> * feat: implement retry-after header for rate limiting responses Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
Signed| @@ -58,7 +58,7 @@ ssl: | ||
| 58 | 58 | # -- SECURITY CONFIGURATION -- |
| 59 | 59 | # Toggle whitelist mode |
| 60 | 60 | whitelistMode: true |
| 61 | 61 | # WhitelistWhen enabled, whitelist will also verify IP in X-Forwarded-Forheaders /enabled X-Real-IPin headers`forwardedHeaders` section. |
| 62 | 62 | enableForwardedWhitelist: true |
| 63 | 63 | # Whitelist of allowed IP addresses |
| 64 | 64 | whitelist: |
| @@ -189,9 +189,24 @@ logging: | ||
| 189 | 189 | minLogLevel: 0 |
| 190 | 190 | # -- RATE LIMITING CONFIGURATION -- |
| 191 | 191 | rateLimiting: |
| 192 | 192 | # Use X-Real-IPany headerof insteadthe ofenabled socketheaders in the `forwardedHeaders` section to identify the client IP for rate limiting. |
| 193 | - # Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy) | |
| 193 | + # If disabled, only the socket IP will be used, which may not work correctly if you are behind a reverse proxy. | |
| 194 | 194 | preferRealIpHeader: false |
| 195 | + # Set the maximum number of allowed failed basic authentication attempts before rate limiting is applied. Set to 0 to disable rate limiting for basic auth. | |
| 196 | + basicAuthMaxAttempts: 5 | |
| 197 | + # Set the maximum number of allowed failed account login attempts before rate limiting is applied. Set to 0 to disable rate limiting for account logins. | |
| 198 | + accountsLoginMaxAttempts: 5 | |
| 199 | + # Set the maximum number of allowed failed account recovery attempts before rate limiting is applied. Set to 0 to disable rate limiting for account recovery. | |
| 200 | + accountsRecoverMaxAttempts: 5 | |
| 201 | +# Set to true to enable support for real IPs in certain request headers for features like IP whitelisting, rate limiting and access logging. | |
| 202 | +# Only change if you are sure that you use a correctly configured reverse proxy, otherwise this may lead to IP spoofing. | |
| 203 | +forwardedHeaders: | |
| 204 | + # X-Real-IP header (common with Nginx and Caddy) | |
| 205 | + xRealIp: true | |
| 206 | + # X-Forwarded-For header (common with many proxies, but may contain multiple IPs - only the first one will be used) | |
| 207 | + xForwardedFor: true | |
| 208 | + # CF-Connecting-IP header (used by Cloudflare Tunnels) | |
| 209 | + cfConnectingIp: false | |
| 195 | 210 | |
| 196 | 211 | ## BACKUP CONFIGURATION |
| 197 | 212 | backups: |
| @@ -3,23 +3,23 @@ import crypto from 'node:crypto'; | ||
| 3 | 3 | import storage from 'node-persist'; |
| 4 | 4 | import express from 'express'; |
| 5 | 5 | import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible'; |
| 6 | 6 | import { getIpFromRequestgetIpAddress, getRealIpFromHeaderretryAfter } from '../express-common.js'; |
| 7 | 7 | import { color, Cache, getConfigValue } from '../util.js'; |
| 8 | 8 | import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js'; |
| 9 | 9 | |
| 10 | 10 | const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false, 'boolean'); |
| 11 | 11 | const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false, 'boolean'); |
| 12 | +const LOGIN_POINTS = getConfigValue('rateLimiting.accountsLoginMaxAttempts', 5, 'number'); | |
| 13 | +const RECOVER_POINTS = getConfigValue('rateLimiting.accountsRecoverMaxAttempts', 5, 'number'); | |
| 12 | 14 | const MFA_CACHE = new Cache(5 * 60 * 1000); |
| 13 | 15 | |
| 14 | -const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request); | |
| 15 | - | |
| 16 | 16 | export const router = express.Router(); |
| 17 | 17 | const loginLimiter = new RateLimiterMemory({ |
| 18 | - points: 5, | |
| 18 | + points: LOGIN_POINTS > 0 ? LOGIN_POINTS : Number.MAX_SAFE_INTEGER, | |
| 19 | 19 | duration: 60, |
| 20 | 20 | }); |
| 21 | 21 | const recoverLimiter = new RateLimiterMemory({ |
| 22 | - points: 5, | |
| 22 | + points: RECOVER_POINTS > 0 ? RECOVER_POINTS : Number.MAX_SAFE_INTEGER, | |
| 23 | 23 | duration: 300, |
| 24 | 24 | }); |
| 25 | 25 | |
| @@ -63,7 +63,7 @@ router.post('/login', async (request, response) => { | ||
| 63 | 63 | return response.status(400).json({ error: 'Missing required fields' }); |
| 64 | 64 | } |
| 65 | 65 | |
| 66 | 66 | const ip = getIpAddress(request, PREFER_REAL_IP_HEADER); |
| 67 | 67 | await loginLimiter.consume(ip); |
| 68 | 68 | |
| 69 | 69 | /** @type {import('../users.js').User} */ |
| @@ -95,8 +95,8 @@ router.post('/login', async (request, response) => { | ||
| 95 | 95 | return response.json({ handle: user.handle }); |
| 96 | 96 | } catch (error) { |
| 97 | 97 | if (error instanceof RateLimiterRes) { |
| 98 | 98 | console.error('Login failed: Rate limited from', getIpAddress(request, PREFER_REAL_IP_HEADER)); |
| 99 | 99 | return retryAfter(response, error).status(429).send({ error: 'Too many attempts. Try again later or recover your password.' }); |
| 100 | 100 | } |
| 101 | 101 | |
| 102 | 102 | console.error('Login failed:', error); |
| @@ -111,7 +111,7 @@ router.post('/recover-step1', async (request, response) => { | ||
| 111 | 111 | return response.status(400).json({ error: 'Missing required fields' }); |
| 112 | 112 | } |
| 113 | 113 | |
| 114 | 114 | const ip = getIpAddress(request, PREFER_REAL_IP_HEADER); |
| 115 | 115 | await recoverLimiter.consume(ip); |
| 116 | 116 | |
| 117 | 117 | /** @type {import('../users.js').User} */ |
| @@ -135,8 +135,8 @@ router.post('/recover-step1', async (request, response) => { | ||
| 135 | 135 | return response.sendStatus(204); |
| 136 | 136 | } catch (error) { |
| 137 | 137 | if (error instanceof RateLimiterRes) { |
| 138 | 138 | console.error('Recover step 1 failed: Rate limited from', getIpAddress(request, PREFER_REAL_IP_HEADER)); |
| 139 | 139 | return retryAfter(response, error).status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' }); |
| 140 | 140 | } |
| 141 | 141 | |
| 142 | 142 | console.error('Recover step 1 failed:', error); |
| @@ -153,7 +153,12 @@ router.post('/recover-step2', async (request, response) => { | ||
| 153 | 153 | |
| 154 | 154 | /** @type {import('../users.js').User} */ |
| 155 | 155 | const user = await storage.getItem(toKey(request.body.handle)); |
| 156 | 156 | const ip = getIpAddress(request, PREFER_REAL_IP_HEADER); |
| 157 | + const rateLimit = await recoverLimiter.get(ip); | |
| 158 | + | |
| 159 | + if (rateLimit !== null && rateLimit.consumedPoints > recoverLimiter.points) { | |
| 160 | + throw rateLimit; | |
| 161 | + } | |
| 157 | 162 | |
| 158 | 163 | if (!user) { |
| 159 | 164 | console.error('Recover step 2 failed: User', request.body.handle, 'not found'); |
| @@ -189,8 +194,8 @@ router.post('/recover-step2', async (request, response) => { | ||
| 189 | 194 | return response.sendStatus(204); |
| 190 | 195 | } catch (error) { |
| 191 | 196 | if (error instanceof RateLimiterRes) { |
| 192 | 197 | console.error('Recover step 2 failed: Rate limited from', getIpAddress(request, PREFER_REAL_IP_HEADER)); |
| 193 | 198 | return retryAfter(response, error).status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' }); |
| 194 | 199 | } |
| 195 | 200 | |
| 196 | 201 | console.error('Recover step 2 failed:', error); |
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | import ipaddr from 'ipaddr.js'; |
| 2 | 2 | import ipMatching from 'ip-matching'; |
| 3 | +import { RateLimiterRes } from 'rate-limiter-flexible'; | |
| 4 | +import { getConfigValue } from './util.js'; | |
| 3 | 5 | |
| 4 | 6 | const noopMiddleware = (_req, _res, next) => next(); |
| 5 | 7 | /** @deprecated Do not use. A global middleware is provided at the application level. */ |
| @@ -29,17 +31,46 @@ export function getIpFromRequest(req) { | ||
| 29 | 31 | } |
| 30 | 32 | |
| 31 | 33 | /** |
| 32 | - * Gets the IP address of the client when behind reverse proxy using x-real-ip header, falls back to socket remote address. | |
| 34 | + * Get the client IP address from the request headers. | |
| 33 | - * This function should be used when the application is running behind a reverse proxy (e.g., Nginx, traefik, Caddy...). | |
| 35 | + * @param {import('express').Request} req Express request object | |
| 34 | - * @param {import('express').Request} req Request object | |
| 36 | + * @returns {string|undefined} The client IP address | |
| 35 | - * @returns {string} IP address of the client | |
| 36 | 37 | */ |
| 37 | 38 | export function getRealIpFromHeadergetRealOrForwardedIp(req) { |
| 38 | - if (req.headers['x-real-ip']) { | |
| 39 | + const xRealIpEnabled = !!getConfigValue('forwardedHeaders.xRealIp', true, 'boolean'); | |
| 40 | + const cfConnectingIpEnabled = !!getConfigValue('forwardedHeaders.cfConnectingIp', false, 'boolean'); | |
| 41 | + const xForwardedForEnabled = !!getConfigValue('forwardedHeaders.xForwardedFor', true, 'boolean'); | |
| 42 | + | |
| 43 | + // Check if X-Real-IP is available | |
| 44 | + if (req.headers['x-real-ip'] && xRealIpEnabled) { | |
| 39 | 45 | return req.headers['x-real-ip'].toString(); |
| 40 | 46 | } |
| 41 | 47 | |
| 42 | - return getIpFromRequest(req); | |
| 48 | + // Check for CF-Connecting-IP (Cloudflare) if available | |
| 49 | + if (req.headers['cf-connecting-ip'] && cfConnectingIpEnabled) { | |
| 50 | + return req.headers['cf-connecting-ip'].toString(); | |
| 51 | + } | |
| 52 | + | |
| 53 | + // Check for X-Forwarded-For and parse if available | |
| 54 | + if (req.headers['x-forwarded-for'] && xForwardedForEnabled) { | |
| 55 | + const ipList = req.headers['x-forwarded-for'].toString().split(',').map(ip => ip.trim()); | |
| 56 | + return ipList[0]; | |
| 57 | + } | |
| 58 | + | |
| 59 | + // If none of the headers are available, return undefined | |
| 60 | + return undefined; | |
| 61 | +} | |
| 62 | + | |
| 63 | +/** | |
| 64 | + * Gets the IP address of the client, optionally including the real/forwarded IP from headers. | |
| 65 | + * Most common use cases: key for rate limiter, logging, etc. where you want to have the real client IP if behind a reverse proxy. | |
| 66 | + * @param {import('express').Request} request Request object | |
| 67 | + * @param {boolean} includeHeaderIp Whether to include the real/forwarded IP from headers | |
| 68 | + * @returns {string} IP address of the client (will include "forwarded" info if includeHeaderIp is true and headers are present) | |
| 69 | + */ | |
| 70 | +export function getIpAddress(request, includeHeaderIp) { | |
| 71 | + const socketIp = getIpFromRequest(request); | |
| 72 | + const forwardedIp = includeHeaderIp && getRealOrForwardedIp(request); | |
| 73 | + return forwardedIp ? `${socketIp} (forwarded: ${forwardedIp})` : socketIp; | |
| 43 | 74 | } |
| 44 | 75 | |
| 45 | 76 | /** |
| @@ -79,3 +110,18 @@ export function filterValidIpPatterns(entries, formatLog) { | ||
| 79 | 110 | |
| 80 | 111 | return validEntries; |
| 81 | 112 | } |
| 113 | + | |
| 114 | +/** | |
| 115 | + * Sets the Retry-After header on the response based on the rate limit information. | |
| 116 | + * @param {import('express').Response} response Express response object | |
| 117 | + * @param {RateLimiterRes} rateLimit The rate limit information from rate-limiter-flexible | |
| 118 | + * @returns {import('express').Response} The response object with the Retry-After header set if applicable | |
| 119 | + */ | |
| 120 | +export function retryAfter(response, rateLimit) { | |
| 121 | + if (response.headersSent || !(rateLimit instanceof RateLimiterRes)) { | |
| 122 | + return response; | |
| 123 | + } | |
| 124 | + const retryAfter = Math.ceil(rateLimit.msBeforeNext / 1000); | |
| 125 | + response.set('Retry-After', retryAfter.toString()); | |
| 126 | + return response; | |
| 127 | +} | |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import path from 'node:path'; |
| 2 | 2 | import fs from 'node:fs'; |
| 3 | 3 | import { getRealIpFromHeadergetIpAddress } from '../express-common.js'; |
| 4 | 4 | import { color, getConfigValue } from '../util.js'; |
| 5 | 5 | |
| 6 | 6 | const enableAccessLog = getConfigValue('logging.enableAccessLog', true, 'boolean'); |
| @@ -32,7 +32,7 @@ export function migrateAccessLog() { | ||
| 32 | 32 | */ |
| 33 | 33 | export default function accessLoggerMiddleware() { |
| 34 | 34 | return function (req, res, next) { |
| 35 | 35 | const clientIp = getRealIpFromHeadergetIpAddress(req, true); |
| 36 | 36 | const userAgent = req.headers['user-agent']; |
| 37 | 37 | |
| 38 | 38 | if (!knownIPs.has(clientIp)) { |
| @@ -5,53 +5,83 @@ | ||
| 5 | 5 | import { Buffer } from 'node:buffer'; |
| 6 | 6 | import path from 'node:path'; |
| 7 | 7 | import storage from 'node-persist'; |
| 8 | +import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible'; | |
| 8 | 9 | import { getAllUserHandles, toKey, getPasswordHash } from '../users.js'; |
| 9 | 10 | import { getConfigValue, safeReadFileSync } from '../util.js'; |
| 11 | +import { getIpAddress, retryAfter } from '../express-common.js'; | |
| 10 | 12 | |
| 11 | 13 | const PER_USER_BASIC_AUTH = !!getConfigValue('perUserBasicAuth', false, 'boolean'); |
| 12 | 14 | const ENABLE_ACCOUNTS = !!getConfigValue('enableUserAccounts', false, 'boolean'); |
| 15 | +const PREFER_REAL_IP_HEADER = !!getConfigValue('rateLimiting.preferRealIpHeader', false, 'boolean'); | |
| 16 | +const BASIC_AUTH_ATTEMPTS = getConfigValue('rateLimiting.basicAuthMaxAttempts', 5, 'number'); | |
| 17 | + | |
| 18 | +const basicAuthLimiter = new RateLimiterMemory({ | |
| 19 | + points: BASIC_AUTH_ATTEMPTS > 0 ? BASIC_AUTH_ATTEMPTS : Number.MAX_SAFE_INTEGER, | |
| 20 | + duration: 60, | |
| 21 | +}); | |
| 13 | 22 | |
| 14 | 23 | const basicAuthMiddleware = async function (request, response, callback) { |
| 15 | - const unauthorizedWebpage = safeReadFileSync(path.join(globalThis.DATA_ROOT, '_errors', 'unauthorized.html')) ?? ''; | |
| 16 | 24 | const unauthorizedResponse = (res) => { |
| 25 | + const unauthorizedWebpage = safeReadFileSync(path.join(globalThis.DATA_ROOT, '_errors', 'unauthorized.html')) ?? ''; | |
| 17 | 26 | res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); |
| 18 | 27 | return res.status(401).send(unauthorizedWebpage); |
| 19 | 28 | }; |
| 20 | 29 | |
| 21 | - const basicAuthUserName = getConfigValue('basicAuthUser.username'); | |
| 30 | + try { | |
| 22 | 31 | const basicAuthUserPasswordip = getConfigValuegetIpAddress('basicAuthUser.password'request, PREFER_REAL_IP_HEADER); |
| 23 | - const authHeader = request.headers.authorization; | |
| 24 | 32 | |
| 25 | - if (!authHeader) { | |
| 33 | + const basicAuthUserName = getConfigValue('basicAuthUser.username'); | |
| 26 | - return unauthorizedResponse(response); | |
| 34 | + const basicAuthUserPassword = getConfigValue('basicAuthUser.password'); | |
| 27 | - } | |
| 35 | + const authHeader = request.headers.authorization; | |
| 36 | + | |
| 37 | + if (!authHeader) { | |
| 38 | + return unauthorizedResponse(response); | |
| 39 | + } | |
| 28 | 40 | |
| 29 | 41 | const [scheme, credentials] = authHeader.split(' '); |
| 30 | 42 | |
| 31 | 43 | if (scheme !== 'Basic' || !credentials) { |
| 32 | 44 | return unauthorizedResponse(response); |
| 33 | 45 | } |
| 34 | 46 | |
| 35 | - const usePerUserAuth = PER_USER_BASIC_AUTH && ENABLE_ACCOUNTS; | |
| 47 | + const rateLimit = await basicAuthLimiter.get(ip); | |
| 36 | - const [username, ...passwordParts] = Buffer.from(credentials, 'base64') | |
| 48 | + | |
| 37 | - .toString('utf8') | |
| 49 | + if (rateLimit !== null && rateLimit.consumedPoints > basicAuthLimiter.points) { | |
| 38 | - .split(':'); | |
| 50 | + throw rateLimit; | |
| 39 | - const password = passwordParts.join(':'); | |
| 51 | + } | |
| 40 | 52 | |
| 41 | - if (!usePerUserAuth && username === basicAuthUserName && password === basicAuthUserPassword) { | |
| 53 | + const usePerUserAuth = PER_USER_BASIC_AUTH && ENABLE_ACCOUNTS; | |
| 42 | - return callback(); | |
| 54 | + const [username, ...passwordParts] = Buffer.from(credentials, 'base64') | |
| 43 | - } else if (usePerUserAuth) { | |
| 55 | + .toString('utf8') | |
| 44 | - const userHandles = await getAllUserHandles(); | |
| 56 | + .split(':'); | |
| 45 | - for (const userHandle of userHandles) { | |
| 57 | + const password = passwordParts.join(':'); | |
| 46 | - if (username === userHandle) { | |
| 58 | + | |
| 47 | - const user = await storage.getItem(toKey(userHandle)); | |
| 59 | + if (!usePerUserAuth && username === basicAuthUserName && password === basicAuthUserPassword) { | |
| 48 | - if (user && user.enabled && (user.password && user.password === getPasswordHash(password, user.salt))) { | |
| 60 | + await basicAuthLimiter.delete(ip); | |
| 49 | 61 | return callback(); |
| 62 | + } else if (usePerUserAuth) { | |
| 63 | + const userHandles = await getAllUserHandles(); | |
| 64 | + for (const userHandle of userHandles) { | |
| 65 | + if (username === userHandle) { | |
| 66 | + const user = await storage.getItem(toKey(userHandle)); | |
| 67 | + if (user && user.enabled && (user.password && user.password === getPasswordHash(password, user.salt))) { | |
| 68 | + await basicAuthLimiter.delete(ip); | |
| 69 | + return callback(); | |
| 70 | + } | |
| 50 | 71 | } |
| 51 | 72 | } |
| 52 | 73 | } |
| 74 | + | |
| 75 | + await basicAuthLimiter.consume(ip); | |
| 76 | + return unauthorizedResponse(response); | |
| 77 | + } catch (error) { | |
| 78 | + if (error instanceof RateLimiterRes) { | |
| 79 | + console.error('Basic auth failed: Rate limited from', getIpAddress(request, PREFER_REAL_IP_HEADER), request.method, request.originalUrl); | |
| 80 | + return retryAfter(response, error).sendStatus(429); | |
| 81 | + } | |
| 82 | + console.error('Basic auth error:', error); | |
| 83 | + return response.sendStatus(500); | |
| 53 | 84 | } |
| 54 | - return unauthorizedResponse(response); | |
| 55 | 85 | }; |
| 56 | 86 | |
| 57 | 87 | export default basicAuthMiddleware; |
| @@ -6,7 +6,7 @@ import Handlebars from 'handlebars'; | ||
| 6 | 6 | import ipMatching from 'ip-matching'; |
| 7 | 7 | import isDocker from 'is-docker'; |
| 8 | 8 | |
| 9 | 9 | import { filterValidIpPatterns, getIpFromRequest, getRealOrForwardedIp } from '../express-common.js'; |
| 10 | 10 | import { color, getConfigValue, safeReadFileSync } from '../util.js'; |
| 11 | 11 | |
| 12 | 12 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); |
| @@ -29,31 +29,6 @@ if (fs.existsSync(whitelistPath)) { | ||
| 29 | 29 | whitelist = filterValidIpPatterns(whitelist, (entry, message) => `${color.red('Warning')}: Ignoring invalid whitelist entry ${color.yellow(entry)} - ${message}`); |
| 30 | 30 | |
| 31 | 31 | /** |
| 32 | - * Get the client IP address from the request headers. | |
| 33 | - * @param {import('express').Request} req Express request object | |
| 34 | - * @returns {string|undefined} The client IP address | |
| 35 | - */ | |
| 36 | -function getForwardedIp(req) { | |
| 37 | - if (!enableForwardedWhitelist) { | |
| 38 | - return undefined; | |
| 39 | - } | |
| 40 | - | |
| 41 | - // Check if X-Real-IP is available | |
| 42 | - if (req.headers['x-real-ip']) { | |
| 43 | - return req.headers['x-real-ip'].toString(); | |
| 44 | - } | |
| 45 | - | |
| 46 | - // Check for X-Forwarded-For and parse if available | |
| 47 | - if (req.headers['x-forwarded-for']) { | |
| 48 | - const ipList = req.headers['x-forwarded-for'].toString().split(',').map(ip => ip.trim()); | |
| 49 | - return ipList[0]; | |
| 50 | - } | |
| 51 | - | |
| 52 | - // If none of the headers are available, return undefined | |
| 53 | - return undefined; | |
| 54 | -} | |
| 55 | - | |
| 56 | -/** | |
| 57 | 32 | * Resolves the IP addresses of Docker hostnames and adds them to the whitelist. |
| 58 | 33 | * @returns {Promise<void>} Promise that resolves when the Docker hostnames are resolved |
| 59 | 34 | */ |
| @@ -92,7 +67,7 @@ export default async function getWhitelistMiddleware() { | ||
| 92 | 67 | |
| 93 | 68 | return function (req, res, next) { |
| 94 | 69 | const clientIp = getIpFromRequest(req); |
| 95 | 70 | const forwardedIp = getForwardedIpenableForwardedWhitelist && getRealOrForwardedIp(req); |
| 96 | 71 | const userAgent = req.headers['user-agent']; |
| 97 | 72 | |
| 98 | 73 | /** |
| @@ -107,7 +82,7 @@ export default async function getWhitelistMiddleware() { | ||
| 107 | 82 | |
| 108 | 83 | //clientIp = req.connection.remoteAddress.split(':').pop(); |
| 109 | 84 | if (!isIPInWhitelist(whitelist, clientIp) |
| 110 | 85 | || (forwardedIp && !isIPInWhitelist(whitelist, forwardedIp)) |
| 111 | 86 | ) { |
| 112 | 87 | // Log the connection attempt with real IP address |
| 113 | 88 | const ipDetails = forwardedIp |