52 lines
1.4 KiB
JavaScript
52 lines
1.4 KiB
JavaScript
'use strict';
|
|
|
|
// A rate limit ensures that a request parameter gets flagged if it goes
|
|
// above a limit.
|
|
module.exports = class RateLimit {
|
|
constructor(options = {}) {
|
|
// this.hits: Map from request parameters to the number of hits.
|
|
this.hits = new Map();
|
|
this.period = options.period || 200; // 3 min ⅓, in seconds
|
|
this.maxHitsPerPeriod = options.maxHitsPerPeriod || 500;
|
|
this.banned = new Set();
|
|
this.bannedUrls = new Set();
|
|
this.whitelist = options.whitelist
|
|
|| /(?!)/; // Matches nothing by default.
|
|
setInterval(this.resetHits.bind(this), this.period * 1000);
|
|
}
|
|
|
|
resetHits() {
|
|
this.hits.clear();
|
|
this.banned.clear();
|
|
this.bannedUrls.clear();
|
|
}
|
|
|
|
isBanned(reqParam, req, res) {
|
|
const hitsInCurrentPeriod = this.hits.get(reqParam) || 0;
|
|
if ((reqParam != null) && !this.whitelist.test(reqParam)
|
|
&& (hitsInCurrentPeriod > this.maxHitsPerPeriod)) {
|
|
this.banned.add(reqParam);
|
|
}
|
|
|
|
if (this.banned.has(reqParam)) {
|
|
res.statusCode = 429;
|
|
res.setHeader('Retry-After', String(this.period));
|
|
res.end(`Exceeded limit ${this.maxHitsPerPeriod} requests ` +
|
|
`per ${this.period} seconds`);
|
|
this.bannedUrls.add(req.url);
|
|
return true;
|
|
}
|
|
|
|
this.hits.set(reqParam, hitsInCurrentPeriod + 1);
|
|
return false;
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
banned: [...this.banned],
|
|
hits: [...this.hits],
|
|
urls: [...this.bannedUrls],
|
|
};
|
|
}
|
|
}
|