diff --git a/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js b/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js index 1c121bc25ae..69709aa30ae 100644 --- a/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js +++ b/packages/ddp-rate-limiter/ddp-rate-limiter-test-service.js @@ -1,9 +1,14 @@ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; +import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; +import { RATE_LIMIT_NUM_CALLS, RATE_LIMIT_INTERVAL_TIME_MS } from './ddp-rate-limiter-tests-common'; + Meteor.methods({ // Adds in a new rule with the specific intervalTime and connectionId as // passed in to speed up testing & allow the rule to apply to the connection // testing the rate limit. - addRuleToDDPRateLimiter: function () { - var connection = this.connection; + addRuleToDDPRateLimiter() { + const connection = this.connection; connection.lastRateLimitEvent = connection.lastRateLimitEvent || {}; connection.lastMethodName = connection.lastMethodName || ''; // XXX In Javascript v8 engine, we are currently guaranteed the ordering of @@ -12,36 +17,36 @@ Meteor.methods({ // test. // // This is important because we use `connection.lastMethodName` to - // ignore the "getLastRateLimitEvent" method so that it can return + // ignore the 'getLastRateLimitEvent' method so that it can return // the actual last rate limit event rather than the one - // corresponding to the method call to "getLastRateLimitEvent". + // corresponding to the method call to 'getLastRateLimitEvent'. this.ruleId = DDPRateLimiter.addRule({ - name: function (name) { + name(name) { connection.lastMethodName = name; if (name !== 'getLastRateLimitEvent') { connection.lastRateLimitEvent.name = name; } - return name !== "a-method-that-is-not-rate-limited"; + return name !== 'a-method-that-is-not-rate-limited'; }, - userId: function (userId) { + userId(userId) { connection.lastRateLimitEvent.userId = userId; return true; }, - type: function (type) { + type(type) { // Special check to return proper name since 'getLastRateLimitEvent' // is another method call - if (connection.lastMethodName !== 'getLastRateLimitEvent'){ + if (connection.lastMethodName !== 'getLastRateLimitEvent') { connection.lastRateLimitEvent.type = type; } return true; }, - clientAddress: function (clientAddress) { - connection.lastRateLimitEvent.clientAddress = clientAddress + clientAddress(clientAddress) { + connection.lastRateLimitEvent.clientAddress = clientAddress; return true; }, - connectionId: this.connection.id - }, RATE_LIMIT_NUM_CALLS, RATE_LIMIT_INTERVAL_TIME_MS, function(reply, ruleInput) { - if (connection.lastMethodName !== 'getLastRateLimitEvent'){ + connectionId: this.connection.id, + }, RATE_LIMIT_NUM_CALLS, RATE_LIMIT_INTERVAL_TIME_MS, (reply, ruleInput) => { + if (connection.lastMethodName !== 'getLastRateLimitEvent') { connection.lastRateLimitEvent.reply = reply; connection.lastRateLimitEvent.ruleInput = ruleInput; } @@ -49,34 +54,32 @@ Meteor.methods({ return this.ruleId; }, - getLastRateLimitEvent: function () { + getLastRateLimitEvent() { return this.connection.lastRateLimitEvent; }, // Server side method to remove rule from DDP Rate Limiter - removeRuleFromDDPRateLimiter: function (id) { + removeRuleFromDDPRateLimiter(id) { return DDPRateLimiter.removeRule(id); }, // Print all the server rules for debugging purposes. - printCurrentListOfRules: function () { + printCurrentListOfRules() { console.log('Current list of rules :', DDPRateLimiter.printRules()); }, - removeUserByUsername: function (username) { - Meteor.users.remove({username: username}); + removeUserByUsername(username) { + Meteor.users.remove({ username }); }, - dummyMethod: function () { - return "yup"; + dummyMethod() { + return 'yup'; }, - 'a-method-that-is-not-rate-limited': function () { - return "not-rate-limited"; + 'a-method-that-is-not-rate-limited'() { + return 'not-rate-limited'; }, - addDefaultAccountsRateLimitRule: function () { + addDefaultAccountsRateLimitRule() { Accounts.addDefaultRateLimit(); }, - removeDefaultAccountsRateLimitRule: function () { + removeDefaultAccountsRateLimitRule() { return Accounts.removeDefaultRateLimit(); - } + }, }); -Meteor.publish("testSubscription", function () { - return []; -}); +Meteor.publish('testSubscription', () => []); diff --git a/packages/ddp-rate-limiter/ddp-rate-limiter-tests-common.js b/packages/ddp-rate-limiter/ddp-rate-limiter-tests-common.js index 72ca8d2bfcc..d3ff4d38ca5 100644 --- a/packages/ddp-rate-limiter/ddp-rate-limiter-tests-common.js +++ b/packages/ddp-rate-limiter/ddp-rate-limiter-tests-common.js @@ -1,3 +1,3 @@ // Common settings for DDPRateLimiter tests. -RATE_LIMIT_NUM_CALLS = 5; -RATE_LIMIT_INTERVAL_TIME_MS = 5000; \ No newline at end of file +export const RATE_LIMIT_NUM_CALLS = 5; +export const RATE_LIMIT_INTERVAL_TIME_MS = 5000; diff --git a/packages/ddp-rate-limiter/ddp-rate-limiter-tests.js b/packages/ddp-rate-limiter/ddp-rate-limiter-tests.js index 3c300181bf3..c99f27366c3 100644 --- a/packages/ddp-rate-limiter/ddp-rate-limiter-tests.js +++ b/packages/ddp-rate-limiter/ddp-rate-limiter-tests.js @@ -1,46 +1,49 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; +import { Accounts } from 'meteor/accounts-base'; +import { RATE_LIMIT_NUM_CALLS, RATE_LIMIT_INTERVAL_TIME_MS } from './ddp-rate-limiter-tests-common'; + // Test that we do hit the default login rate limit. // XXX Removed to fix testing as other packages currently hit the default rate // limit. -testAsyncMulti("ddp rate limiter - default rate limit", [ + +testAsyncMulti('ddp rate limiter - default rate limit', [ function (test, expect) { // Add in the default rate limiter rule Meteor.call('addDefaultAccountsRateLimitRule'); - _.extend(this, createTestUser(test, expect)); + Object.assign(this, createTestUser(test, expect)); }, function (test, expect) { - Meteor.logout(expect(function (error) { + Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); }, function (test, expect) { - var self = this; - callFnMultipleTimesThenExpectResult(test, expect, - Meteor.loginWithPassword.bind(Meteor, self.username, 'fakePassword'), + Meteor.loginWithPassword.bind(Meteor, this.username, 'fakePassword'), { expectedError: 403, expectedResult: undefined, expectedRateLimitWillBeHit: true, - expectedIntervalTimeInMs: 10000 - } + expectedIntervalTimeInMs: 10000, + }, ); }, function (test, expect) { - Meteor.call("removeUserByUsername", this.username, expect(function () {})); + Meteor.call('removeUserByUsername', this.username, expect(() => {})); // Remove the default rate limiter rule Meteor.call('removeDefaultAccountsRateLimitRule'); - } + }, ]); -testAsyncMulti("ddp rate limiter - matchers get passed correct arguments", [ +testAsyncMulti('ddp rate limiter - matchers get passed correct arguments', [ function (test, expect) { - _.extend(this, createTestUser(test, expect)); + Object.assign(this, createTestUser(test, expect)); }, function (test, expect) { - var self = this; - Meteor.call("addRuleToDDPRateLimiter", expect(function(error, result) { - self.ruleId = result; + Meteor.call('addRuleToDDPRateLimiter', expect((error, result) => { + this.ruleId = result; })); }, function (test, expect) { @@ -48,52 +51,50 @@ testAsyncMulti("ddp rate limiter - matchers get passed correct arguments", [ Meteor.call.bind(Meteor, 'dummyMethod'), { expectedError: undefined, - expectedResult: "yup", - expectedRateLimitWillBeHit: true - } + expectedResult: 'yup', + expectedRateLimitWillBeHit: true, + }, ); }, function (test, expect) { - var self = this; Meteor.call( - "getLastRateLimitEvent", expect(function (error, result) { + 'getLastRateLimitEvent', expect((error, result) => { test.equal(error, undefined); test.equal(result.userId, Meteor.userId()); - test.equal(result.type, "method"); - test.equal(result.name, "dummyMethod"); - test.isNotUndefined(result.clientAddress, "clientAddress is not defined"); + test.equal(result.type, 'method'); + test.equal(result.name, 'dummyMethod'); + test.isNotUndefined(result.clientAddress, 'clientAddress is not defined'); })); }, function (test, expect) { - Meteor.call("removeUserByUsername", this.username, expect(function () {})); + Meteor.call('removeUserByUsername', this.username, expect(() => {})); }, function (test, expect) { - var self = this; // Cleanup - Meteor.call('removeRuleFromDDPRateLimiter', self.ruleId, - expect(function(error, result) { - test.equal(result,true); - })); - } + Meteor.call('removeRuleFromDDPRateLimiter', this.ruleId, + expect((error, result) => { + test.equal(result, true); + }), + ); + }, ]); -testAsyncMulti("ddp rate limiter - callbacks get passed correct arguments", [ +testAsyncMulti('ddp rate limiter - callbacks get passed correct arguments', [ function (test, expect) { - _.extend(this, createTestUser(test, expect)); + Object.assign(this, createTestUser(test, expect)); }, function (test, expect) { - var self = this; - Meteor.call("addRuleToDDPRateLimiter", expect(function(error, result) { - self.ruleId = result; + Meteor.call('addRuleToDDPRateLimiter', expect((error, result) => { + this.ruleId = result; })); }, function (test, expect) { - Meteor.call('dummyMethod', expect(function() {})); + Meteor.call('dummyMethod', expect(() => {})); }, function (test, expect) { - var self = this; Meteor.call( - "getLastRateLimitEvent", expect(function (error, result) { + 'getLastRateLimitEvent', + expect((error, result) => { test.isTrue(result.reply.allowed); test.isTrue(result.reply.timeToReset < RATE_LIMIT_INTERVAL_TIME_MS + 100); test.equal(result.reply.numInvocationsLeft, 4); @@ -101,7 +102,8 @@ testAsyncMulti("ddp rate limiter - callbacks get passed correct arguments", [ test.equal(result.ruleInput.userId, Meteor.userId()); test.equal(result.ruleInput.type, 'method'); test.equal(result.ruleInput.name, 'dummyMethod'); - })); + }), + ); }, function (test, expect) { // Wait for the rule to reset @@ -109,81 +111,75 @@ testAsyncMulti("ddp rate limiter - callbacks get passed correct arguments", [ }, function (test, expect) { // Call RATE_LIMIT_NUM_CALLS + 1 times to make the rule exceed limit and reject the execution - for (var i = 0; i < RATE_LIMIT_NUM_CALLS + 1; i++) { - Meteor.call('dummyMethod', expect(function() {})); + for (let i = 0; i < RATE_LIMIT_NUM_CALLS + 1; i++) { + Meteor.call('dummyMethod', expect(() => {})); } }, function (test, expect) { - var self = this; - Meteor.call( - "getLastRateLimitEvent", expect(function (error, result) { - test.isFalse(result.reply.allowed); - test.isTrue(result.reply.timeToReset < RATE_LIMIT_INTERVAL_TIME_MS + 100); - test.equal(result.reply.numInvocationsLeft, 0); + Meteor.call('getLastRateLimitEvent', expect((error, result) => { + test.isFalse(result.reply.allowed); + test.isTrue(result.reply.timeToReset < RATE_LIMIT_INTERVAL_TIME_MS + 100); + test.equal(result.reply.numInvocationsLeft, 0); - test.equal(result.ruleInput.userId, Meteor.userId()); - test.equal(result.ruleInput.type, 'method'); - test.equal(result.ruleInput.name, 'dummyMethod'); - })); + test.equal(result.ruleInput.userId, Meteor.userId()); + test.equal(result.ruleInput.type, 'method'); + test.equal(result.ruleInput.name, 'dummyMethod'); + })); }, function (test, expect) { - Meteor.call("removeUserByUsername", this.username, expect(function () {})); + Meteor.call('removeUserByUsername', this.username, expect(() => {})); }, function (test, expect) { - var self = this; // Cleanup - Meteor.call('removeRuleFromDDPRateLimiter', self.ruleId, - expect(function(error, result) { - test.equal(result,true); - })); - } + Meteor.call('removeRuleFromDDPRateLimiter', this.ruleId, + expect((error, result) => { + test.equal(result, true); + }), + ); + }, ]); -testAsyncMulti("ddp rate limiter - we can return with type 'subscription'", [ +testAsyncMulti('ddp rate limiter - we can return with type \'subscription\'', [ function (test, expect) { - var self = this; - Meteor.call("addRuleToDDPRateLimiter", expect( - function(error, result) { - self.ruleId = result; + Meteor.call('addRuleToDDPRateLimiter', expect( + (error, result) => { + this.ruleId = result; })); }, function (test, expect) { Meteor.subscribe('testSubscription'); - Meteor.call('getLastRateLimitEvent', expect(function(error, result){ + Meteor.call('getLastRateLimitEvent', expect((error, result) =>{ test.equal(error, undefined); - test.equal(result.type, "subscription"); - test.equal(result.name, "testSubscription"); - test.isNotUndefined(result.clientAddress, "clientAddress is not defined"); + test.equal(result.type, 'subscription'); + test.equal(result.name, 'testSubscription'); + test.isNotUndefined(result.clientAddress, 'clientAddress is not defined'); })); }, function (test, expect) { - var self = this; // Cleanup - Meteor.call('removeRuleFromDDPRateLimiter', self.ruleId, - expect(function(error, result) { + Meteor.call('removeRuleFromDDPRateLimiter', this.ruleId, + expect((error, result) => { test.equal(result, true); - })); - } + }), + ); + }, ]); -testAsyncMulti("ddp rate limiter - rate limits to subscriptions", [ +testAsyncMulti('ddp rate limiter - rate limits to subscriptions', [ function (test, expect) { - var self = this; - Meteor.call("addRuleToDDPRateLimiter", expect( - function(error, result) { - self.ruleId = result; - }) - ); + Meteor.call('addRuleToDDPRateLimiter', expect((error, result) => { + this.ruleId = result; + })); }, function (test, expect) { - this.doSub = function (cb) { + this.doSub = (cb) => { Meteor.subscribe('testSubscription', { - onReady: function () { + onReady() { cb(null, true); }, - onStop: function (error) { + onStop(error) { cb(error, undefined); - } + }, }); }; @@ -191,46 +187,47 @@ testAsyncMulti("ddp rate limiter - rate limits to subscriptions", [ { expectedError: null, expectedResult: true, - expectedRateLimitWillBeHit: true - } + expectedRateLimitWillBeHit: true, + }, ); }, function (test, expect) { // After removing rule, subscriptions are no longer rate limited. - var self = this; - Meteor.call('removeRuleFromDDPRateLimiter', self.ruleId, - expect(function(error, result) { - test.equal(result,true); - })); + Meteor.call('removeRuleFromDDPRateLimiter', this.ruleId, + expect((error, result) => { + test.equal(result, true); + }), + ); }, function (test, expect) { callFnMultipleTimesThenExpectResult(test, expect, this.doSub, - { - expectedError: null, - expectedResult: true, - expectedIntervalTimeInMs: false - }); + { + expectedError: null, + expectedResult: true, + expectedIntervalTimeInMs: false, + }, + ); callFnMultipleTimesThenExpectResult(test, expect, this.doSub, - { - expectedError: null, - expectedResult: true, - expectedIntervalTimeInMs: false - }); - } + { + expectedError: null, + expectedResult: true, + expectedIntervalTimeInMs: false, + }, + ); + }, ]); // - If you wait 5 seconds you are no longer rate limited -testAsyncMulti("ddp rate limiter - rate limit resets after " + - "RATE_LIMIT_INTERVAL_TIME_MS", [ +testAsyncMulti('ddp rate limiter - rate limit resets after ' + + 'RATE_LIMIT_INTERVAL_TIME_MS', [ function (test, expect) { - _.extend(this, createTestUser(test, expect)); + Object.assign(this, createTestUser(test, expect)); }, function (test, expect) { - var self = this; - Meteor.call("addRuleToDDPRateLimiter", expect(function(error, result) { - self.ruleId = result; + Meteor.call('addRuleToDDPRateLimiter', expect((error, result) => { + this.ruleId = result; })); }, @@ -239,9 +236,9 @@ testAsyncMulti("ddp rate limiter - rate limit resets after " + Meteor.call.bind(Meteor, 'dummyMethod'), { expectedError: undefined, - expectedResult: "yup", - expectedRateLimitWillBeHit: true - } + expectedResult: 'yup', + expectedRateLimitWillBeHit: true, + }, ); }, function (test, expect) { @@ -252,26 +249,25 @@ testAsyncMulti("ddp rate limiter - rate limit resets after " + Meteor.call.bind(Meteor, 'dummyMethod'), { expectedError: undefined, - expectedResult: "yup", - expectedRateLimitWillBeHit: true - } + expectedResult: 'yup', + expectedRateLimitWillBeHit: true, + }, ); }, function (test, expect) { - var self = this; - Meteor.call('removeRuleFromDDPRateLimiter', self.ruleId, - expect(function(error, result) { + Meteor.call('removeRuleFromDDPRateLimiter', this.ruleId, + expect((error, result) => { test.equal(result, true); - })); - } + }), + ); + }, ]); -testAsyncMulti("ddp rate limiter - 'a-method-that-is-not-rate-limited' is not" + - " rate limited", [ +testAsyncMulti('ddp rate limiter - \'a-method-that-is-not-rate-limited\' is not' + + ' rate limited', [ function (test, expect) { - var self = this; - Meteor.call('addRuleToDDPRateLimiter', expect(function(error, result){ - self.ruleId = result; + Meteor.call('addRuleToDDPRateLimiter', expect((error, result) =>{ + this.ruleId = result; })); }, function (test, expect) { @@ -279,95 +275,97 @@ testAsyncMulti("ddp rate limiter - 'a-method-that-is-not-rate-limited' is not" + Meteor.call.bind(Meteor, 'a-method-that-is-not-rate-limited'), { expectedError: undefined, - expectedResult: "not-rate-limited", - expectedRateLimitWillBeHit: false - }); + expectedResult: 'not-rate-limited', + expectedRateLimitWillBeHit: false, + }, + ); }, function (test, expect) { - var self = this; - Meteor.call('removeRuleFromDDPRateLimiter', self.ruleId, - expect(function(error, result) { + Meteor.call('removeRuleFromDDPRateLimiter', this.ruleId, + expect((error, result) => { test.equal(result, true); - })); - } + }), + ); + }, ]); // When we have a rate limited client and we remove the rate limit rule, // all requests should be allowed immediately afterwards. -testAsyncMulti("ddp rate limiter - test removing rule with rateLimited " + - "client lets them send new queries", [ +testAsyncMulti('ddp rate limiter - test removing rule with rateLimited ' + + 'client lets them send new queries', [ function (test, expect) { - _.extend(this, createTestUser(test, expect)); + Object.assign(this, createTestUser(test, expect)); }, function (test, expect) { - var self = this; - Meteor.call("addRuleToDDPRateLimiter", expect(function(error, result) { - self.ruleId = result; + Meteor.call('addRuleToDDPRateLimiter', expect((error, result) => { + this.ruleId = result; })); }, function (test, expect) { - Meteor.logout(expect(function (error) { + Meteor.logout(expect((error) => { test.equal(error, undefined); test.equal(Meteor.user(), null); })); }, function (test, expect) { - var self = this; // By removing the rule from the DDP rate limiter, we no longer restrict // them even though they were rate limited - Meteor.call('removeRuleFromDDPRateLimiter', self.ruleId, - expect(function(error, result) { - test.equal(result,true); - })); + Meteor.call('removeRuleFromDDPRateLimiter', this.ruleId, + expect((error, result) => { + test.equal(result, true); + }), + ); }, function (test, expect) { callFnMultipleTimesThenExpectResult(test, expect, Meteor.call.bind(Meteor, 'dummyMethod'), { expectedError: undefined, - expectedResult: "yup", - expectedRateLimitWillBeHit: false - } + expectedResult: 'yup', + expectedRateLimitWillBeHit: false, + }, ); callFnMultipleTimesThenExpectResult(test, expect, Meteor.call.bind(Meteor, 'dummyMethod'), { expectedError: undefined, - expectedResult: "yup", - expectedRateLimitWillBeHit: false - } + expectedResult: 'yup', + expectedRateLimitWillBeHit: false, + }, ); }, function (test, expect) { - Meteor.call("removeUserByUsername", this.username, expect(function () {})); - } + Meteor.call('removeUserByUsername', this.username, expect(function () {})); + }, ]); function createTestUser(test, expect) { const username = Random.id(); - const email = Random.id() + '-intercept@example.com'; + const email = `${Random.id()}-intercept@example.com`; const password = 'password'; - Accounts.createUser({ - username: username, - email: email, - password: password - }, - expect(function (error, result) { - test.equal(error, undefined); - test.notEqual(Meteor.userId(), null); - })); + Accounts.createUser( + { + username, + email, + password, + }, + expect((error) => { + test.equal(error, undefined); + test.notEqual(Meteor.userId(), null); + }), + ); - return {username, email, password}; -}; + return { username, email, password }; +} /** * A utility function that runs an arbitrary JavaScript function with a single * Node-style callback argument multiple times, verifying that the callback is * fired with certain arguments; then run the function one more time, - * conditionally verifying that the callback is now fired with the "too-many- - * request" rate limit error. + * conditionally verifying that the callback is now fired with the 'too-many- + * request' rate limit error. * * @param test As in testAsyncMulti * @param expect As in testAsyncMulti @@ -379,21 +377,28 @@ function createTestUser(test, expect) { * @param {boolean} expectedRateLimitWillBeHit Should we hit rate limit */ function callFnMultipleTimesThenExpectResult( - test, expect, fn, {expectedError, expectedResult, expectedRateLimitWillBeHit, - expectedIntervalTimeInMs}) { - - for (var i = 0; i < RATE_LIMIT_NUM_CALLS; i++) { - fn(expect(function (error, result) { + test, + expect, + fn, + { + expectedError, + expectedResult, + expectedRateLimitWillBeHit, + expectedIntervalTimeInMs, + }, +) { + for (let i = 0; i < RATE_LIMIT_NUM_CALLS; i++) { + fn(expect((error, result) => { test.equal(error && error.error, expectedError); test.equal(result, expectedResult); })); } - fn(expect(function (error, result) { + fn(expect((error, result) => { if (expectedRateLimitWillBeHit) { - test.equal(error && error.error, 'too-many-requests', 'error : ' + error); - test.isTrue(error && error.details.timeToReset < - expectedIntervalTimeInMs || RATE_LIMIT_INTERVAL_TIME_MS, 'too long'); + test.equal(error && error.error, 'too-many-requests', `error : ${error}`); + test.isTrue((error && error.details.timeToReset < + expectedIntervalTimeInMs) || RATE_LIMIT_INTERVAL_TIME_MS, 'too long'); test.equal(result, undefined, 'result is not undefined'); } else { test.equal(error && error.error, expectedError); diff --git a/packages/ddp-rate-limiter/ddp-rate-limiter.js b/packages/ddp-rate-limiter/ddp-rate-limiter.js index c2b5c335f28..5ffd6dac023 100644 --- a/packages/ddp-rate-limiter/ddp-rate-limiter.js +++ b/packages/ddp-rate-limiter/ddp-rate-limiter.js @@ -1,19 +1,23 @@ +import { RateLimiter } from 'meteor/rate-limit'; + // Rate Limiter built into DDP with a default error message. See README or // online documentation for more details. -DDPRateLimiter = {}; +const DDPRateLimiter = {}; -var errorMessage = function (rateLimitResult) { - return "Error, too many requests. Please slow down. You must wait " + - Math.ceil(rateLimitResult.timeToReset / 1000) + " seconds before " + - "trying again."; +let errorMessage = (rateLimitResult) => { + return 'Error, too many requests. Please slow down. You must wait ' + + `${Math.ceil(rateLimitResult.timeToReset / 1000)} seconds before ` + + 'trying again.'; }; -var rateLimiter = new RateLimiter(); -DDPRateLimiter.getErrorMessage = function (rateLimitResult) { - if (typeof errorMessage === 'function') +const rateLimiter = new RateLimiter(); + +DDPRateLimiter.getErrorMessage = (rateLimitResult) => { + if (typeof errorMessage === 'function') { return errorMessage(rateLimitResult); - else + } else { return errorMessage; + } }; /** @@ -25,7 +29,7 @@ DDPRateLimiter.getErrorMessage = function (rateLimitResult) { * of the error message. * @locus Server */ -DDPRateLimiter.setErrorMessage = function (message) { +DDPRateLimiter.setErrorMessage = (message) => { errorMessage = message; }; @@ -67,13 +71,10 @@ DDPRateLimiter.setErrorMessage = function (message) { * @param {function} callback function to be called after a rule is executed. * @locus Server */ -DDPRateLimiter.addRule = function (matcher, numRequests, timeInterval, callback) { - return rateLimiter.addRule(matcher, numRequests, timeInterval, callback); -}; +DDPRateLimiter.addRule = (matcher, numRequests, timeInterval, callback) => + rateLimiter.addRule(matcher, numRequests, timeInterval, callback); -DDPRateLimiter.printRules = function () { - return rateLimiter.rules; -}; +DDPRateLimiter.printRules = () => rateLimiter.rules; /** * @summary Removes the specified rule from the rate limiter. If rule had @@ -82,16 +83,14 @@ DDPRateLimiter.printRules = function () { * @return {boolean} True if a rule was removed. * @locus Server */ -DDPRateLimiter.removeRule = function (id) { - return rateLimiter.removeRule(id); -}; +DDPRateLimiter.removeRule = id => rateLimiter.removeRule(id); // This is accessed inside livedata_server.js, but shouldn't be called by any // user. -DDPRateLimiter._increment = function (input) { +DDPRateLimiter._increment = (input) => { rateLimiter.increment(input); }; -DDPRateLimiter._check = function (input) { - return rateLimiter.check(input); -}; +DDPRateLimiter._check = input => rateLimiter.check(input); + +export { DDPRateLimiter }; diff --git a/packages/ddp-rate-limiter/package.js b/packages/ddp-rate-limiter/package.js index ae990e7a932..730cededb7c 100644 --- a/packages/ddp-rate-limiter/package.js +++ b/packages/ddp-rate-limiter/package.js @@ -8,13 +8,14 @@ Package.describe({ git: '', // By default, Meteor will default to using README.md for documentation. // To avoid submitting documentation, set this field to null. - documentation: 'README.md' + documentation: 'README.md', }); Package.onUse(function(api) { api.use('rate-limit', 'server'); + api.use('ecmascript'); api.export('DDPRateLimiter', 'server'); - api.addFiles('ddp-rate-limiter.js', 'server'); + api.mainModule('ddp-rate-limiter.js', 'server'); }); Package.onTest(function(api) { @@ -24,7 +25,6 @@ Package.onTest(function(api) { 'ddp', 'ecmascript', 'es5-shim']); api.use('ddp-rate-limiter'); - api.addFiles('ddp-rate-limiter-tests-common.js'); - api.addFiles('ddp-rate-limiter-test-service.js', 'server'); - api.addFiles('ddp-rate-limiter-tests.js', 'client'); + api.mainModule('ddp-rate-limiter-test-service.js', 'server'); + api.mainModule('ddp-rate-limiter-tests.js', 'client'); }); diff --git a/packages/rate-limit/package.js b/packages/rate-limit/package.js index 8fb323d5679..fd9dcda61da 100644 --- a/packages/rate-limit/package.js +++ b/packages/rate-limit/package.js @@ -1,29 +1,29 @@ Package.describe({ name: 'rate-limit', - version: '1.0.8', + version: '1.0.9', // Brief, one-line summary of the package. summary: 'An algorithm for rate limiting anything', // URL to the Git repository containing the source code for this package. git: '', // By default, Meteor will default to using README.md for documentation. // To avoid submitting documentation, set this field to null. - documentation: 'README.md' + documentation: 'README.md', }); Package.onUse(function(api) { - api.use('underscore'); api.use('random'); - api.addFiles('rate-limit.js'); - api.export("RateLimiter"); + api.use('ecmascript'); + api.mainModule('rate-limit.js'); + api.export('RateLimiter'); }); Package.onTest(function(api) { api.use('test-helpers', ['client', 'server']); - api.use('underscore'); + api.use('ecmascript'); api.use('random'); api.use('ddp-rate-limiter'); api.use('tinytest'); api.use('rate-limit'); api.use('ddp-common'); - api.addFiles('rate-limit-tests.js'); + api.mainModule('rate-limit-tests.js'); }); diff --git a/packages/rate-limit/rate-limit-tests.js b/packages/rate-limit/rate-limit-tests.js index cdb8db5320d..5d22a58ae63 100644 --- a/packages/rate-limit/rate-limit-tests.js +++ b/packages/rate-limit/rate-limit-tests.js @@ -19,167 +19,160 @@ // XXX These tests should be refactored to use Tinytest.add instead of // testAsyncMulti as they're all on the server. Any future tests should be // written that way. +import { Meteor } from 'meteor/meteor'; +import { RateLimiter } from 'meteor/rate-limit'; +import { DDPCommon } from 'meteor/ddp-common'; + Tinytest.add('rate limit tests - Check empty constructor creation', function (test) { - r = new RateLimiter(); + const r = new RateLimiter(); test.equal(r.rules, {}); -}); + }, +); Tinytest.add('rate limit tests - Check single rule with multiple ' + 'invocations, only 1 that matches', - function (test) { - r = new RateLimiter(); - var userIdOne = 1; - var restrictJustUserIdOneRule = { - userId: userIdOne, - IPAddr: null, - method: null - }; - - r.addRule(restrictJustUserIdOneRule, 1, 1000); - var connectionHandle = createTempConnectionHandle(123, '127.0.0.1'); - var methodInvc1 = createTempMethodInvocation(userIdOne, connectionHandle, - 'login'); - var methodInvc2 = createTempMethodInvocation(2, connectionHandle, - 'login'); - for (var i = 0; i < 2; i++) { - r.increment(methodInvc1); - r.increment(methodInvc2); - } - test.equal(r.check(methodInvc1).allowed, false); - test.equal(r.check(methodInvc2).allowed, true); - }); +function (test) { + const r = new RateLimiter(); + const userIdOne = 1; + const restrictJustUserIdOneRule = { + userId: userIdOne, + IPAddr: null, + method: null, + }; + r.addRule(restrictJustUserIdOneRule, 1, 1000); + const connectionHandle = createTempConnectionHandle(123, '127.0.0.1'); + const methodInvc1 = createTempMethodInvocation(userIdOne, connectionHandle, + 'login'); + const methodInvc2 = createTempMethodInvocation(2, connectionHandle, + 'login'); + for (let i = 0; i < 2; i++) { + r.increment(methodInvc1); + r.increment(methodInvc2); + } + test.equal(r.check(methodInvc1).allowed, false); + test.equal(r.check(methodInvc2).allowed, true); +}, +); -testAsyncMulti("rate limit tests - Run multiple invocations and wait for one" + - " to reset", [ +testAsyncMulti('rate limit tests - Run multiple invocations and wait for one' + + ' to reset', [ function (test, expect) { - var self = this; - self.r = new RateLimiter(); - self.userIdOne = 1; - self.userIdTwo = 2; - self.restrictJustUserIdOneRule = { - userId: self.userIdOne, + this.r = new RateLimiter(); + this.userIdOne = 1; + this.userIdTwo = 2; + this.restrictJustUserIdOneRule = { + userId: this.userIdOne, IPAddr: null, - method: null + method: null, }; - self.r.addRule(self.restrictJustUserIdOneRule, 1, 500); - self.connectionHandle = createTempConnectionHandle(123, '127.0.0.1') - self.methodInvc1 = createTempMethodInvocation(self.userIdOne, - self.connectionHandle, 'login'); - self.methodInvc2 = createTempMethodInvocation(self.userIdTwo, - self.connectionHandle, 'login'); - for (var i = 0; i < 2; i++) { - self.r.increment(self.methodInvc1); - self.r.increment(self.methodInvc2); + this.r.addRule(this.restrictJustUserIdOneRule, 1, 500); + this.connectionHandle = createTempConnectionHandle(123, '127.0.0.1') + this.methodInvc1 = createTempMethodInvocation(this.userIdOne, + this.connectionHandle, 'login'); + this.methodInvc2 = createTempMethodInvocation(this.userIdTwo, + this.connectionHandle, 'login'); + for (let i = 0; i < 2; i++) { + this.r.increment(this.methodInvc1); + this.r.increment(this.methodInvc2); } - test.equal(self.r.check(self.methodInvc1).allowed, false); - test.equal(self.r.check(self.methodInvc2).allowed, true); - Meteor.setTimeout(expect(function () {}), 1000); + test.equal(this.r.check(this.methodInvc1).allowed, false); + test.equal(this.r.check(this.methodInvc2).allowed, true); + Meteor.setTimeout(expect(function () { }), 1000); }, - function (test, expect) { - var self = this; - for (var i = 0; i < 100; i++) { - self.r.increment(self.methodInvc2); + function (test) { + for (let i = 0; i < 100; i++) { + this.r.increment(this.methodInvc2); } - - test.equal(self.r.check(self.methodInvc1).allowed, true); - test.equal(self.r.check(self.methodInvc2).allowed, true); - } + test.equal(this.r.check(this.methodInvc1).allowed, true); + test.equal(this.r.check(this.methodInvc2).allowed, true); + }, ]); Tinytest.add('rate limit tests - Check two rules that affect same methodInvc' + - ' still throw', - function (test) { - r = new RateLimiter(); - var loginMethodRule = { - userId: null, - IPAddr: null, - method: 'login' - }; - var onlyLimitEvenUserIdRule = { - userId: function (userId) { - return userId % 2 === 0 - }, - IPAddr: null, - method: null - }; - r.addRule(loginMethodRule, 10, 100); - r.addRule(onlyLimitEvenUserIdRule, 4, 100); - - var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1'); - var methodInvc1 = createTempMethodInvocation(1, connectionHandle, - 'login'); - var methodInvc2 = createTempMethodInvocation(2, connectionHandle, - 'login'); - var methodInvc3 = createTempMethodInvocation(3, connectionHandle, - 'test'); - - for (var i = 0; i < 5; i++) { - r.increment(methodInvc1); - r.increment(methodInvc2); - r.increment(methodInvc3); - }; - - // After for loop runs, we only have 10 runs, so that's under the limit - test.equal(r.check(methodInvc1).allowed, true); - // However, this triggers userId rule since this userId is even - test.equal(r.check(methodInvc2).allowed, false); - test.equal(r.check(methodInvc2).allowed, false); - - // Running one more test causes it to be false, since we're at 11 now. + ' still throw', function (test) { + const r = new RateLimiter(); + const loginMethodRule = { + userId: null, + IPAddr: null, + method: 'login', + }; + const onlyLimitEvenUserIdRule = { + userId: userId => userId % 2 === 0, + IPAddr: null, + method: null, + }; + r.addRule(loginMethodRule, 10, 100); + r.addRule(onlyLimitEvenUserIdRule, 4, 100); + const connectionHandle = createTempConnectionHandle(1234, '127.0.0.1'); + const methodInvc1 = createTempMethodInvocation(1, connectionHandle, + 'login'); + const methodInvc2 = createTempMethodInvocation(2, connectionHandle, + 'login'); + const methodInvc3 = createTempMethodInvocation(3, connectionHandle, + 'test'); + for (let i = 0; i < 5; i++) { r.increment(methodInvc1); - test.equal(r.check(methodInvc1).allowed, false); - // 3rd Method Invocation isn't affected by either rules. - test.equal(r.check(methodInvc3).allowed, true); - - }); + r.increment(methodInvc2); + r.increment(methodInvc3); + } + // After for loop runs, we only have 10 runs, so that's under the limit + test.equal(r.check(methodInvc1).allowed, true); + // However, this triggers userId rule since this userId is even + test.equal(r.check(methodInvc2).allowed, false); + test.equal(r.check(methodInvc2).allowed, false); + // Running one more test causes it to be false, since we're at 11 now. + r.increment(methodInvc1); + test.equal(r.check(methodInvc1).allowed, false); + // 3rd Method Invocation isn't affected by either rules. + test.equal(r.check(methodInvc3).allowed, true); +}); Tinytest.add('rate limit tests - Check one rule affected by two different ' + - 'invocations', - function (test) { - r = new RateLimiter(); - var loginMethodRule = { - userId: null, - IPAddr: null, - method: 'login' - } - r.addRule(loginMethodRule, 10, 10000); + 'invocations', function (test) { + const r = new RateLimiter(); + const loginMethodRule = { + userId: null, + IPAddr: null, + method: 'login', + }; + r.addRule(loginMethodRule, 10, 10000); - var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1'); - var methodInvc1 = createTempMethodInvocation(1, connectionHandle, - 'login'); - var methodInvc2 = createTempMethodInvocation(2, connectionHandle, - 'login'); + const connectionHandle = createTempConnectionHandle(1234, '127.0.0.1'); + const methodInvc1 = createTempMethodInvocation(1, connectionHandle, + 'login'); + const methodInvc2 = createTempMethodInvocation(2, connectionHandle, + 'login'); - for (var i = 0; i < 5; i++) { - r.increment(methodInvc1); - r.increment(methodInvc2); - } - // This throws us over the limit since both increment the login rule - // counter + for (let i = 0; i < 5; i++) { r.increment(methodInvc1); + r.increment(methodInvc2); + } + // This throws us over the limit since both increment the login rule + // counter + r.increment(methodInvc1); - test.equal(r.check(methodInvc1).allowed, false); - test.equal(r.check(methodInvc2).allowed, false); - }); + test.equal(r.check(methodInvc1).allowed, false); + test.equal(r.check(methodInvc2).allowed, false); +}); -Tinytest.add("rate limit tests - add global rule", function (test) { - r = new RateLimiter(); - var globalRule = { +Tinytest.add('rate limit tests - add global rule', function (test) { + const r = new RateLimiter(); + const globalRule = { userId: null, IPAddr: null, - method: null - } + method: null, + }; r.addRule(globalRule, 1, 10000); - var connectionHandle = createTempConnectionHandle(1234, '127.0.0.1'); - var connectionHandle2 = createTempConnectionHandle(1234, '127.0.0.2'); + const connectionHandle = createTempConnectionHandle(1234, '127.0.0.1'); + const connectionHandle2 = createTempConnectionHandle(1234, '127.0.0.2'); - var methodInvc1 = createTempMethodInvocation(1, connectionHandle, + const methodInvc1 = createTempMethodInvocation(1, connectionHandle, 'login'); - var methodInvc2 = createTempMethodInvocation(2, connectionHandle2, + const methodInvc2 = createTempMethodInvocation(2, connectionHandle2, 'test'); - var methodInvc3 = createTempMethodInvocation(3, connectionHandle, + const methodInvc3 = createTempMethodInvocation(3, connectionHandle, 'user-accounts'); // First invocation, all methods would still be allowed. @@ -196,29 +189,27 @@ Tinytest.add("rate limit tests - add global rule", function (test) { Tinytest.add('rate limit tests - Fuzzy rule match does not trigger rate limit', function (test) { - r = new RateLimiter(); - var rule = { - a: function (inp) { - return inp % 3 == 0 - }, + const r = new RateLimiter(); + const rule = { + a: inp => inp % 3 === 0, b: 5, - c: "hi", - } + c: 'hi', + }; r.addRule(rule, 1, 10000); - var input = { + const input = { a: 3, - b: 5 - } - for (var i = 0; i < 5; i++) { + b: 5, + }; + for (let i = 0; i < 5; i++) { r.increment(input); } test.equal(r.check(input).allowed, true); - var matchingInput = { + const matchingInput = { a: 3, b: 5, - c: "hi", - d: 1 - } + c: 'hi', + d: 1, + }; r.increment(matchingInput); r.increment(matchingInput); // Past limit so should be false @@ -226,61 +217,59 @@ Tinytest.add('rate limit tests - Fuzzy rule match does not trigger rate limit', // Add secondary rule and check that longer time is returned when multiple // rules limits are hit - var newRule = { - a: function (inp) { - return inp % 3 == 0 - }, + const newRule = { + a: inp => inp % 3 === 0, b: 5, - c: "hi", - d: 1 - } + c: 'hi', + d: 1, + }; r.addRule(newRule, 1, 10); // First rule should still throw while second rule will trigger as well, // causing us to return longer time to reset to user r.increment(matchingInput); r.increment(matchingInput); test.equal(r.check(matchingInput).timeToReset > 50, true); - } + }, ); /****** Test Our Helper Methods *****/ -Tinytest.add("rate limit tests - test matchRule method", function (test) { - r = new RateLimiter(); - var globalRule = { +Tinytest.add('rate limit tests - test matchRule method', function (test) { + const r = new RateLimiter(); + const globalRule = { userId: null, IPAddr: null, type: null, - name: null - } - var globalRuleId = r.addRule(globalRule); + name: null, + }; + const globalRuleId = r.addRule(globalRule); - var rateLimiterInput = { + const rateLimiterInput = { userId: 1023, - IPAddr: "127.0.0.1", + IPAddr: '127.0.0.1', type: 'sub', - name: 'getSubLists' + name: 'getSubLists', }; test.equal(r.rules[globalRuleId].match(rateLimiterInput), true); - var oneNotNullRule = { + const oneNotNullRule = { userId: 102, IPAddr: null, type: null, - name: null - } + name: null, + }; - var oneNotNullId = r.addRule(oneNotNullRule); + const oneNotNullId = r.addRule(oneNotNullRule); test.equal(r.rules[oneNotNullId].match(rateLimiterInput), false); oneNotNullRule.userId = 1023; test.equal(r.rules[oneNotNullId].match(rateLimiterInput), true); - var notCompleteInput = { + const notCompleteInput = { userId: 102, - IPAddr: '127.0.0.1' + IPAddr: '127.0.0.1', }; test.equal(r.rules[globalRuleId].match(notCompleteInput), true); test.equal(r.rules[oneNotNullId].match(notCompleteInput), false); @@ -288,78 +277,76 @@ Tinytest.add("rate limit tests - test matchRule method", function (test) { Tinytest.add('rate limit tests - test generateMethodKey string', function (test) { - r = new RateLimiter(); - var globalRule = { + const r = new RateLimiter(); + const globalRule = { userId: null, IPAddr: null, type: null, - name: null - } - var globalRuleId = r.addRule(globalRule); + name: null, + }; + const globalRuleId = r.addRule(globalRule); - var rateLimiterInput = { + const rateLimiterInput = { userId: 1023, - IPAddr: "127.0.0.1", + IPAddr: '127.0.0.1', type: 'sub', - name: 'getSubLists' + name: 'getSubLists', }; - test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), ""); + test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), ''); globalRule.userId = 1023; test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), - "userId1023"); + 'userId1023'); - var ruleWithFuncs = { - userId: function (input) { - return input % 2 === 0 - }, + const ruleWithFuncs = { + userId: input => input % 2 === 0, IPAddr: null, - type: null + type: null, }; - var funcRuleId = r.addRule(ruleWithFuncs); - test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), ""); + const funcRuleId = r.addRule(ruleWithFuncs); + test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), ''); rateLimiterInput.userId = 1024; test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), - "userId1024"); + 'userId1024'); - var multipleRules = ruleWithFuncs; + const multipleRules = ruleWithFuncs; multipleRules.IPAddr = '127.0.0.1'; - var multipleRuleId = r.addRule(multipleRules); + const multipleRuleId = r.addRule(multipleRules); test.equal(r.rules[multipleRuleId]._generateKeyString(rateLimiterInput), - "userId1024IPAddr127.0.0.1") - } + 'userId1024IPAddr127.0.0.1'); + }, ); function createTempConnectionHandle(id, clientIP) { return { - id: id, - close: function () { - self.close(); + id, + close() { + this.close(); }, - onClose: function (fn) { - var cb = Meteor.bindEnvironment(fn, "connection onClose callback"); - if (self.inQueue) { - self._closeCallbacks.push(cb); + onClose(fn) { + const cb = Meteor.bindEnvironment(fn, 'connection onClose callback'); + if (this.inQueue) { + this._closeCallbacks.push(cb); } else { // if we're already closed, call the callback. Meteor.defer(cb); } }, clientAddress: clientIP, - httpHeaders: null + httpHeaders: null, }; } function createTempMethodInvocation(userId, connectionHandle, methodName) { - var methodInv = new DDPCommon.MethodInvocation({ + const methodInv = new DDPCommon.MethodInvocation({ isSimulation: false, - userId: userId, + userId, setUserId: null, unblock: false, connection: connectionHandle, - randomSeed: 1234 + randomSeed: 1234, }); methodInv.method = methodName; return methodInv; -} \ No newline at end of file +} diff --git a/packages/rate-limit/rate-limit.js b/packages/rate-limit/rate-limit.js index 7e772a7e264..46e244b81f7 100644 --- a/packages/rate-limit/rate-limit.js +++ b/packages/rate-limit/rate-limit.js @@ -1,7 +1,12 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; + // Default time interval (in milliseconds) to reset rate limit counters -var DEFAULT_INTERVAL_TIME_IN_MILLISECONDS = 1000; +const DEFAULT_INTERVAL_TIME_IN_MILLISECONDS = 1000; // Default number of events allowed per time interval -var DEFAULT_REQUESTS_PER_INTERVAL = 10; +const DEFAULT_REQUESTS_PER_INTERVAL = 10; + +const hasOwn = Object.prototype.hasOwnProperty; // A rule is defined by an options object that contains two fields, // `numRequestsAllowed` which is the number of events allowed per interval, and @@ -17,56 +22,48 @@ var DEFAULT_REQUESTS_PER_INTERVAL = 10; // counter reaches the `numRequestsAllowed` within a given `intervalTime`, a // rate limit is reached and future inputs that map to that counter will // result in errors being returned to the client. -var Rule = function (options, matchers) { - var self = this; - - self.id = Random.id(); +class Rule { + constructor(options, matchers) { + this.id = Random.id(); - self.options = options; + this.options = options; - self._matchers = matchers; + this._matchers = matchers; - self._lastResetTime = new Date().getTime(); + this._lastResetTime = new Date().getTime(); - // Dictionary of input keys to counters - self.counters = {}; -}; - -_.extend(Rule.prototype, { + // Dictionary of input keys to counters + this.counters = {}; + } // Determine if this rule applies to the given input by comparing all // rule.matchers. If the match fails, search short circuits instead of // iterating through all matchers. - match: function (input) { - var self = this; - var ruleMatches = true; - return _.every(self._matchers, function (matcher, key) { - if (matcher !== null) { - if (!(_.has(input,key))) { - return false; - } else { - if (typeof matcher === 'function') { + match(input) { + return Object + .entries(this._matchers) + .every(([key, matcher]) => { + if (matcher !== null) { + if (!hasOwn.call(input, key)) { + return false; + } else if (typeof matcher === 'function') { if (!(matcher(input[key]))) { return false; } - } else { - if (matcher !== input[key]) { - return false; - } + } else if (matcher !== input[key]) { + return false; } } - } - return true; - }); - }, + return true; + }); + } // Generates unique key string for provided input by concatenating all the // keys in the matcher with the corresponding values in the input. // Only called if rule matches input. - _generateKeyString: function (input) { - var self = this; - var returnString = ""; - _.each(self._matchers, function (matcher, key) { - if (matcher !== null) { + _generateKeyString(input) { + return Object.entries(this._matchers) + .filter(([key]) => this._matchers[key] !== null) + .reduce((returnString, [key, matcher]) => { if (typeof matcher === 'function') { if (matcher(input[key])) { returnString += key + input[key]; @@ -74,35 +71,33 @@ _.extend(Rule.prototype, { } else { returnString += key + input[key]; } - } - }); - return returnString; - }, + return returnString; + }, ''); + } // Applies the provided input and returns the key string, time since counters // were last reset and time to next reset. - apply: function (input) { - var self = this; - var keyString = self._generateKeyString(input); - var timeSinceLastReset = new Date().getTime() - self._lastResetTime; - var timeToNextReset = self.options.intervalTime - timeSinceLastReset; + apply(input) { + const key = this._generateKeyString(input); + const timeSinceLastReset = new Date().getTime() - this._lastResetTime; + const timeToNextReset = this.options.intervalTime - timeSinceLastReset; return { - key: keyString, - timeSinceLastReset: timeSinceLastReset, - timeToNextReset: timeToNextReset + key, + timeSinceLastReset, + timeToNextReset, }; - }, + } + // Reset counter dictionary for this specific rule. Called once the // timeSinceLastReset has exceeded the intervalTime. _lastResetTime is // set to be the current time in milliseconds. - resetCounter: function () { - var self = this; - + resetCounter() { // Delete the old counters dictionary to allow for garbage collection - self.counters = {}; - self._lastResetTime = new Date().getTime(); - }, - _executeCallback: function (reply, ruleInput) { + this.counters = {}; + this._lastResetTime = new Date().getTime(); + } + + _executeCallback(reply, ruleInput) { try { if (this.options.callback) { this.options.callback(reply, ruleInput); @@ -111,169 +106,162 @@ _.extend(Rule.prototype, { // Do not throw error here console.error(e); } - }, -}); - -// Initialize rules to be an empty dictionary. -RateLimiter = function () { - var self = this; + } +} - // Dictionary of all rules associated with this RateLimiter, keyed by their - // id. Each rule object stores the rule pattern, number of events allowed, - // last reset time and the rule reset interval in milliseconds. - self.rules = {}; -}; +class RateLimiter { + // Initialize rules to be an empty dictionary. + constructor() { + // Dictionary of all rules associated with this RateLimiter, keyed by their + // id. Each rule object stores the rule pattern, number of events allowed, + // last reset time and the rule reset interval in milliseconds. -/** - * Checks if this input has exceeded any rate limits. - * @param {object} input dictionary containing key-value pairs of attributes - * that match to rules - * @return {object} Returns object of following structure - * { 'allowed': boolean - is this input allowed - * 'timeToReset': integer | Infinity - returns time until counters are reset - * in milliseconds - * 'numInvocationsLeft': integer | Infinity - returns number of calls left - * before limit is reached - * } - * If multiple rules match, the least number of invocations left is returned. - * If the rate limit has been reached, the longest timeToReset is returned. - */ -RateLimiter.prototype.check = function (input) { - var self = this; - var reply = { - allowed: true, - timeToReset: 0, - numInvocationsLeft: Infinity, - }; + this.rules = {}; + } - var matchedRules = self._findAllMatchingRules(input); - _.each(matchedRules, function (rule) { - var ruleResult = rule.apply(input); - var numInvocations = rule.counters[ruleResult.key]; + /** + * Checks if this input has exceeded any rate limits. + * @param {object} input dictionary containing key-value pairs of attributes + * that match to rules + * @return {object} Returns object of following structure + * { 'allowed': boolean - is this input allowed + * 'timeToReset': integer | Infinity - returns time until counters are reset + * in milliseconds + * 'numInvocationsLeft': integer | Infinity - returns number of calls left + * before limit is reached + * } + * If multiple rules match, the least number of invocations left is returned. + * If the rate limit has been reached, the longest timeToReset is returned. + */ + check(input) { + const reply = { + allowed: true, + timeToReset: 0, + numInvocationsLeft: Infinity, + }; - if (ruleResult.timeToNextReset < 0) { - // Reset all the counters since the rule has reset - rule.resetCounter(); - ruleResult.timeSinceLastReset = new Date().getTime() - - rule._lastResetTime; - ruleResult.timeToNextReset = rule.options.intervalTime; - numInvocations = 0; - } + const matchedRules = this._findAllMatchingRules(input); + matchedRules.forEach((rule) => { + const ruleResult = rule.apply(input); + let numInvocations = rule.counters[ruleResult.key]; - if (numInvocations > rule.options.numRequestsAllowed) { - // Only update timeToReset if the new time would be longer than the - // previously set time. This is to ensure that if this input triggers - // multiple rules, we return the longest period of time until they can - // successfully make another call - if (reply.timeToReset < ruleResult.timeToNextReset) { - reply.timeToReset = ruleResult.timeToNextReset; - }; - reply.allowed = false; - reply.numInvocationsLeft = 0; - rule._executeCallback(reply, input); - } else { - // If this is an allowed attempt and we haven't failed on any of the - // other rules that match, update the reply field. - if (rule.options.numRequestsAllowed - numInvocations < - reply.numInvocationsLeft && reply.allowed) { - reply.timeToReset = ruleResult.timeToNextReset; - reply.numInvocationsLeft = rule.options.numRequestsAllowed - - numInvocations; + if (ruleResult.timeToNextReset < 0) { + // Reset all the counters since the rule has reset + rule.resetCounter(); + ruleResult.timeSinceLastReset = new Date().getTime() - + rule._lastResetTime; + ruleResult.timeToNextReset = rule.options.intervalTime; + numInvocations = 0; } - rule._executeCallback(reply, input); - } - }); - return reply; -}; - -/** - * Adds a rule to dictionary of rules that are checked against on every call. - * Only inputs that pass all of the rules will be allowed. Returns unique rule - * id that can be passed to `removeRule`. - * @param {object} rule Input dictionary defining certain attributes and - * rules associated with them. - * Each attribute's value can either be a value, a function or null. All - * functions must return a boolean of whether the input is matched by that - * attribute's rule or not - * @param {integer} numRequestsAllowed Optional. Number of events allowed per - * interval. Default = 10. - * @param {integer} intervalTime Optional. Number of milliseconds before - * rule's counters are reset. Default = 1000. - * @param {function} callback Optional. Function to be called after a - * rule is executed. Two objects will be passed to this function. - * The first one is the result of RateLimiter.prototype.check - * The second is the input object of the rule, it has the following structure: - * { - * 'type': string - either 'method' or 'subscription' - * 'name': string - the name of the method or subscription being called - * 'userId': string - the user ID attempting the method or subscription - * 'connectionId': string - a string representing the user's DDP connection - * 'clientAddress': string - the IP address of the user - * } - * @return {string} Returns unique rule id - */ -RateLimiter.prototype.addRule = function (rule, numRequestsAllowed, - intervalTime, callback) { - var self = this; - var options = { - numRequestsAllowed: numRequestsAllowed || DEFAULT_REQUESTS_PER_INTERVAL, - intervalTime: intervalTime || DEFAULT_INTERVAL_TIME_IN_MILLISECONDS, - callback: callback && Meteor.bindEnvironment(callback), - }; + if (numInvocations > rule.options.numRequestsAllowed) { + // Only update timeToReset if the new time would be longer than the + // previously set time. This is to ensure that if this input triggers + // multiple rules, we return the longest period of time until they can + // successfully make another call + if (reply.timeToReset < ruleResult.timeToNextReset) { + reply.timeToReset = ruleResult.timeToNextReset; + } + reply.allowed = false; + reply.numInvocationsLeft = 0; + rule._executeCallback(reply, input); + } else { + // If this is an allowed attempt and we haven't failed on any of the + // other rules that match, update the reply field. + if (rule.options.numRequestsAllowed - numInvocations < + reply.numInvocationsLeft && reply.allowed) { + reply.timeToReset = ruleResult.timeToNextReset; + reply.numInvocationsLeft = rule.options.numRequestsAllowed - + numInvocations; + } + rule._executeCallback(reply, input); + } + }); + return reply; + } - var newRule = new Rule(options, rule); - this.rules[newRule.id] = newRule; - return newRule.id; -}; + /** + * Adds a rule to dictionary of rules that are checked against on every call. + * Only inputs that pass all of the rules will be allowed. Returns unique rule + * id that can be passed to `removeRule`. + * @param {object} rule Input dictionary defining certain attributes and + * rules associated with them. + * Each attribute's value can either be a value, a function or null. All + * functions must return a boolean of whether the input is matched by that + * attribute's rule or not + * @param {integer} numRequestsAllowed Optional. Number of events allowed per + * interval. Default = 10. + * @param {integer} intervalTime Optional. Number of milliseconds before + * rule's counters are reset. Default = 1000. + * @param {function} callback Optional. Function to be called after a + * rule is executed. Two objects will be passed to this function. + * The first one is the result of RateLimiter.prototype.check + * The second is the input object of the rule, it has the following structure: + * { + * 'type': string - either 'method' or 'subscription' + * 'name': string - the name of the method or subscription being called + * 'userId': string - the user ID attempting the method or subscription + * 'connectionId': string - a string representing the user's DDP connection + * 'clientAddress': string - the IP address of the user + * } + * @return {string} Returns unique rule id + */ + addRule(rule, numRequestsAllowed, intervalTime, callback) { + const options = { + numRequestsAllowed: numRequestsAllowed || DEFAULT_REQUESTS_PER_INTERVAL, + intervalTime: intervalTime || DEFAULT_INTERVAL_TIME_IN_MILLISECONDS, + callback: callback && Meteor.bindEnvironment(callback), + }; -/** - * Increment counters in every rule that match to this input - * @param {object} input Dictionary object containing attributes that may - * match to rules - */ -RateLimiter.prototype.increment = function (input) { - var self = this; + const newRule = new Rule(options, rule); + this.rules[newRule.id] = newRule; + return newRule.id; + } - // Only increment rule counters that match this input - var matchedRules = self._findAllMatchingRules(input); - _.each(matchedRules, function (rule) { - var ruleResult = rule.apply(input); + /** + * Increment counters in every rule that match to this input + * @param {object} input Dictionary object containing attributes that may + * match to rules + */ + increment(input) { + // Only increment rule counters that match this input + const matchedRules = this._findAllMatchingRules(input); + matchedRules.forEach((rule) => { + const ruleResult = rule.apply(input); - if (ruleResult.timeSinceLastReset > rule.options.intervalTime) { - // Reset all the counters since the rule has reset - rule.resetCounter(); - } + if (ruleResult.timeSinceLastReset > rule.options.intervalTime) { + // Reset all the counters since the rule has reset + rule.resetCounter(); + } - // Check whether the key exists, incrementing it if so or otherwise - // adding the key and setting its value to 1 - if (_.has(rule.counters, ruleResult.key)) - rule.counters[ruleResult.key]++; - else - rule.counters[ruleResult.key] = 1; - }); -}; + // Check whether the key exists, incrementing it if so or otherwise + // adding the key and setting its value to 1 + if (hasOwn.call(rule.counters, ruleResult.key)) { + rule.counters[ruleResult.key]++; + } else { + rule.counters[ruleResult.key] = 1; + } + }); + } -// Returns an array of all rules that apply to provided input -RateLimiter.prototype._findAllMatchingRules = function (input) { - var self = this; + // Returns an array of all rules that apply to provided input + _findAllMatchingRules(input) { + return Object.values(this.rules).filter(rule => rule.match(input)); + } - return _.filter(self.rules, function(rule) { - return rule.match(input); - }); -}; -/** - * Provides a mechanism to remove rules from the rate limiter. Returns boolean - * about success. - * @param {string} id Rule id returned from #addRule - * @return {boolean} Returns true if rule was found and deleted, else false. - */ -RateLimiter.prototype.removeRule = function (id) { - var self = this; - if (self.rules[id]) { - delete self.rules[id]; - return true; - } else { + /** + * Provides a mechanism to remove rules from the rate limiter. Returns boolean + * about success. + * @param {string} id Rule id returned from #addRule + * @return {boolean} Returns true if rule was found and deleted, else false. + */ + removeRule(id) { + if (this.rules[id]) { + delete this.rules[id]; + return true; + } return false; } -}; +} + +export { RateLimiter };