Last active
February 19, 2021 02:42
-
-
Save jeremybradbury/7a4d33f8d5da66848ab6c919b927dddf to your computer and use it in GitHub Desktop.
Simple async request rate limit middleware, for ExpressJS endpoints, using Redis with 1 second TTLs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//// | |
// MIT License | |
// Copyright (c) 2021 Jeremy Bradbury | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
//// | |
// requires redis-server running | |
const redis = require("redis"); // requires the redis client in your package.json | |
const { promisify } = require("util"); // all redis client callbacks conform to promisify's requirements | |
const client = redis.createClient(); | |
const cGet = promisify(client.get).bind(client); | |
const cIncr = promisify(client.incr).bind(client); | |
const cExpire = promisify(client.expire).bind(client); | |
const defaultRate = 1; // 1 req per second, per ip | |
// modified from: https://redis.io/commands/incr#pattern-rate-limiter | |
const rateLimit = async (ip, next, reqPerScond = defaultRate) => { | |
const ts = Date.now().toString().slice(0, 10); // seconds beyond epoch, strip the last 3 (ms) | |
const key = `${ip}@${ts}`; // squish ip@timestamp together, doesn't matter how if it's always the same | |
const current = await cGet(key); // were there requests this second? | |
if (current && current > reqPerScond) { | |
// were there too many? | |
return false; // fail: send 429 | |
} | |
await cIncr(key); // make a 1 or add to it | |
// we only need to know about requests in the current second | |
await cExpire(key, 1); // so keep only 1 key/value per public ip, for only 1 second | |
next(); // no need to await | |
return true; // success: next was called | |
}; | |
// 1 req per second, per ip | |
const throttleByIp = async (req, res, next) => { | |
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; // in nginx we trust, all others are suspect! | |
if (!(await rateLimit(ip, next, req.perSecond || defaultRate))) { | |
// next is passed to rate limit | |
return res.sendStatus(429); // 429 - Too Many Requests | |
} // next already called | |
}; // core middleware used by exported wrapper functions | |
// 10 reqs per second, per ip | |
const fastThrottle = async (req, res, next) => { | |
req.perSecond = 10; | |
return await throttleByIp(req, res, next); // return base middleware, rather than requiring both per endpoint | |
}; | |
// 3 reqs per second, per ip | |
const normalThrottle = async (req, res, next) => { | |
req.perSecond = 3; | |
return await throttleByIp(req, res, next); // return base middleware, rather than requiring both per endpoint | |
}; | |
// 1 req per second, per ip | |
const slowThrottle = throttleByIp; // defaultRate === 1, use base middleware | |
// add more, change the rates or strip down | |
module.exports = { | |
fastThrottle, // 10 requests / ip / sec | |
normalThrottle, // 3 requests / ip / sec | |
slowThrottle, // 1 request / ip / sec # equal to throttleByIp() | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment