Merge branch 'staging' into immutable-config

eb31d7baa2a0d1f0441902961ffb8bdfad6ac27e

Cohee <18619528+Cohee1207@users.noreply.github.com>

3 files changed, +29 -7Ignore whitespace
default/config.yaml+5 -0
@@ -83,6 +83,11 @@ sessionTimeout: -1
8383disableCsrfProtection: false
8484# Disable startup security checks - NOT RECOMMENDED
8585securityOverride: false
86+# -- RATE LIMITING CONFIGURATION --
87+rateLimiting:
88+ # Use X-Real-IP header instead of socket IP for rate limiting
89+ # Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
90+ preferRealIpHeader: false
8691# -- ADVANCED CONFIGURATION --
8792# Open the browser automatically
8893autorun: true
src/endpoints/users-public.js+10 -7
@@ -3,13 +3,16 @@ import crypto from 'node:crypto';
33import storage from 'node-persist';
44import express from 'express';
55import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
66import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js';
77import { color, Cache, getConfigValue } from '../util.js';
88import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
99
1010const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false, 'boolean');
11+const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false, 'boolean');
1112const MFA_CACHE = new Cache(5 * 60 * 1000);
1213
14+const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request);
15+
1316export const router = express.Router();
1417const loginLimiter = new RateLimiterMemory({
1518 points: 5,
@@ -60,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
6063 return response.status(400).json({ error: 'Missing required fields' });
6164 }
6265
6366 const ip = getIpFromRequestgetIpAddress(request);
6467 await loginLimiter.consume(ip);
6568
6669 /** @type {import('../users.js').User} */
@@ -92,7 +95,7 @@ router.post('/login', jsonParser, async (request, response) => {
9295 return response.json({ handle: user.handle });
9396 } catch (error) {
9497 if (error instanceof RateLimiterRes) {
9598 console.error('Login failed: Rate limited from', getIpFromRequestgetIpAddress(request));
9699 return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' });
97100 }
98101
@@ -108,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
108111 return response.status(400).json({ error: 'Missing required fields' });
109112 }
110113
111114 const ip = getIpFromRequestgetIpAddress(request);
112115 await recoverLimiter.consume(ip);
113116
114117 /** @type {import('../users.js').User} */
@@ -132,7 +135,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
132135 return response.sendStatus(204);
133136 } catch (error) {
134137 if (error instanceof RateLimiterRes) {
135138 console.error('Recover step 1 failed: Rate limited from', getIpFromRequestgetIpAddress(request));
136139 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
137140 }
138141
@@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
150153
151154 /** @type {import('../users.js').User} */
152155 const user = await storage.getItem(toKey(request.body.handle));
153156 const ip = getIpFromRequestgetIpAddress(request);
154157
155158 if (!user) {
156159 console.error('Recover step 2 failed: User', request.body.handle, 'not found');
@@ -186,7 +189,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
186189 return response.sendStatus(204);
187190 } catch (error) {
188191 if (error instanceof RateLimiterRes) {
189192 console.error('Recover step 2 failed: Rate limited from', getIpFromRequestgetIpAddress(request));
190193 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
191194 }
192195
src/express-common.js+14 -0
@@ -25,3 +25,17 @@ export function getIpFromRequest(req) {
2525 }
2626 return clientIp;
2727}
28+
29+/**
30+ * Gets the IP address of the client when behind reverse proxy using x-real-ip header, falls back to socket remote address.
31+ * This function should be used when the application is running behind a reverse proxy (e.g., Nginx, traefik, Caddy...).
32+ * @param {import('express').Request} req Request object
33+ * @returns {string} IP address of the client
34+ */
35+export function getRealIpFromHeader(req) {
36+ if (req.headers['x-real-ip']) {
37+ return req.headers['x-real-ip'].toString();
38+ }
39+
40+ return getIpFromRequest(req);
41+}