var error = require("http-error"); var MAX_EXPIRE_INTERVAL = 5000; /** * In memory rate limiter as connect middleware */ module.exports = ratelimit; function ratelimit(key, duration, max) { var requests = Object.create(null); // in case there handles like 'constructor' var rootKey = "params"; if (/^req\./.test(key)) { rootKey = null; key = key.replace(/^req\./, ""); } setInterval(function() { Object.keys(requests).forEach(expireRequests); }, Math.min(duration * 0.75, MAX_EXPIRE_INTERVAL)); function expireRequests(handle) { var requestsForHandle = requests[handle]; var totalToSplice = 0; var expireTime = Date.now() - duration; /* Requests are already sorted by date as they are appended, so we just loop until we find one that shouldn't have expired and splice them from the list */ for (totalToSplice = 0; totalToSplice < requestsForHandle.length; totalToSplice++) { if (requestsForHandle[totalToSplice] >= expireTime) break; } requests[handle].splice(0, totalToSplice); if (requests[handle].length == 0) { delete requests[handle]; } return true; } // Returns a deep value from an object. E.g. resolveValue({user: {id: 5}}, "user.id") === 5 function resolveValue(obj, path) { return path.split('.').reduce(function(prev, curr) { return prev ? prev[curr] : undefined; }, obj); } return function(req, res, next) { var root = rootKey ? req[rootKey] : req; var handle = resolveValue(root, key); requests[handle] = requests[handle] || []; if (requests[handle].length >= max) { var err = new error.TooManyRequests("Rate limit exceeded"); err.retryIn = Math.min(duration, 5000); return next(err); } requests[handle].push(Date.now()); return next(); }; }