Merge branch 'staging' into immutable-config

This commit is contained in:
Cohee
2025-02-20 21:54:41 +02:00
3 changed files with 29 additions and 7 deletions

View File

@@ -83,6 +83,11 @@ sessionTimeout: -1
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

View File

@@ -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, 'boolean');
const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false, 'boolean');
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.' });
}

View File

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