520 lines
17 KiB
JavaScript
520 lines
17 KiB
JavaScript
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
|
|
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 limit cloud functions with user session token', async () => {
|
|
await Parse.User.signUp('myUser', 'password');
|
|
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');
|
|
});
|
|
|
|
describe('zone', () => {
|
|
const middlewares = require('../lib/middlewares');
|
|
it('can use global zone', async () => {
|
|
await reconfigureServer({
|
|
rateLimit: {
|
|
requestPath: '*',
|
|
requestTimeWindow: 10000,
|
|
requestCount: 1,
|
|
errorResponseMessage: 'Too many requests',
|
|
includeInternalRequests: true,
|
|
zone: Parse.Server.RateLimitZone.global,
|
|
},
|
|
});
|
|
const fakeReq = {
|
|
originalUrl: 'http://example.com/parse/',
|
|
url: 'http://example.com/',
|
|
body: {
|
|
_ApplicationId: 'test',
|
|
},
|
|
headers: {
|
|
'X-Parse-Application-Id': 'test',
|
|
'X-Parse-REST-API-Key': 'rest',
|
|
},
|
|
get: key => {
|
|
return fakeReq.headers[key];
|
|
},
|
|
};
|
|
fakeReq.ip = '127.0.0.1';
|
|
let fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status', 'setHeader', 'json']);
|
|
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
|
fakeReq.ip = '127.0.0.2';
|
|
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status', 'setHeader']);
|
|
let resolvingPromise;
|
|
const promise = new Promise(resolve => {
|
|
resolvingPromise = resolve;
|
|
});
|
|
fakeRes.json = jasmine.createSpy('json').and.callFake(resolvingPromise);
|
|
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
|
throw 'Should not call next';
|
|
});
|
|
await promise;
|
|
expect(fakeRes.status).toHaveBeenCalledWith(429);
|
|
expect(fakeRes.json).toHaveBeenCalledWith({
|
|
code: Parse.Error.CONNECTION_FAILED,
|
|
error: 'Too many requests',
|
|
});
|
|
});
|
|
|
|
it('can use session zone', async () => {
|
|
await reconfigureServer({
|
|
rateLimit: {
|
|
requestPath: '/functions/*',
|
|
requestTimeWindow: 10000,
|
|
requestCount: 1,
|
|
errorResponseMessage: 'Too many requests',
|
|
includeInternalRequests: true,
|
|
zone: Parse.Server.RateLimitZone.session,
|
|
},
|
|
});
|
|
Parse.Cloud.define('test', () => 'Abc');
|
|
await Parse.User.signUp('username', 'password');
|
|
await Parse.Cloud.run('test');
|
|
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
|
);
|
|
await Parse.User.logIn('username', 'password');
|
|
await Parse.Cloud.run('test');
|
|
});
|
|
|
|
it('can use user zone', async () => {
|
|
await reconfigureServer({
|
|
rateLimit: {
|
|
requestPath: '/functions/*',
|
|
requestTimeWindow: 10000,
|
|
requestCount: 1,
|
|
errorResponseMessage: 'Too many requests',
|
|
includeInternalRequests: true,
|
|
zone: Parse.Server.RateLimitZone.user,
|
|
},
|
|
});
|
|
Parse.Cloud.define('test', () => 'Abc');
|
|
await Parse.User.signUp('username', 'password');
|
|
await Parse.Cloud.run('test');
|
|
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
|
);
|
|
await Parse.User.logIn('username', 'password');
|
|
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
|
);
|
|
});
|
|
});
|
|
|
|
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: [{ requestPath: 'a', requestTimeWindow: 1000, requestCount: 3, zone: 'abc' }],
|
|
})
|
|
).toThrow('rateLimit.zone must be one of global, session, user, or ip');
|
|
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"`);
|
|
});
|
|
describe_only(() => {
|
|
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
|
|
})('with RedisCache', function () {
|
|
it('does work with cache', async () => {
|
|
await reconfigureServer({
|
|
rateLimit: [
|
|
{
|
|
requestPath: '/classes/*',
|
|
requestTimeWindow: 10000,
|
|
requestCount: 1,
|
|
errorResponseMessage: 'Too many requests',
|
|
includeInternalRequests: true,
|
|
redisUrl: 'redis://localhost:6379',
|
|
},
|
|
],
|
|
});
|
|
const obj = new Parse.Object('Test');
|
|
await obj.save();
|
|
await expectAsync(obj.save()).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
|
);
|
|
const cache = new RedisCacheAdapter();
|
|
await cache.connect();
|
|
const value = await cache.get('rl:127.0.0.1');
|
|
expect(value).toEqual(2);
|
|
const ttl = await cache.client.ttl('rl:127.0.0.1');
|
|
expect(ttl).toEqual(10);
|
|
});
|
|
});
|
|
});
|