diff --git a/.gitignore b/.gitignore index c6bba59..e0938cc 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Config stuff +config.json \ No newline at end of file diff --git a/config.json.default b/config.json.default new file mode 100644 index 0000000..8b15ed3 --- /dev/null +++ b/config.json.default @@ -0,0 +1,7 @@ +{ + "updateInterval": 60, + "rateLimiterEnabled": true, + "rateLimitWindow": 1, + "rateLimitMax": 30, + "behindProxy": false +} \ No newline at end of file diff --git a/index.js b/index.js index 8b02429..7aaa974 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,7 @@ I'll try to comment it as best as I can, but I'm not the best at explaining thin const Steam = require("steam-server-query") const express = require('express'); +const rateLimit = require('express-rate-limit'); const colors = require("colors"); const semver = require("semver"); const childProcess = require('child_process'); @@ -342,7 +343,26 @@ setInterval(() => { updateMasterList(); }, config.updateInterval * 1000); updateMasterList(); +if (config.rateLimiterEnabled) { + app.use(rateLimit({ + windowMs: config.rateLimitWindow * 60 * 1000, // X minutes + max: config.rateLimitMax, // limit each IP to X requests per windowMs. + keyGenerator: function (req) { + return config.behindProxy ? req.headers['x-real-ip'] : req.ip; + }, + skipFailedRequests: true, + handler: function (req, res /*, next*/ ) { + const remainingTime = Math.round((req.rateLimit.resetTime - Date.now()) / 1000); + res.status(429).json({ + error: 'Too Many Requests', + message: `You have exceeded the rate limit. Please try again in ${remainingTime} seconds.`, + remainingTime: remainingTime + }); + console.log(`${colors.red(`[ERROR ${new Date()}]`)} ${req.headers["user-agent"]}@${req.ip} exceeded rate limit!`); + } + })); +} app.get('/check', (req, res) => { // Check that all required parameters are present if (!req.query.address) { @@ -423,6 +443,8 @@ app.get('/', (req, res) => { "repo": "https://github.com/TerraDevelopers/TerraStatusAPI", "commit": getGitCommitDetails() }, + // Rate limit X requests per Y minutes per IP, as a string + "rateLimit": `${config.rateLimitMax} requests per ${config.rateLimitWindow} minutes`, "debug": { // "yourIP" Either the IP of the user, or the IP of the proxy if one is used, proxy IP header is x-real-ip "yourIP": req.headers["x-real-ip"] || req.ip, diff --git a/package-lock.json b/package-lock.json index 5f552e9..a536c86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "colors": "^1.4.0", "express": "^4.18.2", + "express-rate-limit": "^6.7.0", "semver": "^7.3.8", "steam-server-query": "^1.1.3" } @@ -207,6 +208,17 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "engines": { + "node": ">= 12.9.0" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -787,6 +799,12 @@ "vary": "~1.1.2" } }, + "express-rate-limit": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "requires": {} + }, "finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", diff --git a/package.json b/package.json index 349dddf..402be82 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "colors": "^1.4.0", "express": "^4.18.2", + "express-rate-limit": "^6.7.0", "semver": "^7.3.8", "steam-server-query": "^1.1.3" }