feat: Add request rate limiter based on IP address (#8174)
This commit is contained in:
@@ -85,6 +85,7 @@ export class Config {
|
||||
requestKeywordDenylist,
|
||||
allowExpiredAuthDataToken,
|
||||
logLevels,
|
||||
rateLimit,
|
||||
}) {
|
||||
if (masterKey === readOnlyMasterKey) {
|
||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||
@@ -126,6 +127,7 @@ export class Config {
|
||||
this.validateEnforcePrivateUsers(enforcePrivateUsers);
|
||||
this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
|
||||
this.validateRequestKeywordDenylist(requestKeywordDenylist);
|
||||
this.validateRateLimit(rateLimit);
|
||||
this.validateLogLevels(logLevels);
|
||||
}
|
||||
|
||||
@@ -517,6 +519,48 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
static validateRateLimit(rateLimit) {
|
||||
if (!rateLimit) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Object.prototype.toString.call(rateLimit) !== '[object Object]' &&
|
||||
!Array.isArray(rateLimit)
|
||||
) {
|
||||
throw `rateLimit must be an array or object`;
|
||||
}
|
||||
const options = Array.isArray(rateLimit) ? rateLimit : [rateLimit];
|
||||
for (const option of options) {
|
||||
if (Object.prototype.toString.call(option) !== '[object Object]') {
|
||||
throw `rateLimit must be an array of objects`;
|
||||
}
|
||||
if (option.requestPath == null) {
|
||||
throw `rateLimit.requestPath must be defined`;
|
||||
}
|
||||
if (typeof option.requestPath !== 'string') {
|
||||
throw `rateLimit.requestPath must be a string`;
|
||||
}
|
||||
if (option.requestTimeWindow == null) {
|
||||
throw `rateLimit.requestTimeWindow must be defined`;
|
||||
}
|
||||
if (typeof option.requestTimeWindow !== 'number') {
|
||||
throw `rateLimit.requestTimeWindow must be a number`;
|
||||
}
|
||||
if (option.includeInternalRequests && typeof option.includeInternalRequests !== 'boolean') {
|
||||
throw `rateLimit.includeInternalRequests must be a boolean`;
|
||||
}
|
||||
if (option.requestCount == null) {
|
||||
throw `rateLimit.requestCount must be defined`;
|
||||
}
|
||||
if (typeof option.requestCount !== 'number') {
|
||||
throw `rateLimit.requestCount must be a number`;
|
||||
}
|
||||
if (option.errorResponseMessage && typeof option.errorResponseMessage !== 'string') {
|
||||
throw `rateLimit.errorResponseMessage must be a string`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateEmailVerifyTokenExpiresAt() {
|
||||
if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) {
|
||||
return undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@ import corsMiddleware from 'cors';
|
||||
import { createServer, renderGraphiQL } from '@graphql-yoga/node';
|
||||
import { execute, subscribe } from 'graphql';
|
||||
import { SubscriptionServer } from 'subscriptions-transport-ws';
|
||||
import { handleParseErrors, handleParseHeaders } from '../middlewares';
|
||||
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
|
||||
import requiredParameter from '../requiredParameter';
|
||||
import defaultLogger from '../logger';
|
||||
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
|
||||
@@ -82,6 +82,7 @@ class ParseGraphQLServer {
|
||||
|
||||
app.use(this.config.graphQLPath, corsMiddleware());
|
||||
app.use(this.config.graphQLPath, handleParseHeaders);
|
||||
app.use(this.config.graphQLPath, handleParseSession);
|
||||
app.use(this.config.graphQLPath, handleParseErrors);
|
||||
app.use(this.config.graphQLPath, async (req, res) => {
|
||||
const server = await this._getServer();
|
||||
|
||||
@@ -411,6 +411,13 @@ module.exports.ParseServerOptions = {
|
||||
'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
rateLimit: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT',
|
||||
help:
|
||||
"Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.<br><br>\u2139\uFE0F Mind the following limitations:<br>- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses<br>- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable<br>- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.",
|
||||
action: parsers.arrayParser,
|
||||
default: [],
|
||||
},
|
||||
readOnlyMasterKey: {
|
||||
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY',
|
||||
help: 'Read-only key, which has the same capabilities as MasterKey without writes',
|
||||
@@ -516,6 +523,52 @@ module.exports.ParseServerOptions = {
|
||||
help: 'Key sent with outgoing webhook calls',
|
||||
},
|
||||
};
|
||||
module.exports.RateLimitOptions = {
|
||||
errorResponseMessage: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE',
|
||||
help:
|
||||
'The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.',
|
||||
default: 'Too many requests.',
|
||||
},
|
||||
includeInternalRequests: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS',
|
||||
help:
|
||||
'Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
includeMasterKey: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_MASTER_KEY',
|
||||
help:
|
||||
'Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
requestCount: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT',
|
||||
help:
|
||||
'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied.',
|
||||
action: parsers.numberParser('requestCount'),
|
||||
},
|
||||
requestMethods: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS',
|
||||
help:
|
||||
'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.',
|
||||
action: parsers.arrayParser,
|
||||
},
|
||||
requestPath: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_PATH',
|
||||
help:
|
||||
'The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html',
|
||||
required: true,
|
||||
},
|
||||
requestTimeWindow: {
|
||||
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW',
|
||||
help:
|
||||
'The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.',
|
||||
action: parsers.numberParser('requestTimeWindow'),
|
||||
},
|
||||
};
|
||||
module.exports.SecurityOptions = {
|
||||
checkGroups: {
|
||||
env: 'PARSE_SERVER_SECURITY_CHECK_GROUPS',
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
* @property {ProtectedFields} protectedFields Protected fields that should be treated with extra security when fetching details.
|
||||
* @property {String} publicServerURL Public URL to your parse server with http:// or https://.
|
||||
* @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications
|
||||
* @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.<br><br>ℹ️ Mind the following limitations:<br>- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses<br>- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable<br>- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.
|
||||
* @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes
|
||||
* @property {RequestKeywordDenylist[]} requestKeywordDenylist An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.
|
||||
* @property {String} restAPIKey Key for REST calls
|
||||
@@ -96,6 +97,17 @@
|
||||
* @property {String} webhookKey Key sent with outgoing webhook calls
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface RateLimitOptions
|
||||
* @property {String} errorResponseMessage The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.
|
||||
* @property {Boolean} includeInternalRequests Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.
|
||||
* @property {Boolean} includeMasterKey Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.
|
||||
* @property {Number} requestCount The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied.
|
||||
* @property {String[]} requestMethods Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.
|
||||
* @property {String} requestPath The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html
|
||||
* @property {Number} requestTimeWindow The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface SecurityOptions
|
||||
* @property {CheckGroup[]} checkGroups The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.
|
||||
|
||||
@@ -292,6 +292,29 @@ export interface ParseServerOptions {
|
||||
/* An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.
|
||||
:DEFAULT: [{"key":"_bsontype","value":"Code"},{"key":"constructor"},{"key":"__proto__"}] */
|
||||
requestKeywordDenylist: ?(RequestKeywordDenylist[]);
|
||||
/* Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.<br><br>ℹ️ Mind the following limitations:<br>- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses<br>- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable<br>- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.
|
||||
:DEFAULT: [] */
|
||||
rateLimit: ?(RateLimitOptions[]);
|
||||
}
|
||||
|
||||
export interface RateLimitOptions {
|
||||
/* The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html */
|
||||
requestPath: string;
|
||||
/* The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied. */
|
||||
requestTimeWindow: ?number;
|
||||
/* The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. */
|
||||
requestCount: ?number;
|
||||
/* The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.
|
||||
:DEFAULT: Too many requests. */
|
||||
errorResponseMessage: ?string;
|
||||
/* Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. */
|
||||
requestMethods: ?(string[]);
|
||||
/* Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.
|
||||
:DEFAULT: false */
|
||||
includeMasterKey: ?boolean;
|
||||
/* Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.
|
||||
:DEFAULT: false */
|
||||
includeInternalRequests: ?boolean;
|
||||
}
|
||||
|
||||
export interface SecurityOptions {
|
||||
|
||||
@@ -179,7 +179,7 @@ class ParseServer {
|
||||
* Create an express app for the parse server
|
||||
* @param {Object} options let you specify the maxUploadSize when creating the express app */
|
||||
static app(options) {
|
||||
const { maxUploadSize = '20mb', appId, directAccess, pages } = options;
|
||||
const { maxUploadSize = '20mb', appId, directAccess, pages, rateLimit = [] } = options;
|
||||
// This app serves the Parse API directly.
|
||||
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
|
||||
var api = express();
|
||||
@@ -214,6 +214,11 @@ class ParseServer {
|
||||
api.use(bodyParser.json({ type: '*/*', limit: maxUploadSize }));
|
||||
api.use(middlewares.allowMethodOverride);
|
||||
api.use(middlewares.handleParseHeaders);
|
||||
const routes = Array.isArray(rateLimit) ? rateLimit : [rateLimit];
|
||||
for (const route of routes) {
|
||||
middlewares.addRateLimit(route, options);
|
||||
}
|
||||
api.use(middlewares.handleParseSession);
|
||||
|
||||
const appRouter = ParseServer.promiseRouter({ appId });
|
||||
api.use(appRouter.expressRouter());
|
||||
|
||||
@@ -53,12 +53,14 @@ export class FilesRouter {
|
||||
limit: maxUploadSize,
|
||||
}), // Allow uploads without Content-Type, or with any Content-Type.
|
||||
Middlewares.handleParseHeaders,
|
||||
Middlewares.handleParseSession,
|
||||
this.createHandler
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/files/:filename',
|
||||
Middlewares.handleParseHeaders,
|
||||
Middlewares.handleParseSession,
|
||||
Middlewares.enforceMasterKeyAccess,
|
||||
this.deleteHandler
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import * as triggers from '../triggers';
|
||||
import Deprecator from '../Deprecator/Deprecator';
|
||||
import { addRateLimit } from '../middlewares';
|
||||
const Config = require('../Config');
|
||||
|
||||
function isParseObjectConstructor(object) {
|
||||
@@ -28,6 +29,7 @@ function validateValidator(validator) {
|
||||
skipWithMasterKey: [Boolean],
|
||||
requireUserKeys: [Array, Object],
|
||||
fields: [Array, Object],
|
||||
rateLimit: [Object],
|
||||
};
|
||||
const getType = fn => {
|
||||
if (Array.isArray(fn)) {
|
||||
@@ -72,6 +74,18 @@ function validateValidator(validator) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const getRoute = parseClass => {
|
||||
const route =
|
||||
{
|
||||
_User: 'users',
|
||||
_Session: 'sessions',
|
||||
'@File': 'files',
|
||||
}[parseClass] || 'classes';
|
||||
if (parseClass === '@File') {
|
||||
return `/${route}/:id?*`;
|
||||
}
|
||||
return `/${route}/${parseClass}/:id?*`;
|
||||
};
|
||||
/** @namespace
|
||||
* @name Parse
|
||||
* @description The Parse SDK.
|
||||
@@ -111,6 +125,12 @@ var ParseCloud = {};
|
||||
ParseCloud.define = function (functionName, handler, validationHandler) {
|
||||
validateValidator(validationHandler);
|
||||
triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId);
|
||||
if (validationHandler && validationHandler.rateLimit) {
|
||||
addRateLimit(
|
||||
{ requestPath: `/functions/${functionName}`, ...validationHandler.rateLimit },
|
||||
Parse.applicationId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -164,6 +184,16 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) {
|
||||
Parse.applicationId,
|
||||
validationHandler
|
||||
);
|
||||
if (validationHandler && validationHandler.rateLimit) {
|
||||
addRateLimit(
|
||||
{
|
||||
requestPath: getRoute(className),
|
||||
requestMethods: ['POST', 'PUT'],
|
||||
...validationHandler.rateLimit,
|
||||
},
|
||||
Parse.applicationId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -200,6 +230,16 @@ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) {
|
||||
Parse.applicationId,
|
||||
validationHandler
|
||||
);
|
||||
if (validationHandler && validationHandler.rateLimit) {
|
||||
addRateLimit(
|
||||
{
|
||||
requestPath: getRoute(className),
|
||||
requestMethods: 'DELETE',
|
||||
...validationHandler.rateLimit,
|
||||
},
|
||||
Parse.applicationId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -225,15 +265,22 @@ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) {
|
||||
* @name Parse.Cloud.beforeLogin
|
||||
* @param {Function} func The function to run before a login. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest};
|
||||
*/
|
||||
ParseCloud.beforeLogin = function (handler) {
|
||||
ParseCloud.beforeLogin = function (handler, validationHandler) {
|
||||
let className = '_User';
|
||||
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
||||
// validation will occur downstream, this is to maintain internal
|
||||
// code consistency with the other hook types.
|
||||
className = triggers.getClassName(handler);
|
||||
handler = arguments[1];
|
||||
validationHandler = arguments.length >= 2 ? arguments[2] : null;
|
||||
}
|
||||
triggers.addTrigger(triggers.Types.beforeLogin, className, handler, Parse.applicationId);
|
||||
if (validationHandler && validationHandler.rateLimit) {
|
||||
addRateLimit(
|
||||
{ requestPath: `/login`, requestMethods: 'POST', ...validationHandler.rateLimit },
|
||||
Parse.applicationId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -402,6 +449,16 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) {
|
||||
Parse.applicationId,
|
||||
validationHandler
|
||||
);
|
||||
if (validationHandler && validationHandler.rateLimit) {
|
||||
addRateLimit(
|
||||
{
|
||||
requestPath: getRoute(className),
|
||||
requestMethods: 'GET',
|
||||
...validationHandler.rateLimit,
|
||||
},
|
||||
Parse.applicationId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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