diff --git a/README.md b/README.md index 5a6e267..59741b9 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,21 @@ # Simple Rate Limiter Filter -A basic rate limiter implementation filter for [Clyde](https://github.com/acanimal/clyde) API gateway, which allows to limit the provider's rate consume. +A basic rate limiter implementation filter for [Clyde](https://github.com/acanimal/clyde) API gateway, which allows to limit the rate consume. > Implementation is based on [limiter](https://github.com/jhurliman/node-rate-limiter) module. -- [Configuration](#configuration) - - [Examples](#examples) - - [Limit global access to 100 req/sec](#limit-global-access-to-100-reqsec) - - [Limit access to a provider to 100 req/sec](#limit-access-to-a-provider-to-100-reqsec) - - [Limit access to a provider to 100 req/sec and to the `userA` consumer rate limited to 20 req/sec](#limit-access-to-a-provider-to-100-reqsec-and-to-the-usera-consumer-rate-limited-to-20-reqsec) - - [Notes:](#notes) -- [License](#license) +- Configuration + - Examples + - Notes +- License ## Configuration -Rate limiter filter is extremely flexible and allows limit: -* Global access to the Clyde server. -* Global access by a given consumer. -* Access to a given provider. -* Access to a given provider by a given consumer. -* or chain all the previous limitations. - -The filter accepts the configuration properties: +Rate limiter filter is extremely flexible and allows limit access globally or per consumer. The filter accepts the configuration properties: * `global`: Specifies the limits to be applied globally. It must be an object with the properties: - `tokens`: Number of allowed access @@ -33,12 +23,7 @@ The filter accepts the configuration properties: * `consumers`: Specifies the global limits per consumers. For each consumer and object with `tokens` and `interval` properties must be specified. -* `providers`: Specifies the limits for each providers. It must be an object with the properties: - - `global`: Indicates limits applied to the provider (no matter which consumer). - - `consumers`: Indicates the limits per consumer. - -At least one property must be specified, that is, at least `global`, `consumers` or `providers` must be set. - +At least one property must be specified, that is, at least `global` or `consumers` must be set. ## Examples @@ -48,8 +33,8 @@ At least one property must be specified, that is, at least `global`, `consumers` { "prefilters" : [ { - "id" : "rate-limit", - "path" : "simple-rate-limit", + "id" : "rate-limiter", + "path" : "clydeio-simple-rate-limiter", "config" : { "global" : { "tokens" : 100, @@ -58,6 +43,8 @@ At least one property must be specified, that is, at least `global`, `consumers` ... } } + ], + "provider": [ ... ] } @@ -67,24 +54,24 @@ At least one property must be specified, that is, at least `global`, `consumers` ```javascript { - "prefilters" : [ + providers: [ { - "id" : "rate-limit", - "path" : "simple-rate-limit", - "config" : { - "providers" : { - "providerId" : { + "id": "idProvider", + "context": "/provider", + "target": "http://provider_server", + "prefilters" : [ + { + "id" : "rate-limiter", + "path" : "clydeio-simple-rate-limit", + "config" : { "global" : { "tokens" : 100, "interval" : "second" } - }, - ... - }, - ... - } + } + } + ] } - ... ] } ``` @@ -93,13 +80,16 @@ At least one property must be specified, that is, at least `global`, `consumers` ```javascript { - "prefilters" : [ + providers: [ { - "id" : "rate-limit", - "path" : "simple-rate-limit", - "config" : { - "providers" : { - "providerId" : { + "id": "idProvider", + "context": "/provider", + "target": "http://provider_server", + "prefilters" : [ + { + "id" : "rate-limiter", + "path" : "clydeio-simple-rate-limit", + "config" : { "global" : { "tokens" : 100, "interval" : "second" @@ -109,22 +99,19 @@ At least one property must be specified, that is, at least `global`, `consumers` "tokens" : 20, "interval" : "second" } - } - }, - ... - }, - ... - } + } + } + } + ] } - ... ] } ``` -## Notes: +## Notes * It has no sense configure the rate limiter filter as a global or provider's postfilter. -* Limits are applied in the order: global, consumers and providers. Be aware when chaining limits. +* Limits are applied in the order: global and consumers. Be aware when chaining limits. # License diff --git a/lib/index.js b/lib/index.js index a24aa84..e17820f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,53 +1,14 @@ "use strict"; +var InvalidRateLimiterConfiguration = require("./invalid-rate-limiter-configuration-error"); +var RateLimitExceeded = require("./rate-limit-exceeded-error"); var RateLimiter = require("limiter").RateLimiter; -var util = require("util"); - - -/** - * InvalidRateLimitConfiguration error triggered when passed invalid configurations. - * - * @param {String} description Extended description - * @returns {void} - */ -function InvalidRateLimitConfiguration(description) { - Error.captureStackTrace(this, this.constructor); - this.statusCode = 421; - this.name = this.constructor.name; - this.message = "Invalid rate limit configuration !!!"; - if (description) { - this.message += " " + description; - } -} -util.inherits(InvalidRateLimitConfiguration, Error); - - -/** - * RateLimitExceeded error triggered with a rate limit is exceeded. - * - * @param {String} description Extended description - * @returns {void} - */ -function RateLimitExceeded(description) { - Error.captureStackTrace(this, this.constructor); - this.statusCode = 421; - this.name = this.constructor.name; - this.message = "Too many requests !!!"; - if (description) { - this.message += " " + description; - } -} -util.inherits(RateLimitExceeded, Error); - -RateLimitExceeded.GLOBAL_LIMIT_EXCEEDED = "Global rate limit exceeded."; -RateLimitExceeded.PROVIDER_LIMIT_EXCEEDED = "Provider rate limit exceeded."; -RateLimitExceeded.CONSUMER_LIMIT_EXCEEDED = "Consumer rate limit exceeded."; -RateLimitExceeded.PROVIDER_CONSUMER_LIMIT_EXCEEDED = "Consumer quota on the provider exceeded."; /** * Validate the configuration parameters. * + * @private * @param {Object} config Configuration * @returns {void} */ @@ -75,41 +36,18 @@ function validateConfig(config) { return true; } - function isValidProviders(providersCfg) { - var prop, limitConfig; - if (providersCfg) { - for (prop in providersCfg) { - if ({}.hasOwnProperty.call(providersCfg, prop)) { - limitConfig = providersCfg[prop]; - - if (limitConfig.global && !isValidGlobal(limitConfig.global)) { - return false; - } - - if (limitConfig.consumers && !isValidConsumers(limitConfig.consumers)) { - return false; - } - } - } - } - return true; - } - if (!config.global && !config.consumers && !config.providers) { - throw new InvalidRateLimitConfiguration("At least one global, consumers or providers entry is required."); + throw new InvalidRateLimiterConfiguration("At least one global, consumers or providers entry is required."); } if (!isValidGlobal(config.global)) { - throw new InvalidRateLimitConfiguration("Invalid global section."); + throw new InvalidRateLimiterConfiguration("Invalid global section."); } if (!isValidConsumers(config.consumers)) { - throw new InvalidRateLimitConfiguration("Invalid consumers section."); + throw new InvalidRateLimiterConfiguration("Invalid consumers section."); } - if (!isValidProviders(config.providers)) { - throw new InvalidRateLimitConfiguration("Invalid providers section."); - } } /** @@ -143,32 +81,10 @@ function parseLimiters(config) { return limiters; } - function parseProviders(providersCfg) { - var prop, limitConfig, limiters = {}; - if (providersCfg) { - for (prop in providersCfg) { - if ({}.hasOwnProperty.call(providersCfg, prop)) { - limitConfig = providersCfg[prop]; - limiters[prop] = {}; - - if (limitConfig.global) { - limiters[prop].global = parseGlobal(limitConfig); - } - - if (limitConfig.consumers) { - limiters[prop].consumers = parseConsumers(limitConfig.consumers); - } - } - } - } - return limiters; - } - // Parse configuration return { global: parseGlobal(config), - consumers: parseConsumers(config.consumers), - providers: parseProviders(config.providers) + consumers: parseConsumers(config.consumers) }; } @@ -220,70 +136,9 @@ function applyConsumerLimit(consumersLimiters, consumerId, cb) { } -/** - * Apply limits on providers. - * - * @private - * @param {Object} providersLimiters Providers limiters - * @param {String} providerId Provider ID - * @param {Function} cb Callback - * @returns {void} - */ -function applyProviderLimit(providersLimiters, providerId, cb) { - if (!Object.keys(providersLimiters).length || !providerId) { - return cb(); - } - - var provider = providersLimiters[providerId]; - if (!provider || !provider.global) { - return cb(); - } - - if (!provider.global.tryRemoveTokens(1)) { - return cb(new RateLimitExceeded(RateLimitExceeded.PROVIDER_LIMIT_EXCEEDED)); - } - - return cb(); -} - - -/** - * Apply limits on a provider's consumer. - * - * @private - * @param {Object} providersLimiters Providers limiters - * @param {String} providerId Provider ID - * @param {String} consumerId Consumer ID - * @param {Function} cb Callback - * @returns {void} - */ -function applyProviderConsumerLimit(providersLimiters, providerId, consumerId, cb) { - if (!Object.keys(providersLimiters).length || !providerId || !consumerId) { - return cb(); - } - - var provider = providersLimiters[providerId]; - if (!provider || !provider.consumers) { - return cb(); - } - - var limiter = provider.consumers[consumerId]; - if (!limiter) { - return cb(); - } - - if (!limiter.tryRemoveTokens(1)) { - return cb(new RateLimitExceeded(RateLimitExceeded.PROVIDER_CONSUMER_LIMIT_EXCEEDED)); - } - - return cb(); -} - - /** * Simple rate limit implementation. - * Limits can be applied globally, on a concrete provider, on a concrete - * consumer or on a concrete provider's consumer. + * Limits can be applied globally or per consumer. * * @public * @param {String} name Name of the filter @@ -301,10 +156,7 @@ module.exports.init = function(name, config) { // Return middleware function that applies rates limits return function(req, res, next) { - var consumerId, providerId; - if (req.provider && req.provider.providerId) { - providerId = req.provider.providerId; - } + var consumerId; if (req.user && req.user.userId) { consumerId = req.user.userId; } @@ -326,20 +178,9 @@ module.exports.init = function(name, config) { return next(errConsumer); } - applyProviderLimit(limiters.providers, providerId, function(errProvider) { - if (errProvider) { - return next(errProvider); - } - - applyProviderConsumerLimit(limiters.providers, providerId, consumerId, function(err) { - if (err) { - return next(err); - } - - return next(); - }); - }); + return next(); }); + }); }; diff --git a/lib/invalid-rate-limiter-configuration-error.js b/lib/invalid-rate-limiter-configuration-error.js new file mode 100644 index 0000000..dff8050 --- /dev/null +++ b/lib/invalid-rate-limiter-configuration-error.js @@ -0,0 +1,22 @@ +"use strict"; + +var util = require("util"); + +/** + * InvalidRateLimitConfiguration error triggered when passed invalid configurations. + * + * @param {String} description Extended description + * @returns {void} + */ +function InvalidRateLimiterConfiguration(description) { + Error.captureStackTrace(this, this.constructor); + this.statusCode = 421; + this.name = this.constructor.name; + this.message = "Invalid rate limit configuration !!!"; + if (description) { + this.message += " " + description; + } +} +util.inherits(InvalidRateLimiterConfiguration, Error); + +module.exports = InvalidRateLimiterConfiguration; diff --git a/lib/rate-limit-exceeded-error.js b/lib/rate-limit-exceeded-error.js new file mode 100644 index 0000000..d43872a --- /dev/null +++ b/lib/rate-limit-exceeded-error.js @@ -0,0 +1,27 @@ +"use strict"; + +var util = require("util"); + +/** + * RateLimitExceeded error triggered with a rate limit is exceeded. + * + * @param {String} description Extended description + * @returns {void} + */ +function RateLimitExceeded(description) { + Error.captureStackTrace(this, this.constructor); + this.statusCode = 421; + this.name = this.constructor.name; + this.message = "Too many requests !!!"; + if (description) { + this.message += " " + description; + } +} +util.inherits(RateLimitExceeded, Error); + +RateLimitExceeded.GLOBAL_LIMIT_EXCEEDED = "Global rate limit exceeded."; +RateLimitExceeded.PROVIDER_LIMIT_EXCEEDED = "Provider rate limit exceeded."; +RateLimitExceeded.CONSUMER_LIMIT_EXCEEDED = "Consumer rate limit exceeded."; +RateLimitExceeded.PROVIDER_CONSUMER_LIMIT_EXCEEDED = "Consumer quota on the provider exceeded."; + +module.exports = RateLimitExceeded; diff --git a/test/simple-rate-limiter.test.js b/test/simple-rate-limiter.test.js index 9d90994..7fd45ef 100644 --- a/test/simple-rate-limiter.test.js +++ b/test/simple-rate-limiter.test.js @@ -91,7 +91,7 @@ describe("simple-rate-limiter", function() { } }, { - id: "XXX-simple-rate-limiter", + id: "simple-rate-limiter", path: path.join(__dirname, "../lib/index.js"), config: { consumers: { @@ -152,16 +152,12 @@ describe("simple-rate-limiter", function() { prefilters: [ { - id: "AAA-simple-rate-limiter", + id: "simple-rate-limiter", path: path.join(__dirname, "../lib/index.js"), config: { - providers: { - providerA: { - global: { - tokens: 1, - interval: "second" - } - } + global: { + tokens: 1, + interval: "second" } } } @@ -224,14 +220,10 @@ describe("simple-rate-limiter", function() { id: "simple-rate-limit", path: path.join(__dirname, "../lib/index.js"), config: { - providers: { - providerB: { - consumers: { - userB: { - tokens: 1, - interval: "second" - } - } + consumers: { + userB: { + tokens: 1, + interval: "second" } } }