Add rate limiting
We now rate limit IPs, referers and badge type.
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
const secretIsValid = require('./secret-is-valid');
|
||||
const serverSecrets = require('../server-secrets');
|
||||
const config = require('../server-config');
|
||||
const RateLimit = require('./rate-limit');
|
||||
const log = require('../log');
|
||||
|
||||
function secretInvalid(req, res) {
|
||||
@@ -16,10 +18,29 @@ function secretInvalid(req, res) {
|
||||
}
|
||||
|
||||
function setRoutes(server) {
|
||||
server.handle(function(req, res, next) {
|
||||
// Whitelist GitHub IPs.
|
||||
const ipRateLimit = new RateLimit({ whitelist: /^192\.30\.252\.\d+$/ });
|
||||
const badgeTypeRateLimit = new RateLimit();
|
||||
const refererRateLimit = new RateLimit({
|
||||
whitelist: /^https?:\/\/shields\.io\/$/,
|
||||
});
|
||||
|
||||
server.handle(function monitorHandler(req, res, next) {
|
||||
if (req.url.startsWith('/sys/')) {
|
||||
if (secretInvalid(req, res)) { return; }
|
||||
}
|
||||
|
||||
if (config.rateLimit) {
|
||||
const ip = (req.headers['x-forwarded-for'] || '').split(', ')[0]
|
||||
|| req.socket.remoteAddress;
|
||||
const badgeType = req.url.split(/[/-]/).slice(0, 3).join('');
|
||||
const referer = req.headers['referer'];
|
||||
|
||||
if (ipRateLimit.isBanned(ip, req, res)) { return; }
|
||||
if (badgeTypeRateLimit.isBanned(badgeType, req, res)) { return; }
|
||||
if (refererRateLimit.isBanned(referer, req, res)) { return; }
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -41,6 +62,14 @@ function setRoutes(server) {
|
||||
log.addListener(listener);
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/sys/rate-limit', (req, res) => {
|
||||
res.json({
|
||||
ip: ipRateLimit.toJSON(),
|
||||
badgeType: badgeTypeRateLimit.toJSON(),
|
||||
referer: refererRateLimit.toJSON(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
51
lib/sys/rate-limit.js
Normal file
51
lib/sys/rate-limit.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'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 || 300;
|
||||
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],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user