feat: Add request rate limiter based on IP address (#8174)

This commit is contained in:
Daniel
2023-01-06 23:39:02 +11:00
committed by GitHub
parent 0eac5dc6d4
commit 6c79f6a69e
13 changed files with 713 additions and 50 deletions

View File

@@ -7,6 +7,9 @@ import defaultLogger from './logger';
import rest from './rest';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter';
import rateLimit from 'express-rate-limit';
import { RateLimitOptions } from './Options/Definitions';
import pathToRegexp from 'path-to-regexp';
import ipRangeCheck from 'ip-range-check';
export const DEFAULT_ALLOWED_HEADERS =
@@ -189,8 +192,7 @@ export function handleParseHeaders(req, res, next) {
installationId: info.installationId,
isMaster: true,
});
next();
return;
return handleRateLimit(req, res, next);
}
var isReadOnlyMaster = info.masterKey === req.config.readOnlyMasterKey;
@@ -205,8 +207,7 @@ export function handleParseHeaders(req, res, next) {
isMaster: true,
isReadOnly: true,
});
next();
return;
return handleRateLimit(req, res, next);
}
// Client keys are not required in parse-server, but if any have been configured in the server, validate them
@@ -234,8 +235,7 @@ export function handleParseHeaders(req, res, next) {
isMaster: false,
user: req.userFromJWT,
});
next();
return;
return handleRateLimit(req, res, next);
}
if (!info.sessionToken) {
@@ -244,48 +244,70 @@ export function handleParseHeaders(req, res, next) {
installationId: info.installationId,
isMaster: false,
});
next();
}
handleRateLimit(req, res, next);
}
const handleRateLimit = async (req, res, next) => {
const rateLimits = req.config.rateLimits || [];
try {
await Promise.all(
rateLimits.map(async limit => {
const pathExp = new RegExp(limit.path);
if (pathExp.test(req.url)) {
await limit.handler(req, res, err => {
if (err) {
throw err;
}
});
}
})
);
} catch (error) {
res.status(429);
res.json({ code: Parse.Error.CONNECTION_FAILED, error });
return;
}
next();
};
return Promise.resolve()
.then(() => {
// handle the upgradeToRevocableSession path on it's own
if (
info.sessionToken &&
req.url === '/upgradeToRevocableSession' &&
info.sessionToken.indexOf('r:') != 0
) {
return auth.getAuthForLegacySessionToken({
config: req.config,
installationId: info.installationId,
sessionToken: info.sessionToken,
});
} else {
return auth.getAuthForSessionToken({
config: req.config,
installationId: info.installationId,
sessionToken: info.sessionToken,
});
}
})
.then(auth => {
if (auth) {
req.auth = auth;
next();
}
})
.catch(error => {
if (error instanceof Parse.Error) {
next(error);
return;
} else {
// TODO: Determine the correct error scenario.
req.config.loggerController.error('error getting auth for sessionToken', error);
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
}
});
}
export const handleParseSession = async (req, res, next) => {
try {
const info = req.info;
if (req.auth) {
next();
return;
}
let requestAuth = null;
if (
info.sessionToken &&
req.url === '/upgradeToRevocableSession' &&
info.sessionToken.indexOf('r:') != 0
) {
requestAuth = await auth.getAuthForLegacySessionToken({
config: req.config,
installationId: info.installationId,
sessionToken: info.sessionToken,
});
} else {
requestAuth = await auth.getAuthForSessionToken({
config: req.config,
installationId: info.installationId,
sessionToken: info.sessionToken,
});
}
req.auth = requestAuth;
next();
} catch (error) {
if (error instanceof Parse.Error) {
next(error);
return;
}
// TODO: Determine the correct error scenario.
req.config.loggerController.error('error getting auth for sessionToken', error);
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
}
};
function getClientIp(req) {
return req.ip;
@@ -417,6 +439,56 @@ export function promiseEnforceMasterKeyAccess(request) {
return Promise.resolve();
}
export const addRateLimit = (route, config) => {
if (typeof config === 'string') {
config = Config.get(config);
}
for (const key in route) {
if (!RateLimitOptions[key]) {
throw `Invalid rate limit option "${key}"`;
}
}
if (!config.rateLimits) {
config.rateLimits = [];
}
config.rateLimits.push({
path: pathToRegexp(route.requestPath),
handler: rateLimit({
windowMs: route.requestTimeWindow,
max: route.requestCount,
message: route.errorResponseMessage || RateLimitOptions.errorResponseMessage.default,
handler: (request, response, next, options) => {
throw options.message;
},
skip: request => {
if (request.ip === '127.0.0.1' && !route.includeInternalRequests) {
return true;
}
if (route.includeMasterKey) {
return false;
}
if (route.requestMethods) {
if (Array.isArray(route.requestMethods)) {
if (!route.requestMethods.includes(request.method)) {
return true;
}
} else {
const regExp = new RegExp(route.requestMethods);
if (!regExp.test(request.method)) {
return true;
}
}
}
return request.auth.isMaster;
},
keyGenerator: request => {
return request.config.ip;
},
}),
});
Config.put(config);
};
/**
* Deduplicates a request to ensure idempotency. Duplicates are determined by the request ID
* in the request header. If a request has no request ID, it is executed anyway.