diff --git a/package.json b/package.json index 16f50a1..102c00e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "dependencies": { "@tailwindcss/cli": "^4.1.11", "dotenv": "^16.4.5", - "express": "^4.21.0" + "express": "^4.21.0", + "express-rate-limit": "^7.5.1" }, "devDependencies": { "nodemon": "^3.1.7", diff --git a/public/js/app.js b/public/js/app.js index ff25288..28d5bf7 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -49,7 +49,12 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => { try { 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(); statusResult.classList.remove('hidden'); @@ -91,10 +96,17 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => { } } catch (error) { statusResult.classList.remove('hidden'); - statusContent.innerHTML = ` -
Status: Error
-Error: An error occurred while checking the server status
- `; + if (error.message === 'RateLimit') { + statusContent.innerHTML = ` +Status: Rate Limited
+Message: You are sending requests too quickly. Please try again in a moment.
+ `; + } else { + statusContent.innerHTML = ` +Status: Error
+Error: An error occurred while checking the server status
+ `; + } } finally { // Hide loading spinner and re-enable button loadingSpinner.style.display = 'none'; diff --git a/status-site.js b/status-site.js index ebd9e76..a2093fd 100644 --- a/status-site.js +++ b/status-site.js @@ -1,12 +1,40 @@ const express = require('express'); const { promisify } = require('util'); const { exec } = require('child_process'); +const rateLimit = require('express-rate-limit'); const app = express(); const port = 3066; require('dotenv').config(); 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) { try { 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; try { const result = await checkGeyserStatus(host, port); - console.log(result) + console.log(result); res.json(result); } catch (error) { res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` });