Add rate limit with Exponential Backoff

This commit is contained in:
MCHost
2025-07-03 01:13:54 -04:00
parent 96c6211222
commit 28d9d8aff5
3 changed files with 48 additions and 7 deletions

View File

@ -11,7 +11,8 @@
"dependencies": { "dependencies": {
"@tailwindcss/cli": "^4.1.11", "@tailwindcss/cli": "^4.1.11",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.0" "express": "^4.21.0",
"express-rate-limit": "^7.5.1"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.7", "nodemon": "^3.1.7",

View File

@ -49,7 +49,12 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => {
try { try {
const response = await fetch(`/${edition}/${host}/${port}`); const response = await fetch(`/${edition}/${host}/${port}`);
if (!response.ok) throw new Error('Request failed'); if (!response.ok) {
if (response.status === 429) {
throw new Error('RateLimit');
}
throw new Error('Request failed');
}
const result = await response.json(); const result = await response.json();
statusResult.classList.remove('hidden'); statusResult.classList.remove('hidden');
@ -91,10 +96,17 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => {
} }
} catch (error) { } catch (error) {
statusResult.classList.remove('hidden'); statusResult.classList.remove('hidden');
if (error.message === 'RateLimit') {
statusContent.innerHTML = `
<p><strong class="text-blue-400">Status:</strong> <span class="text-yellow-400">Rate Limited</span></p>
<p><strong class="text-blue-400">Message:</strong> You are sending requests too quickly. Please try again in a moment.</p>
`;
} else {
statusContent.innerHTML = ` statusContent.innerHTML = `
<p><strong class="text-blue-400">Status:</strong> <span class="text-red-400">Error</span></p> <p><strong class="text-blue-400">Status:</strong> <span class="text-red-400">Error</span></p>
<p><strong class="text-blue-400">Error:</strong> An error occurred while checking the server status</p> <p><strong class="text-blue-400">Error:</strong> An error occurred while checking the server status</p>
`; `;
}
} finally { } finally {
// Hide loading spinner and re-enable button // Hide loading spinner and re-enable button
loadingSpinner.style.display = 'none'; loadingSpinner.style.display = 'none';

View File

@ -1,12 +1,40 @@
const express = require('express'); const express = require('express');
const { promisify } = require('util'); const { promisify } = require('util');
const { exec } = require('child_process'); const { exec } = require('child_process');
const rateLimit = require('express-rate-limit');
const app = express(); const app = express();
const port = 3066; const port = 3066;
require('dotenv').config(); require('dotenv').config();
const execPromise = promisify(exec); const execPromise = promisify(exec);
// Custom key generator to support Cloudflare's CF-Connecting-IP header
const getClientIp = (req) => {
return req.headers['cf-connecting-ip'] || req.ip;
};
// Rate limiter configuration: 1 request per second with exponential backoff
const limiter = rateLimit({
windowMs: 1000, // 1 second
max: 1, // 1 request per IP
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false, // Disable X-RateLimit headers
keyGenerator: getClientIp, // Use CF-Connecting-IP or fallback to req.ip
handler: (req, res) => {
// Calculate backoff time (exponential, starting at 2 seconds)
const retryAfter = Math.pow(2, Math.min(req.rateLimit.current - req.rateLimit.limit, 5));
res.setHeader('Retry-After', retryAfter);
res.status(429).json({
error: 'Too many requests, please try again later.',
retryAfter: retryAfter
});
}
});
// Apply rate limiting to API routes only
app.use('/java', limiter);
app.use('/bedrock', limiter);
async function checkConnectionStatus(hostname, port) { async function checkConnectionStatus(hostname, port) {
try { try {
const command = `${process.env.STATUS_CHECK_PATH} -host ${hostname} -port ${port}`; const command = `${process.env.STATUS_CHECK_PATH} -host ${hostname} -port ${port}`;
@ -55,7 +83,7 @@ app.get('/bedrock/:host/:port', async (req, res) => {
const { host, port } = req.params; const { host, port } = req.params;
try { try {
const result = await checkGeyserStatus(host, port); const result = await checkGeyserStatus(host, port);
console.log(result) console.log(result);
res.json(result); res.json(result);
} catch (error) { } catch (error) {
res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` }); res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` });