Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add request rate limiter based on IP address #8174

Merged
merged 50 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7cddeba
feat: add rate limiter
dblythy Sep 18, 2022
5b3fd30
Update RateLimit.spec.js
dblythy Sep 18, 2022
eac6544
Update package.json
dblythy Sep 18, 2022
67f4b32
Merge branch 'alpha' into express-rate-limit
mtrezza Sep 18, 2022
25e73b4
reorder
dblythy Sep 19, 2022
f53ac82
Update middlewares.js
dblythy Sep 19, 2022
95f0fbd
fix tests
dblythy Sep 19, 2022
79cbc0b
Update RateLimit.spec.js
dblythy Sep 19, 2022
e539b43
Update middlewares.js
dblythy Sep 19, 2022
bef901f
enforce fields
dblythy Sep 19, 2022
fb52fec
Merge branch 'alpha' into express-rate-limit
dblythy Sep 20, 2022
2b7ee4a
Update src/Options/index.js
dblythy Sep 20, 2022
0d155fd
Update src/Config.js
dblythy Sep 20, 2022
7b9df99
Update src/Config.js
dblythy Sep 20, 2022
2df37e2
refactor
dblythy Sep 20, 2022
f03e1cd
remove default path
dblythy Sep 20, 2022
164329d
run prettier
dblythy Sep 20, 2022
77b57a1
add cluster
dblythy Sep 20, 2022
35e8092
tests
dblythy Sep 20, 2022
22f9c7a
Merge branch 'alpha' into express-rate-limit
dblythy Sep 21, 2022
6205337
Merge branch 'alpha' into express-rate-limit
dblythy Sep 27, 2022
60439bc
Merge branch 'alpha' into express-rate-limit
dblythy Sep 29, 2022
d5eed75
Update RateLimit.spec.js
dblythy Sep 29, 2022
267e528
Merge branch 'express-rate-limit' of https://github.com/dblythy/parse…
dblythy Sep 29, 2022
c3d5258
default rateLimit
dblythy Sep 29, 2022
0036437
Update AuthenticationAdapters.spec.js
dblythy Sep 29, 2022
46d1042
Update AuthenticationAdapters.spec.js
dblythy Sep 29, 2022
d142a33
Update RateLimit.spec.js
dblythy Sep 29, 2022
8cfce2f
Merge branch 'alpha' into express-rate-limit
dblythy Oct 12, 2022
18e9e9d
allow for files
dblythy Oct 12, 2022
560c7ee
Update ParseServer.js
dblythy Oct 12, 2022
79be980
Update RateLimit.spec.js
dblythy Oct 12, 2022
b93c37b
Update src/Options/docs.js
mtrezza Oct 12, 2022
14ad8d2
Update src/Options/index.js
mtrezza Oct 12, 2022
444a59d
Merge branch 'alpha' into express-rate-limit
dblythy Oct 27, 2022
2b551fe
run defintions
dblythy Oct 27, 2022
7503b96
Update RateLimit.spec.js
dblythy Oct 27, 2022
8e9a999
Update RateLimit.spec.js
dblythy Oct 27, 2022
76a744a
Merge branch 'alpha' into express-rate-limit
mtrezza Oct 27, 2022
c499299
Merge branch 'alpha' into express-rate-limit
mtrezza Oct 31, 2022
850bb13
Update src/middlewares.js
dblythy Nov 28, 2022
9728a5a
Merge branch 'alpha' into express-rate-limit
dblythy Dec 30, 2022
3e2b227
Update Config.js
dblythy Dec 30, 2022
496e305
sip
dblythy Dec 30, 2022
d012fa5
Update package-lock.json
dblythy Dec 30, 2022
835e6f3
Update middlewares.js
dblythy Dec 30, 2022
aba538b
wip
dblythy Dec 30, 2022
6d930d3
Apply suggestions from code review
dblythy Jan 6, 2023
3b7e520
Merge branch 'alpha' into express-rate-limit
dblythy Jan 6, 2023
160706c
Merge branch 'alpha' into express-rate-limit
mtrezza Jan 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@
"cors": "2.8.5",
"deepcopy": "2.1.0",
"express": "4.18.1",
"express-rate-limit": "6.6.0",
"follow-redirects": "1.15.1",
"graphql": "16.6.0",
"graphql-list-fields": "2.0.2",
"graphql-tag": "2.12.6",
"graphql-relay": "0.10.0",
"graphql-tag": "2.12.6",
"intersect": "1.0.1",
"jsonwebtoken": "8.5.1",
"jwks-rsa": "2.1.4",
Expand Down
1 change: 1 addition & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const nestedOptionEnvPrefix = {
'PasswordPolicyOptions': 'PARSE_SERVER_PASSWORD_POLICY_',
'SecurityOptions': 'PARSE_SERVER_SECURITY_',
'SchemaOptions': 'PARSE_SERVER_SCHEMA_',
'RateLimitOptions': 'PARSE_SERVER_RATE_LIMIT_',
};

function last(array) {
Expand Down
8 changes: 4 additions & 4 deletions spec/ParseInstallation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1244,11 +1244,11 @@ describe('Installations', () => {
deviceToken: '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306',
deviceType: 'ios',
};
await rest.create(config, auth.nobody(config), '_Installation', input)
await rest.create(config, auth.nobody(config), '_Installation', input);
const functions = {
beforeSave() {},
afterSave() {}
}
afterSave() {},
};
spyOn(functions, 'beforeSave').and.callThrough();
spyOn(functions, 'afterSave').and.callThrough();
Parse.Cloud.beforeSave(Parse.Installation, functions.beforeSave);
Expand Down Expand Up @@ -1283,7 +1283,7 @@ describe('Installations', () => {
},
});
await new Promise(resolve => setTimeout(resolve, 1000));
const installation = await new Parse.Query(Parse.Installation).first({useMasterKey: true});
const installation = await new Parse.Query(Parse.Installation).first({ useMasterKey: true });
expect(installation.get('badge')).toEqual(3);
expect(functions.beforeSave).not.toHaveBeenCalled();
expect(functions.afterSave).not.toHaveBeenCalled();
Expand Down
326 changes: 326 additions & 0 deletions spec/RateLimit.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
describe('rate limit', () => {
it('can limit cloud functions', async () => {
Parse.Cloud.define('test', () => 'Abc');
await reconfigureServer({
rateLimit: [
{
path: '/functions/*',
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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: {
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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: {
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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: [
{
path: '/functions/*',
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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: [
{
path: '/functions/*',
windowMs: 10000,
max: 1,
master: true,
message: 'Too many requests',
restrictInternal: 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: [
{
path: '/classes/*',
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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: [
{
path: '/classes/*',
windowMs: 10000,
max: 1,
method: 'POST',
message: 'Too many requests',
restrictInternal: 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: {
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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 get', async () => {
await reconfigureServer({
rateLimit: [
{
path: '/classes/Test',
windowMs: 10000,
max: 1,
method: 'GET',
message: 'Too many requests',
restrictInternal: 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 () => {
Parse.Cloud.beforeFind('TestObject', () => {}, {
rateLimit: {
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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: [
{
path: '/classes/Test',
windowMs: 10000,
max: 1,
method: 'DELETE',
message: 'Too many requests',
restrictInternal: 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 () => {
Parse.Cloud.beforeDelete('TestDelete', () => {}, {
rateLimit: {
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: true,
},
});
const obj = new Parse.Object('TestDelete');
await obj.save();
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: {
windowMs: 10000,
max: 1,
message: 'Too many requests',
restrictInternal: 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: [
{
path: '/functions/*',
windowMs: 10000,
max: 100,
message: 'Too many requests',
restrictInternal: true,
},
],
});
Parse.Cloud.define('test', () => 'Abc', {
rateLimit: {
windowMs: 10000,
max: 1,
restrictInternal: 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: [
{
path: '/functions/*',
windowMs: 10000,
max: 1,
message: '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 () => {
await expectAsync(reconfigureServer({ rateLimit: 'a', windowMs: 1000, max: 3 })).toBeRejectedWith(
'rateLimit must be an array or object'
);
await expectAsync(reconfigureServer({ rateLimit: ['a'] })).toBeRejectedWith(
'rateLimit must be an array of objects'
);
await expectAsync(reconfigureServer({ rateLimit: [{ path: [] }] })).toBeRejectedWith(
'rateLimit.path must be a string'
);
await expectAsync(reconfigureServer({ rateLimit: [{ windowMs: [] }] })).toBeRejectedWith(
'rateLimit.windowMs must be a number'
);
await expectAsync(
reconfigureServer({ rateLimit: [{ restrictInternal: [], windowMs: 1000, max: 3 }] })
).toBeRejectedWith('rateLimit.restrictInternal must be a boolean');
await expectAsync(reconfigureServer({ rateLimit: [{ max: [], windowMs: 1000 }] })).toBeRejectedWith(
'rateLimit.max must be a number'
);
await expectAsync(reconfigureServer({ rateLimit: [{ message: [], windowMs: 1000, max: 3 }] })).toBeRejectedWith(
'rateLimit.message must be a string'
);
await expectAsync(reconfigureServer({ rateLimit: [{ max: 3 }] })).toBeRejectedWith(
'rateLimit.windowMs must be defined'
);
await expectAsync(reconfigureServer({ rateLimit: [{ windowMs: 3 }] })).toBeRejectedWith(
'rateLimit.max must be defined'
);
});
});
Loading