mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Added documentation to rate limit package and wrote more tests.
Cleaned up code in rate limit, added appropriate packages to package.js and refactored rate limit.
This commit is contained in:
@@ -17,8 +17,10 @@ Package.onUse(function(api) {
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
api.use('test-helpers', ['client', 'server']);
|
||||
api.use('ddp-rate-limiter');
|
||||
api.use('tinytest');
|
||||
api.use('ddp-common');
|
||||
api.use('rate-limit');
|
||||
api.use('ddp-common');
|
||||
api.addFiles('rate-limit-tests.js');
|
||||
});
|
||||
|
||||
@@ -37,6 +37,34 @@ Tinytest.add('Check single rule with multiple invocations, only 1 that matches',
|
||||
}, 1000); */
|
||||
});
|
||||
|
||||
/*testAsyncMulti("Run multiple invocations and wait for one to return", [
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
self.r = new RateLimiter();
|
||||
self.myUserId = 1;
|
||||
self.rule1 = { userId: self.myUserId, IPAddr: null, method: null};
|
||||
|
||||
self.r.addRule(self.rule1, 1, 1000);
|
||||
self.connectionHandle = createTempConnectionHandle(123, '127.0.0.1')
|
||||
self.methodInvc1 = createTempMethodInvocation(self.myUserId, self.connectionHandle, 'login');
|
||||
self.methodInvc2 = createTempMethodInvocation(2, self.connectionHandle, 'login');
|
||||
for (var i = 0; i < 2; i++) {
|
||||
self.r.increment(self.methodInvc1);
|
||||
self.r.increment(self.methodInvc2);
|
||||
}
|
||||
test.equal(self.r.check(self.methodInvc1).valid, false);
|
||||
test.equal(self.r.check(self.methodInvc2).valid, true);
|
||||
setTimeout(expect(function(){}), 1000);
|
||||
}, function (test, expect) {
|
||||
var self = this;
|
||||
for (var i = 0; i < 100; i++) {
|
||||
self.r.increment(self.methodInvc2);
|
||||
}
|
||||
|
||||
test.equal(self.r.check(self.methodInvc1).valid, true);
|
||||
test.equal(self.r.check(self.methodInvc2).valid, true);
|
||||
}]); */
|
||||
|
||||
Tinytest.add('Check two rules that affect same methodInvc still throw', function (test) {
|
||||
r = new RateLimiter();
|
||||
var loginRule = { userId: null, IPAddr: null, method: 'login'};
|
||||
@@ -106,8 +134,6 @@ Tinytest.add("add global rule", function (test) {
|
||||
test.equal(r.check(methodInvc1).valid, false);
|
||||
test.equal(r.check(methodInvc2).valid, false);
|
||||
test.equal(r.check(methodInvc3).valid, false);
|
||||
|
||||
|
||||
})
|
||||
|
||||
function createTempConnectionHandle(id, clientIP) {
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
// Write your package code here!
|
||||
// Default time interval (in milliseconds) to reset rate limit counters
|
||||
var DEFAULT_INTERVAL_TIME_IN_MILLISECONDS = 1000;
|
||||
// Default number of requets allowed per time interval
|
||||
var DEFAULT_REQUESTS_PER_INTERVAL = 10;
|
||||
var RateLimitingDict = {
|
||||
userId: 'userId',
|
||||
IPAddr: 'connection.clientAddress',
|
||||
method: 'method'
|
||||
}
|
||||
|
||||
var ruleMappingtoMethodInvocationDict = function(d, m) {
|
||||
var arr = RateLimitingDict[d].split('.');
|
||||
while (firstGuy = arr.shift()) {
|
||||
if (m[firstGuy])
|
||||
m = m[firstGuy];
|
||||
}
|
||||
return m;
|
||||
};
|
||||
// Mapping from rate limiting rules format to method invocation fields.
|
||||
var RATE_LIMITING_DICT = {
|
||||
userId: 'userId',
|
||||
IPAddr: 'connection.clientAddress',
|
||||
method: 'method'
|
||||
}
|
||||
|
||||
// Initialize rules, ruleId, and invocations to be empty
|
||||
RateLimiter = function () {
|
||||
var self = this;
|
||||
self.rules = [];
|
||||
@@ -24,9 +18,9 @@ RateLimiter = function () {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object of invocation validity, time to reset and number of calls left
|
||||
* @param {[type]}
|
||||
* @return {[type]}
|
||||
* Checks if this method invocation is valid
|
||||
* @param {object} methodInvocation DDPCommon.MethodInvocation object with added 'method' attribute listing the method name
|
||||
* @return {object} Returns object of whether method invocation is valid, time to next reset and number invocations left
|
||||
*/
|
||||
RateLimiter.prototype.check = function(methodInvocation) {
|
||||
var self = this;
|
||||
@@ -35,7 +29,7 @@ RateLimiter.prototype.check = function(methodInvocation) {
|
||||
_.find(self.rules, function(rule) {
|
||||
// Check if this rule should be applied for this method invocation
|
||||
if (RateLimiter.prototype.matchRuleUsingFind(rule, methodInvocation)) {
|
||||
var methodString = RateLimiter._generateMethodInvocationKeyStringFromRuleMapping(rule, methodInvocation);
|
||||
var methodString = self._generateMethodInvocationKeyStringFromRuleMapping(rule, methodInvocation);
|
||||
var timeSinceLastReset = new Date().getTime() - rule._lastResetTime;
|
||||
var timeToNextReset = rule.intervalTime - timeSinceLastReset;
|
||||
var numInvocations = self.ruleInvocationCounters[rule.ruleId][methodString];
|
||||
@@ -50,6 +44,7 @@ RateLimiter.prototype.check = function(methodInvocation) {
|
||||
}
|
||||
}
|
||||
});
|
||||
// console.log('method invocation ', methodInvocation, ' reply :' , reply);
|
||||
return reply;
|
||||
}
|
||||
|
||||
@@ -66,6 +61,16 @@ RateLimiter.prototype.check = function(methodInvocation) {
|
||||
* @param {int} numRequestsAllowed number of requests allowed per timeframe
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds a rule to with a specified identifier query to list of rules that are checked against on every method invocation
|
||||
* @param {object} identifierQuery Specified domain for rate limit rule
|
||||
* { userId: ID | function() | null,
|
||||
* IPAddr: ID | function() | null,
|
||||
* method: name | function() | null
|
||||
* ** All functions must return true/false to input to determine whether it applies or not **
|
||||
* @param {integer} numRequestsAllowed Number of requests allowed per interval
|
||||
* @param {integer} intervalTime Number of milliseconds before interval is reset
|
||||
*/
|
||||
RateLimiter.prototype.addRule = function(identifierQuery, numRequestsAllowed, intervalTime) {
|
||||
identifierQuery.ruleId = this._createNewRuleId();
|
||||
identifierQuery.numRequestsAllowed = numRequestsAllowed ? numRequestsAllowed : DEFAULT_REQUESTS_PER_INTERVAL;
|
||||
@@ -73,52 +78,24 @@ RateLimiter.prototype.addRule = function(identifierQuery, numRequestsAllowed, in
|
||||
identifierQuery._lastResetTime = new Date().getTime();
|
||||
this.rules.push(identifierQuery);
|
||||
}
|
||||
/**
|
||||
* Initial version of Match Rule - NO LONGER NECESSARY :D
|
||||
* @param {[type]}
|
||||
* @param {[type]}
|
||||
* @return {[type]}
|
||||
*/
|
||||
RateLimiter.prototype.matchRule = function(rule, methodInvocation) {
|
||||
console.log(rule);
|
||||
var results = _.map(rule, function(value, key) {
|
||||
if (typeof value === 'function') {
|
||||
// TODO BROKEN - metod invocation in this case must be specified to be an IP, user.Id or client addr
|
||||
if (! value(methodInvocation)) {
|
||||
return false;
|
||||
}
|
||||
} else if (value === null) {
|
||||
return;
|
||||
} else if (key === 'userId') {
|
||||
if (value !== methodInvocation.userId) {
|
||||
return false;
|
||||
}
|
||||
} else if (key === 'IPAddr') {
|
||||
if (value !== methodInvocation.connection.clientAddress) {
|
||||
return false;
|
||||
}
|
||||
} else if (key === 'method') {
|
||||
if (value !== methodInvocation.method) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
var firstFalse = _.find(results, function(v) { return !v; });
|
||||
return firstFalse !== false;
|
||||
}
|
||||
/**
|
||||
* @param {object} rule Rule that is defined according to the spec above
|
||||
* @param {object} methodInvocation Method Invocation as described in DDPCommon.MethodInvocation with an added field of method
|
||||
* @return {boolean} boolean True if this methodInvocation matches said rule, false otherwise
|
||||
*/
|
||||
/**
|
||||
* Matches whether a given method invocation matches a certain rule. Short circuits search if rule and method invocation don't match
|
||||
* @param {object} rule Rule as defined as an identifierQuery above
|
||||
* @param {object} methodInvocation DDPCommon.MethodInvocation object with added 'method' attribute listing the method name
|
||||
* @return {boolean} Returns whether the methodInvocation matches inputted rule
|
||||
*/
|
||||
RateLimiter.prototype.matchRuleUsingFind = function(rule, methodInvocation) {
|
||||
var self = this;
|
||||
var ruleMatches = true;
|
||||
|
||||
_.find(RateLimitingDict, function(value, key) {
|
||||
_.find(RATE_LIMITING_DICT, function(value, key) {
|
||||
if (rule[key]) {
|
||||
var methodInvocationValue = ruleMappingtoMethodInvocationDict(key, methodInvocation);
|
||||
var methodInvocationValue = self._ruleMappingtoMethodInvocationDict(key, methodInvocation);
|
||||
if (typeof rule[key] === 'function') {
|
||||
if (! rule[key](methodInvocationValue)) {
|
||||
ruleMatches = false;
|
||||
@@ -137,8 +114,8 @@ RateLimiter.prototype.matchRuleUsingFind = function(rule, methodInvocation) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} methodInvocation Method invocation object
|
||||
* @return {[type]}
|
||||
* Increment appropriate counters on every method invocation
|
||||
* @param {object} methodInvocation DDPCommon.MethodInvocation object with added 'method' attribute listing the method name
|
||||
*/
|
||||
RateLimiter.prototype.increment = function(methodInvocation) {
|
||||
var self = this;
|
||||
@@ -146,13 +123,13 @@ RateLimiter.prototype.increment = function(methodInvocation) {
|
||||
_.each(this.rules, function(rule) {
|
||||
// Check if this rule should be applied for this method invocation
|
||||
if (RateLimiter.prototype.matchRuleUsingFind(rule, methodInvocation)) {
|
||||
var methodString = RateLimiter._generateMethodInvocationKeyStringFromRuleMapping(rule, methodInvocation);
|
||||
var methodString = self._generateMethodInvocationKeyStringFromRuleMapping(rule, methodInvocation);
|
||||
var timeSinceLastReset = new Date().getTime() - rule._lastResetTime;
|
||||
if (timeSinceLastReset > rule.intervalTime) {
|
||||
rule._lastResetTime = new Date().getTime();
|
||||
// Reset all the counters for this rule
|
||||
_.each(self.ruleInvocationCounters[rule.ruleId], function(methodString) {
|
||||
methodString = 0;
|
||||
_.each(self.ruleInvocationCounters[rule.ruleId], function(value, methodString) {
|
||||
self.ruleInvocationCounters[rule.ruleId][methodString] = 0;
|
||||
});
|
||||
}
|
||||
var timeToNextReset = rule.intervalTime - timeSinceLastReset;
|
||||
@@ -170,23 +147,46 @@ RateLimiter.prototype.increment = function(methodInvocation) {
|
||||
});
|
||||
}
|
||||
|
||||
// Creates new unique rule id
|
||||
RateLimiter.prototype._createNewRuleId = function() {
|
||||
return this.ruleId++;
|
||||
}
|
||||
|
||||
RateLimiter._generateMethodInvocationKeyStringFromRuleMapping = function(rule, methodInvocation) {
|
||||
/**
|
||||
* Generates string of fields that match between method invocation and rule to be used as a key for counters dictionary per rule
|
||||
* @param {object} rule Rule defined as identifierQuery in addRule
|
||||
* @param {object} methodInvocation DDPCommon.MethodInvocation object with added 'method' attribute listing the method name
|
||||
* @return {string} Key string made of all fields from rule that match in method invocation
|
||||
*/
|
||||
RateLimiter.prototype._generateMethodInvocationKeyStringFromRuleMapping = function(rule, methodInvocation) {
|
||||
var self = this;
|
||||
var returnString = "";
|
||||
|
||||
_.each(RateLimitingDict, function(value, key) {
|
||||
_.each(RATE_LIMITING_DICT, function(value, key) {
|
||||
if (rule[key]) {
|
||||
var methodValue = ruleMappingtoMethodInvocationDict(key, methodInvocation);
|
||||
var methodValue = self._ruleMappingtoMethodInvocationDict(key, methodInvocation);
|
||||
if (typeof rule[key] === 'function') {
|
||||
returnString += key + rule[key](methodValue)
|
||||
}
|
||||
returnString += key + methodValue;
|
||||
if (rule[key](methodValue))
|
||||
returnString += key + methodValue;
|
||||
} else {
|
||||
returnString += key + methodValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
return returnString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that uses the RATE_LIMITING_DICT to create a fast way to access values in methodInvocation without manually parsing the paths
|
||||
* @param {string} key Key in rule dictionary (ie userId, IPAddr, method)
|
||||
* @param {string} methodInvocation MethodInvocation object that is traversed to get the final value
|
||||
* @return {object} Returns a string, value, or object of whatever is stored in appropriate field in MethodInvocation
|
||||
*/
|
||||
RateLimiter.prototype._ruleMappingtoMethodInvocationDict = function(key, methodInvocation) {
|
||||
|
||||
var arr = RATE_LIMITING_DICT[key].split('.');
|
||||
while (firstGuy = arr.shift()) {
|
||||
if (firstGuy in methodInvocation)
|
||||
methodInvocation = methodInvocation[firstGuy];
|
||||
}
|
||||
return methodInvocation;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user