feat: Add request rate limiter based on IP address (#8174)
This commit is contained in:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.18.2",
|
||||
"express-rate-limit": "6.6.0",
|
||||
"follow-redirects": "1.15.2",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
@@ -39,6 +40,7 @@
|
||||
"mongodb": "4.10.0",
|
||||
"mustache": "4.2.0",
|
||||
"parse": "3.4.2",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"pg-monitor": "1.5.0",
|
||||
"pg-promise": "10.12.1",
|
||||
"pluralize": "8.0.0",
|
||||
@@ -7275,6 +7277,17 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz",
|
||||
"integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==",
|
||||
"engines": {
|
||||
"node": ">= 12.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4 || ^5"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -26197,6 +26210,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"express-rate-limit": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz",
|
||||
"integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==",
|
||||
"requires": {}
|
||||
},
|
||||
"ext": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.18.2",
|
||||
"express-rate-limit": "6.6.0",
|
||||
"follow-redirects": "1.15.2",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
@@ -48,6 +49,7 @@
|
||||
"mongodb": "4.10.0",
|
||||
"mustache": "4.2.0",
|
||||
"parse": "3.4.2",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"pg-monitor": "1.5.0",
|
||||
"pg-promise": "10.12.1",
|
||||
"pluralize": "8.0.0",
|
||||
@@ -97,8 +99,8 @@
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-fetch": "3.2.10",
|
||||
"node-abort-controller": "3.0.1",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "17.4.6",
|
||||
|
||||
@@ -44,6 +44,7 @@ const nestedOptionEnvPrefix = {
|
||||
SecurityOptions: 'PARSE_SERVER_SECURITY_',
|
||||
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
|
||||
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
|
||||
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
|
||||
};
|
||||
|
||||
function last(array) {
|
||||
@@ -111,7 +112,9 @@ function processProperty(property, iface) {
|
||||
}
|
||||
let defaultValue;
|
||||
if (defaultLine) {
|
||||
defaultValue = defaultLine.split(' ')[1];
|
||||
const defaultArray = defaultLine.split(' ');
|
||||
defaultArray.shift();
|
||||
defaultValue = defaultArray.join(' ');
|
||||
}
|
||||
let type = property.value.type;
|
||||
let isRequired = true;
|
||||
|
||||
370
spec/RateLimit.spec.js
Normal file
370
spec/RateLimit.spec.js
Normal file
@@ -0,0 +1,370 @@
|
||||
describe('rate limit', () => {
|
||||
it('can limit cloud functions', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can add global limit', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await expectAsync(new Parse.Object('Test').save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit cloud with validator', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc', {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can skip with masterKey', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response1).toBe('Abc');
|
||||
const response2 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response2).toBe('Abc');
|
||||
});
|
||||
|
||||
it('should run with masterKey', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
includeMasterKey: true,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit saving objects', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to post', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'POST',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
const obj2 = new Parse.Object('Test');
|
||||
await expectAsync(obj2.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator for post', async () => {
|
||||
Parse.Cloud.beforeSave('Test', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator for file', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save();
|
||||
const file2 = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
|
||||
await expectAsync(file2.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to get', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/Test',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'GET',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
await new Parse.Query('Test').first();
|
||||
await expectAsync(new Parse.Query('Test').first()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator', async () => {
|
||||
await reconfigureServer({ silent: false });
|
||||
Parse.Cloud.beforeFind('TestObject', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
await new Parse.Query('TestObject').first();
|
||||
await expectAsync(new Parse.Query('TestObject').first()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await expectAsync(new Parse.Query('TestObject').get('abc')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to delete', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/Test/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'DELETE',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.destroy();
|
||||
await expectAsync(obj.destroy()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set beforeDelete', async () => {
|
||||
const obj = new Parse.Object('TestDelete');
|
||||
await obj.save();
|
||||
Parse.Cloud.beforeDelete('TestDelete', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
await obj.destroy();
|
||||
await expectAsync(obj.destroy()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set beforeLogin', async () => {
|
||||
Parse.Cloud.beforeLogin(() => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
await Parse.User.signUp('myUser', 'password');
|
||||
await Parse.User.logIn('myUser', 'password');
|
||||
await expectAsync(Parse.User.logIn('myUser', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can define limits via rateLimit and define', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 100,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
Parse.Cloud.define('test', () => 'Abc', {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests.')
|
||||
);
|
||||
});
|
||||
|
||||
it('does not limit internal calls', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
},
|
||||
],
|
||||
});
|
||||
Parse.Cloud.define('test1', () => 'Abc');
|
||||
Parse.Cloud.define('test2', async () => {
|
||||
await Parse.Cloud.run('test1');
|
||||
await Parse.Cloud.run('test1');
|
||||
});
|
||||
await Parse.Cloud.run('test2');
|
||||
});
|
||||
|
||||
it('can validate rateLimit', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const validateRateLimit = ({ rateLimit }) => Config.validateRateLimit(rateLimit);
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: 'a', requestTimeWindow: 1000, requestCount: 3 })
|
||||
).toThrow('rateLimit must be an array or object');
|
||||
expect(() => validateRateLimit({ rateLimit: ['a'] })).toThrow(
|
||||
'rateLimit must be an array of objects'
|
||||
);
|
||||
expect(() => validateRateLimit({ rateLimit: [{ requestPath: [] }] })).toThrow(
|
||||
'rateLimit.requestPath must be a string'
|
||||
);
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: [], requestPath: 'a' }] })
|
||||
).toThrow('rateLimit.requestTimeWindow must be a number');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [
|
||||
{
|
||||
includeInternalRequests: [],
|
||||
requestTimeWindow: 1000,
|
||||
requestCount: 3,
|
||||
requestPath: 'a',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toThrow('rateLimit.includeInternalRequests must be a boolean');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [{ requestCount: [], requestTimeWindow: 1000, requestPath: 'a' }],
|
||||
})
|
||||
).toThrow('rateLimit.requestCount must be a number');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [
|
||||
{ errorResponseMessage: [], requestTimeWindow: 1000, requestCount: 3, requestPath: 'a' },
|
||||
],
|
||||
})
|
||||
).toThrow('rateLimit.errorResponseMessage must be a string');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestCount: 3, requestPath: 'abc' }] })
|
||||
).toThrow('rateLimit.requestTimeWindow must be defined');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: 3, requestPath: 'abc' }] })
|
||||
).toThrow('rateLimit.requestCount must be defined');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: 3, requestCount: 'abc' }] })
|
||||
).toThrow('rateLimit.requestPath must be defined');
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
rateLimit: [{ requestTimeWindow: 3, requestCount: 1, path: 'abc', requestPath: 'a' }],
|
||||
})
|
||||
).toBeRejectedWith(`Invalid rate limit option "path"`);
|
||||
});
|
||||
});
|
||||
@@ -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