feat: Add request rate limiter based on IP address (#8174)
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user