mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Feature: Add configurable X-Real-IP header support for rate limiting (#3504)
* fix: correct client IP detection behind reverse proxy
* Revert "fix: correct client IP detection behind reverse proxy"
This reverts commit 7207506240
.
* feat: support X-Real-IP header for reverse proxy setups
* feat: add option to use x-real-ip for rate limiting behind reverse proxy
* docs: update rate limiting configuration comments for X-Real-IP usage
* refactor: extract getIpAddress function to reduce code duplication
* revert(whitelist): rate limit settings shouldn't affect whitelist
This commit is contained in:
@ -85,6 +85,11 @@ cookieSecret: ''
|
||||
disableCsrfProtection: false
|
||||
# Disable startup security checks - NOT RECOMMENDED
|
||||
securityOverride: false
|
||||
# -- RATE LIMITING CONFIGURATION --
|
||||
rateLimiting:
|
||||
# Use X-Real-IP header instead of socket IP for rate limiting
|
||||
# Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
|
||||
preferRealIpHeader: false
|
||||
# -- ADVANCED CONFIGURATION --
|
||||
# Open the browser automatically
|
||||
autorun: true
|
||||
|
@ -3,13 +3,16 @@ import crypto from 'node:crypto';
|
||||
import storage from 'node-persist';
|
||||
import express from 'express';
|
||||
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
|
||||
import { jsonParser, getIpFromRequest } from '../express-common.js';
|
||||
import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js';
|
||||
import { color, Cache, getConfigValue } from '../util.js';
|
||||
import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
|
||||
|
||||
const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);
|
||||
const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false);
|
||||
const MFA_CACHE = new Cache(5 * 60 * 1000);
|
||||
|
||||
const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request);
|
||||
|
||||
export const router = express.Router();
|
||||
const loginLimiter = new RateLimiterMemory({
|
||||
points: 5,
|
||||
@ -60,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
|
||||
return response.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const ip = getIpFromRequest(request);
|
||||
const ip = getIpAddress(request);
|
||||
await loginLimiter.consume(ip);
|
||||
|
||||
/** @type {import('../users.js').User} */
|
||||
@ -92,7 +95,7 @@ router.post('/login', jsonParser, async (request, response) => {
|
||||
return response.json({ handle: user.handle });
|
||||
} catch (error) {
|
||||
if (error instanceof RateLimiterRes) {
|
||||
console.error('Login failed: Rate limited from', getIpFromRequest(request));
|
||||
console.error('Login failed: Rate limited from', getIpAddress(request));
|
||||
return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' });
|
||||
}
|
||||
|
||||
@ -108,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
|
||||
return response.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const ip = getIpFromRequest(request);
|
||||
const ip = getIpAddress(request);
|
||||
await recoverLimiter.consume(ip);
|
||||
|
||||
/** @type {import('../users.js').User} */
|
||||
@ -132,7 +135,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
|
||||
return response.sendStatus(204);
|
||||
} catch (error) {
|
||||
if (error instanceof RateLimiterRes) {
|
||||
console.error('Recover step 1 failed: Rate limited from', getIpFromRequest(request));
|
||||
console.error('Recover step 1 failed: Rate limited from', getIpAddress(request));
|
||||
return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
|
||||
}
|
||||
|
||||
@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
|
||||
|
||||
/** @type {import('../users.js').User} */
|
||||
const user = await storage.getItem(toKey(request.body.handle));
|
||||
const ip = getIpFromRequest(request);
|
||||
const ip = getIpAddress(request);
|
||||
|
||||
if (!user) {
|
||||
console.error('Recover step 2 failed: User', request.body.handle, 'not found');
|
||||
@ -186,7 +189,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
|
||||
return response.sendStatus(204);
|
||||
} catch (error) {
|
||||
if (error instanceof RateLimiterRes) {
|
||||
console.error('Recover step 2 failed: Rate limited from', getIpFromRequest(request));
|
||||
console.error('Recover step 2 failed: Rate limited from', getIpAddress(request));
|
||||
return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
|
||||
}
|
||||
|
||||
|
@ -25,3 +25,17 @@ export function getIpFromRequest(req) {
|
||||
}
|
||||
return clientIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IP address of the client when behind reverse proxy using x-real-ip header, falls back to socket remote address.
|
||||
* This function should be used when the application is running behind a reverse proxy (e.g., Nginx, traefik, Caddy...).
|
||||
* @param {import('express').Request} req Request object
|
||||
* @returns {string} IP address of the client
|
||||
*/
|
||||
export function getRealIpFromHeader(req) {
|
||||
if (req.headers['x-real-ip']) {
|
||||
return req.headers['x-real-ip'].toString();
|
||||
}
|
||||
|
||||
return getIpFromRequest(req);
|
||||
}
|
||||
|
Reference in New Issue
Block a user