-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Route options #34
base: master
Are you sure you want to change the base?
Route options #34
Changes from 2 commits
71c5823
645dfcf
06aca4c
13a378a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW I don't think limitd can be missing or falsey at this stage so this can probably be removed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dschenkelman do you want me to also remove this check ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, this is fine |
||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks |
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe this is a premature optimization? I think hapi merges per request. Storing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea sure. It just feels a bit wasteful since it can be deduced in advance. |
||
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(); | ||
}); | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking a bit more and we can probably change this to
allRoutes
as you suggested.if it's set to
true
a user can still disable a specific route by setting the route settings tofalse
instead of an objectSo we can accommodate both use cases this way.