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