From 71c58233fd52004ef5b3ad715403d04d25fff8db Mon Sep 17 00:00:00 2001 From: Yoni Jah Date: Tue, 15 Aug 2017 09:56:07 +0800 Subject: [PATCH 1/4] Upgrade mocha and limitd dev dependencies, Fix test to properly display errors and to work with limitd 5.x --- package.json | 4 +- test/index.js | 75 ++++++++++++++++------------------ test/limitdConfig/settings.yml | 2 + test/limitdServer.js | 6 ++- 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index e4dcb6d..0172ca5 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "chai": "^2.1.0", "hapi": "^15.2.0", "js-yaml": "^3.2.7", - "limitd": "^4.20.1", - "mocha": "^2.1.0", + "limitd": "^5.3.1", + "mocha": "^3.5.0", "request": "^2.81.0", "rimraf": "^2.3.1", "xtend": "^4.0.0" diff --git a/test/index.js b/test/index.js index 9d79bc9..37bef59 100644 --- a/test/index.js +++ b/test/index.js @@ -16,6 +16,18 @@ function getLimitdClient(address) { return new LimitdClient({ hosts: [ address ] }); } +function PromInject (req, cb) { + return new Promise((resolve, reject) => { + server.inject(req, (res) => { + try { + resolve(cb(res)); + } catch(e) { + reject(e); + } + }) + }) +} + describe('options validation', () => { it ('should fail if event is not specified', () => { plugin.register(null, { @@ -144,10 +156,10 @@ describe('with server', () => { after(server.stop); - it ('should send response with error', done => { + it ('should send response with error', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); expect(res.statusCode).to.equal(500); @@ -155,7 +167,6 @@ describe('with server', () => { expect(body.error).to.equal('Internal Server Error'); expect(body.message).to.equal('An internal server error occurred'); - done(); }); }); }); @@ -173,13 +184,12 @@ describe('with server', () => { }); after(server.stop); - it('should return 200', done => { + it('should return 200', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { expect(res.statusCode).to.equal(200); expect(res.payload).to.equal('created'); - done(); }); }); }); @@ -198,9 +208,9 @@ describe('with server', () => { }); after(server.stop); - it('should return what onError returns', done => { + it('should return what onError returns', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); expect(res.statusCode).to.equal(500); @@ -208,7 +218,6 @@ describe('with server', () => { expect(body.error).to.equal('Internal Server Error'); expect(body.message).to.equal('An internal server error occurred'); - done(); }); }); }); @@ -228,10 +237,10 @@ describe('with server', () => { }); after(server.stop); - it ('should send response with error', done => { + it ('should send response with error', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); expect(res.statusCode).to.equal(500); @@ -239,7 +248,6 @@ describe('with server', () => { expect(body.error).to.equal('Internal Server Error'); expect(body.message).to.equal('An internal server error occurred'); - done(); }); }); }); @@ -257,10 +265,10 @@ describe('with server', () => { }); after(server.stop); - it ('should send response with error', done => { + it ('should send response with error', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); expect(res.statusCode).to.equal(500); @@ -268,7 +276,6 @@ describe('with server', () => { expect(body.error).to.equal('Internal Server Error'); expect(body.message).to.equal('An internal server error occurred'); - done(); }); }); }); @@ -324,20 +331,17 @@ function itBehavesLikeWhenLimitdIsRunning(options) { after(server.stop); - it('should send response with 429 and headers', done => { + it('should send response with 429 and headers', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); const headers = res.headers; - expect(body.statusCode).to.equal(429); expect(body.error).to.equal('Too Many Requests'); expect(headers['x-ratelimit-limit']).to.equal(0); expect(headers['x-ratelimit-remaining']).to.equal(0); expect(headers['x-ratelimit-reset']).to.equal(0); - - done(); }); }); }); @@ -355,9 +359,9 @@ function itBehavesLikeWhenLimitdIsRunning(options) { after(server.stop); - it('should send response with 429 and headers', done => { + it('should send response with 429 and headers', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); const headers = res.headers; @@ -368,7 +372,6 @@ function itBehavesLikeWhenLimitdIsRunning(options) { expect(headers['x-ratelimit-remaining']).to.equal(0); expect(headers['x-ratelimit-reset']).to.equal(0); - done(); }); }); }); @@ -387,13 +390,12 @@ function itBehavesLikeWhenLimitdIsRunning(options) { after(server.stop); - it('should send response with 200', done => { + it('should send response with 200', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { expect(res.statusCode).to.equal(200); expect(res.payload).to.equal('created'); - done(); }); }); }); @@ -412,10 +414,10 @@ function itBehavesLikeWhenLimitdIsRunning(options) { after(server.stop); - it('should send response with 200 if limit is not passed and set limit header', function(done){ + it('should send response with 200 if limit is not passed and set limit header', () =>{ const request = { method: 'POST', url: '/users', payload: { } }; const startDate = Math.floor((new Date()).getTime() / 1000); - server.inject(request, res => { + return PromInject(request, res => { expect(res.statusCode).to.equal(200); expect(res.payload).to.equal('created'); @@ -424,7 +426,6 @@ function itBehavesLikeWhenLimitdIsRunning(options) { expect(headers['x-ratelimit-remaining']).to.equal(999999); expect(headers['x-ratelimit-reset']).to.be.greaterThan(startDate); - done(); }); }); }); @@ -442,10 +443,10 @@ function itBehavesLikeWhenLimitdIsRunning(options) { after(server.stop); - it('should send response with 403 if limit is not passed and set limit header', function(done){ + it('should send response with 403 if limit is not passed and set limit header', () => { const request = { method: 'POST', url: '/users', payload: { } }; const startDate = Math.floor((new Date()).getTime() / 1000); - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); const headers = res.headers; @@ -456,7 +457,6 @@ function itBehavesLikeWhenLimitdIsRunning(options) { expect(headers['x-ratelimit-remaining']).to.equal(999999); expect(headers['x-ratelimit-reset']).to.be.greaterThan(startDate); - done(); }); }); }); @@ -484,10 +484,10 @@ function itBehavesLikeWhenLimitdIsRunning(options) { after(server.stop); - it('should send response with 200 if limit is not passed and set limit header to the lowest remaining limit', function(done){ + it('should send response with 200 if limit is not passed and set limit header to the lowest remaining limit', () => { const request = { method: 'POST', url: '/users', payload: { } }; const startDate = Math.floor((new Date()).getTime() / 1000); - server.inject(request, res => { + return PromInject(request, res => { expect(res.statusCode).to.equal(200); expect(res.payload).to.equal('created'); @@ -495,8 +495,6 @@ function itBehavesLikeWhenLimitdIsRunning(options) { expect(headers['x-ratelimit-limit']).to.equal(3); expect(headers['x-ratelimit-remaining']).to.equal(2); expect(headers['x-ratelimit-reset']).to.be.greaterThan(startDate); - - done(); }); }); }); @@ -527,9 +525,9 @@ function itBehavesLikeWhenLimitdIsRunning(options) { after(server.stop); - it('should send response with 429 if limit has passed for some plugin configuration and set limit header', function(done){ + it('should send response with 429 if limit has passed for some plugin configuration and set limit header', () => { const request = { method: 'POST', url: '/users', payload: { } }; - server.inject(request, res => { + return PromInject(request, res => { const body = JSON.parse(res.payload); const headers = res.headers; @@ -540,7 +538,6 @@ function itBehavesLikeWhenLimitdIsRunning(options) { expect(headers['x-ratelimit-remaining']).to.equal(0); expect(headers['x-ratelimit-reset']).to.equal(0); - done(); }); }); }); diff --git a/test/limitdConfig/settings.yml b/test/limitdConfig/settings.yml index d778cc7..d802cc7 100644 --- a/test/limitdConfig/settings.yml +++ b/test/limitdConfig/settings.yml @@ -1,3 +1,5 @@ +#port to listen on +port: 9001 buckets: users: diff --git a/test/limitdServer.js b/test/limitdServer.js index 7cf16db..9062349 100644 --- a/test/limitdServer.js +++ b/test/limitdServer.js @@ -13,7 +13,7 @@ function create(port) { port = port || 9001; let server; - + return { start: function(cb) { @@ -23,7 +23,9 @@ function create(port) { rimraf.sync(db_file); } catch(err){} - server = new LimitdServer(xtend({db: db_file, port: port}, require('./limitdConfig'))); + const conf = require('./limitdConfig'); + conf.db = db_file; + server = new LimitdServer(xtend({db: db_file, port: port}, conf)); server.start(function (err, address) { if (err) { return cb(err); } From 645dfcfcc32cba9be7e8a6389fcb8bbaed7b3b96 Mon Sep 17 00:00:00 2001 From: Yoni Jah Date: Tue, 15 Aug 2017 12:32:32 +0800 Subject: [PATCH 2/4] Add settings (enabled, sendResponseHeaders), Add feature to allow overrid route settings --- README.md | 5 +- lib/index.js | 97 +++++++++++++--------- test/index.js | 216 ++++++++++++++++++++++++++++++++++++++++++++++++- test/server.js | 100 +++++++++++++++++++---- 4 files changed, 361 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 9fc78e6..f0c8a2f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ server.register({ event: 'onPostAuth', type: 'users', limitd: limitdClient, - extractKey: function(request, reply, done){ + extractKey: function(request, reply, done) { var key = request.auth.credentials.userId; done(null, key); } @@ -44,11 +44,14 @@ The object has the following schema (validated [here](./lib/index.js) using [Joi * `done: (err: Error, key: String)` - A function that takes an error as the first parameter and the bucket key as the second parameter. **Optional** +* `enabled: Boolean default(true)` - if true rate limiting will be enabled for all routes. Setting this `false` will only rate limit routes which have the plugin defined +* `sendResponseHeaders: Boolean default(true)` - if `true` rate limiting headers will be sent with response * `onError: (error, reply) => ()` - A function that takes the `error` that occurred when trying to get a token from the bucket and the `reply` interface. * `error: Error` - The error that occurred. * `reply: Reply` - The hapi.js [reply interface](http://hapijs.com/api#reply-interface). > If an error occurs and no function is provided, the request lifecycle continues normally as if there was no token bucket restriction. This is a useful default behavior in case the limitd server goes down. + ## Contributing Feel free to open issues with questions/bugs/features. PRs are also welcome. diff --git a/lib/index.js b/lib/index.js index 78c28fe..d251a06 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,7 +9,9 @@ const schema = Joi.object().keys({ type: [Joi.string(), Joi.func()], limitd: Joi.object(), onError: Joi.func(), - extractKey: Joi.func() + extractKey: Joi.func(), + enabled: Joi.boolean().default(true), + sendResponseHeaders: Joi.boolean().default(true) }).requiredKeys('type', 'event', 'limitd', 'extractKey'); function setResponseHeader(request, header, value) { @@ -22,41 +24,24 @@ function setResponseHeader(request, header, value) { } } -function setupPreResponseExt(server, options) { - server.ext('onPreResponse', (request, reply) => { - const requestLimit = request.plugins.patova && request.plugins.patova.limit; - - if (requestLimit && requestLimit.conformant){ - const headers = new RateLimitHeaders( - requestLimit.limit, - requestLimit.remaining, - requestLimit.reset); - - Object.keys(headers).forEach( - key => setResponseHeader(request, key, headers[key])); - } - reply.continue(); - }); -} - function getMinimumLimit(limit1, limit2) { if (!limit1) { return limit2; } if (!limit2) { return limit1; } - if (limit1 && limit2.remaining > limit1.remaining) { + if (limit2.remaining > limit1.remaining) { return limit1; } return limit2; } -function setupRateLimitEventExt(server, options) { - const event = options.event; - const extractKey = options.extractKey; - const onError = options.onError; +function setupRateLimitEventExt(server, pluginOptions) { + const event = pluginOptions.event; + const onError = pluginOptions.onError; - const extractKeyAndTakeToken = function(limitd, request, reply, type) { - extractKey(request, reply, (err, key) =>{ + const extractKeyAndTakeToken = function(options, request, reply, type) { + options.extractKey(request, reply, (err, key) =>{ + const limitd = options.limitd; if (err) { return reply(err); } if (!limitd) { @@ -65,14 +50,14 @@ function setupRateLimitEventExt(server, options) { } limitd.take(type, key, (err, currentLimitResponse) => { - if (err){ + if (err) { if (onError) { return onError(err, reply); } // by default we don't fail if limitd is unavailable return reply.continue(); } - const oldMinimumLimitResponse = request.plugins.patova && request.plugins.patova.limit - const newMinimumLimitResponse = getMinimumLimit(currentLimitResponse, oldMinimumLimitResponse) + const oldMinimumLimitResponse = request.plugins.patova && request.plugins.patova.limit; + const newMinimumLimitResponse = getMinimumLimit(currentLimitResponse, oldMinimumLimitResponse); request.plugins.patova = request.plugins.patova || {}; request.plugins.patova.limit = newMinimumLimitResponse; @@ -83,17 +68,19 @@ function setupRateLimitEventExt(server, options) { } const error = Boom.tooManyRequests(); - error.output.headers = new RateLimitHeaders( - newMinimumLimitResponse.limit, - newMinimumLimitResponse.remaining, - newMinimumLimitResponse.reset); + if (options.sendResponseHeaders) { + error.output.headers = new RateLimitHeaders( + newMinimumLimitResponse.limit, + newMinimumLimitResponse.remaining, + newMinimumLimitResponse.reset); + } reply(error); }); }); }; - const getType = function(request, reply, callback) { + const getType = function(options, request, reply, callback) { const type = options.type; if (typeof type !== 'function') { @@ -114,18 +101,52 @@ function setupRateLimitEventExt(server, options) { }; server.ext(event, (request, reply) => { + const options = getRouteOptions(request); + if (!options.enabled) { + return reply.continue(); + } // This handler is going to be called one time per registration of patova - getType(request, reply, (err, type) => { - extractKeyAndTakeToken(options.limitd, request, reply, type); + getType(options, request, reply, (err, type) => { + extractKeyAndTakeToken(options, request, reply, type); }); }); + + server.ext('onPreResponse', (request, reply) => { + const requestLimit = request.plugins.patova && request.plugins.patova.limit; + + if (requestLimit && requestLimit.conformant) { + const options = getRouteOptions(request); + if (options.sendResponseHeaders) { + const headers = new RateLimitHeaders( + requestLimit.limit, + requestLimit.remaining, + requestLimit.reset); + + Object.keys(headers).forEach( + key => setResponseHeader(request, key, headers[key])); + } + } + reply.continue(); + }); + + function getRouteOptions(request) { + const routeOptions = request.route.settings.plugins.patova; + if (routeOptions === undefined) { + return pluginOptions; + } + if (routeOptions._merged) { //already merged with previous pluginOptions; + return routeOptions; + } + request.route.settings.plugins.patova = Object.assign({_merged: true}, pluginOptions, {enabled: true}, routeOptions); + return request.route.settings.plugins.patova; + } + } -exports.register = function (server, options, next) { - Joi.validate(options, schema, { abortEarly: false }, (err, processedOptions) => { +exports.register = function(server, pluginOptions, next) { + Joi.validate(pluginOptions, schema, { abortEarly: false }, (err, processedOptions) => { if (err) { return next(err); } setupRateLimitEventExt(server, processedOptions); - setupPreResponseExt(server, processedOptions); next(); }); }; diff --git a/test/index.js b/test/index.js index 37bef59..a078618 100644 --- a/test/index.js +++ b/test/index.js @@ -171,7 +171,7 @@ describe('with server', () => { }); }); - describe ('when limitd does not provide a response and there is no onError',function(){ + describe ('when limitd does not provide a response and there is no onError', () => { before(done => { server.start({ replyError: false }, { type: 'user', @@ -400,8 +400,220 @@ function itBehavesLikeWhenLimitdIsRunning(options) { }); }); + describe('when plugin is disabled by default', () => { + before(done => { + server.start({ replyError: false }, { + type: options.emptyType, + limitd: getLimitdClient(address), + extractKey: (request, reply, done) => { done(null, 'notImportant'); }, + event: 'onPostAuth', + onError: (err, reply) => { reply(Boom.wrap(err, 500)); }, + enabled: false + }, done); + }); + + after(server.stop); + + it('should send response with 200 for default routes', () => { + const request = { method: 'POST', url: '/users', payload: { } }; + return PromInject(request, res => { + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('created'); + + }); + }); + + it('should send response with 200 if route explicitly disables patova', () => { + const request = { method: 'POST', url: '/no_limit', payload: { } }; + return PromInject(request, res => { + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('created'); + + }); + }); + + it('should send response with 429 if route has patova settings', () => { + const request = { method: 'POST', url: '/empty', payload: { } }; + return PromInject(request, res => { + const body = JSON.parse(res.payload); + const headers = res.headers; + + expect(res.statusCode).to.equal(429); + expect(body.error).to.equal('Too Many Requests'); + + expect(headers['x-ratelimit-limit']).to.equal(0); + expect(headers['x-ratelimit-remaining']).to.equal(0); + expect(headers['x-ratelimit-reset']).to.equal(0); + + }); + }); + + it('should send response with 429 if route explicitly enables patova', () => { + const request = { method: 'POST', url: '/always_limit', payload: { } }; + return PromInject(request, res => { + const body = JSON.parse(res.payload); + const headers = res.headers; + + expect(res.statusCode).to.equal(429); + expect(body.error).to.equal('Too Many Requests'); + + expect(headers['x-ratelimit-limit']).to.equal(0); + expect(headers['x-ratelimit-remaining']).to.equal(0); + expect(headers['x-ratelimit-reset']).to.equal(0); + + }); + }); + }); + + describe('when routes overrides default type settings', () => { + before(done => { + server.start({ replyError: false }, { + type: options.usersType, + limitd: getLimitdClient(address), + extractKey: (request, reply, done) => { done(null, 'key'); }, + event: 'onPostAuth', + onError: (err, reply) => { reply(Boom.wrap(err, 500)); } + }, done); + }); + + after(server.stop); + + it('should use route type', () => { + const request = { method: 'POST', url: '/empty', payload: { } }; + return PromInject(request, res => { + const body = JSON.parse(res.payload); + const headers = res.headers; + + expect(res.statusCode).to.equal(429); + expect(body.error).to.equal('Too Many Requests'); + + expect(headers['x-ratelimit-limit']).to.equal(0); + expect(headers['x-ratelimit-remaining']).to.equal(0); + expect(headers['x-ratelimit-reset']).to.equal(0); + + }); + }); + }); + + describe('when routes overrides default enable settings', () => { + + before(done => { + server.start({ replyError: false }, { + type: options.emptyType, + limitd: getLimitdClient(address), + extractKey: (request, reply, done) => { done(null, 'key'); }, + event: 'onPostAuth', + onError: (err, reply) => { reply(Boom.wrap(err, 500)); } + }, done); + }); + + after(server.stop); + + it('should not be enabled for disabled routes', () => { + const request = { method: 'POST', url: '/no_limit', payload: { } }; + return PromInject(request, res => { + expect(res.statusCode).to.equal(200); + expect(res.payload).to.equal('created'); + + }); + }); + }); + + describe('when routes headers are disabled', () => { + + before(done => { + server.start({ replyError: false }, { + type: options.usersType, + limitd: getLimitdClient(address), + extractKey: (request, reply, done) => { done(null, 'key'); }, + event: 'onPostAuth', + onError: (err, reply) => { reply(Boom.wrap(err, 500)); }, + sendResponseHeaders: false + }, done); + }); + + after(server.stop); + + it('should not send headers', () => { + const request = { method: 'POST', url: '/empty', payload: { } }; + return PromInject(request, res => { + const headers = res.headers; + + expect(headers).to.not.have.property('x-ratelimit-limit'); + expect(headers).to.not.have.property('x-ratelimit-remaining'); + expect(headers).to.not.have.property('x-ratelimit-reset'); + }); + }); + + it('should send headers when route explicitly enables them', () => { + const request = { method: 'POST', url: '/always_headers', payload: { } }; + return PromInject(request, res => { + const headers = res.headers; + + expect(headers).to.have.property('x-ratelimit-limit'); + expect(headers).to.have.property('x-ratelimit-remaining'); + expect(headers).to.have.property('x-ratelimit-reset'); + }); + }); + + it('should not send headers when route explicitly disables them', () => { + const request = { method: 'POST', url: '/no_headers', payload: { } }; + return PromInject(request, res => { + const headers = res.headers; + + expect(headers).to.not.have.property('x-ratelimit-limit'); + expect(headers).to.not.have.property('x-ratelimit-remaining'); + expect(headers).to.not.have.property('x-ratelimit-reset'); + }); + }); + }); + + describe('when caching route settings', () => { + + before(done => { + server.start({ replyError: false }, { + type: options.usersType, + limitd: getLimitdClient(address), + extractKey: (request, reply, done) => { done(null, 'key'); }, + event: 'onPostAuth', + onError: (err, reply) => { reply(Boom.wrap(err, 500)); } + }, done); + }); + + after(server.stop); + + it('should respect route settings on multiple calls', () => { + const request = { method: 'POST', url: '/empty', payload: { } }; + return PromInject(request, res => { + const body = JSON.parse(res.payload); + const headers = res.headers; + + expect(res.statusCode).to.equal(429); + expect(body.error).to.equal('Too Many Requests'); + + expect(headers['x-ratelimit-limit']).to.equal(0); + expect(headers['x-ratelimit-remaining']).to.equal(0); + expect(headers['x-ratelimit-reset']).to.equal(0); + }).then(() => { + const request = { method: 'POST', url: '/empty', payload: { } }; + return PromInject(request, res => { + const body = JSON.parse(res.payload); + const headers = res.headers; + + expect(res.statusCode).to.equal(429); + expect(body.error).to.equal('Too Many Requests'); + + expect(headers['x-ratelimit-limit']).to.equal(0); + expect(headers['x-ratelimit-remaining']).to.equal(0); + expect(headers['x-ratelimit-reset']).to.equal(0); + }); + }); + }); + }); + describe('when limitd responds conformant', () => { describe('and request response is normal', () => { + before((done) => { server.start({ replyError: false }, { type: options.usersType, @@ -431,6 +643,7 @@ function itBehavesLikeWhenLimitdIsRunning(options) { }); describe('and request response is an error', () => { + before((done) => { server.start({ replyError: true }, { type: options.usersType, @@ -500,6 +713,7 @@ function itBehavesLikeWhenLimitdIsRunning(options) { }); describe('when limitd responds not conformant', () => { + before((done) => { server.start({ replyError: false }, [ { diff --git a/test/server.js b/test/server.js index 5ec5b5e..faee439 100644 --- a/test/server.js +++ b/test/server.js @@ -1,37 +1,103 @@ -var Hapi = require('hapi'); -var plugin = require('../'); -var Boom = require('boom'); +'use strict'; +const Hapi = require('hapi'); +const plugin = require('../'); +const Boom = require('boom'); -var server; +let server; -exports.start = function(replyOptions, pluginOptions, done){ +exports.start = function(replyOptions, pluginOptions, done) { server = new Hapi.Server(); server.connection({ host: 'localhost', - port: 3001, + port: 3001 }); + function handler(request, reply) { + if (replyOptions.replyError) { + reply(Boom.forbidden('You cannot access Zion')); + } else { + reply('created'); + } + } + server.route({ method: 'POST', path:'/users', - handler: function (request, reply) { - if (replyOptions.replyError) { - reply(Boom.forbidden('You cannot access Zion')); - } else { - reply('created'); - } - } + handler }); server.route({ method: 'GET', path:'/forever', - handler: function (request, reply) { + handler: function(request, reply) { setTimeout(() => reply('created'), 1000); } }); + server.route({ + method: 'POST', + path:'/empty', + config: { + plugins: { + patova: { + type: 'empty' + } + } + }, + handler + }); + + server.route({ + method: 'POST', + path:'/no_limit', + config: { + plugins: { + patova: { + enabled: false + } + } + }, + handler + }); + + server.route({ + method: 'POST', + path:'/always_limit', + config: { + plugins: { + patova: {} + } + }, + handler + }); + + server.route({ + method: 'POST', + path:'/no_headers', + config: { + plugins: { + patova: { + sendResponseHeaders: false + } + } + }, + handler + }); + + server.route({ + method: 'POST', + path:'/always_headers', + config: { + plugins: { + patova: { + sendResponseHeaders: true + } + } + }, + handler + }); + const allPluginOptions = Array.isArray(pluginOptions) ? pluginOptions : [ pluginOptions ]; const plugins = allPluginOptions.map(pluginOptions => this.desc(pluginOptions)); @@ -46,14 +112,14 @@ exports.start = function(replyOptions, pluginOptions, done){ exports.desc = function(pluginOptions) { return { register: plugin, - options: pluginOptions, + options: pluginOptions }; }; -exports.inject = function(){ +exports.inject = function() { server.inject.apply(server, Array.prototype.slice.call(arguments, 0)); }; -exports.stop = function(done){ +exports.stop = function(done) { server.stop(done); }; From 06aca4c262dc0e24970fdd8936a8e7fe9e4e82ec Mon Sep 17 00:00:00 2001 From: Yoni Jah Date: Thu, 24 Aug 2017 09:34:03 +0800 Subject: [PATCH 3/4] Remove caching of route settings --- lib/index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index d251a06..300e41b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -134,11 +134,7 @@ function setupRateLimitEventExt(server, pluginOptions) { if (routeOptions === undefined) { return pluginOptions; } - if (routeOptions._merged) { //already merged with previous pluginOptions; - return routeOptions; - } - request.route.settings.plugins.patova = Object.assign({_merged: true}, pluginOptions, {enabled: true}, routeOptions); - return request.route.settings.plugins.patova; + return Object.assign({}, pluginOptions, {enabled: true}, routeOptions); } } From 13a378a2b12f5e3b941b6131ddec725151a3dd17 Mon Sep 17 00:00:00 2001 From: Yoni Jah Date: Thu, 24 Aug 2017 09:35:25 +0800 Subject: [PATCH 4/4] Fix onError setting not being checked for each route settings --- lib/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 300e41b..7e826ac 100644 --- a/lib/index.js +++ b/lib/index.js @@ -37,7 +37,6 @@ function getMinimumLimit(limit1, limit2) { function setupRateLimitEventExt(server, pluginOptions) { const event = pluginOptions.event; - const onError = pluginOptions.onError; const extractKeyAndTakeToken = function(options, request, reply, type) { options.extractKey(request, reply, (err, key) =>{ @@ -51,7 +50,7 @@ function setupRateLimitEventExt(server, pluginOptions) { limitd.take(type, key, (err, currentLimitResponse) => { if (err) { - if (onError) { return onError(err, reply); } + if (options.onError) { return options.onError(err, reply); } // by default we don't fail if limitd is unavailable return reply.continue(); }