Skip to content
This repository has been archived by the owner on Jan 18, 2018. It is now read-only.

Commit

Permalink
Initial implementation of hapi-limiter
Browse files Browse the repository at this point in the history
  • Loading branch information
cadecairos committed May 27, 2015
1 parent 5e904fb commit 17d2a73
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
node_js:
- '0.12'
- iojs
sudo: false
cache:
directories:
- node_modules
after_script:
- npm run test:reportcoverage
117 changes: 117 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
var Hoek = require('hoek');
var Boom = require('boom');
var hapiLimiter = 'hapi-limiter';

var internals = {
defaults: {
cache: {
expiresIn: 1000 * 60 * 15,
segment: hapiLimiter
},
limit: 15,
ttl: 1000 * 60 * 15,
generateKeyFunc: function(request) {
var methodAndPath = request.method + ':' + request.path + ':';
var ip = request.headers['x-forwarded-for'];

if ( !ip ) {
ip = request.info.remoteAddress;
}

return methodAndPath + ip;
}
}
};


exports.register = function(server, options, done) {
var globalSettings = Hoek.applyToDefaults(internals.defaults, options);

var cacheClient = globalSettings.cacheClient;

if ( !cacheClient ) {
cacheClient = server.cache(globalSettings.cache);
}

server.ext('onPreHandler', function(request, reply) {
var routePlugins = request.route.settings.plugins;

if (
!routePlugins[hapiLimiter] ||
!routePlugins[hapiLimiter].enable
) {
return reply.continue();
}

var pluginSettings = Hoek.applyToDefaults(globalSettings, routePlugins[hapiLimiter]);

var keyValue = pluginSettings.generateKeyFunc(request);

cacheClient.get(keyValue, function(err, value, cached) {
if ( err ) {
return reply(err);
}
request.plugins[hapiLimiter] = {};
request.plugins[hapiLimiter].limit = pluginSettings.limit;

if ( !cached ) {
var reset = Date.now() + pluginSettings.ttl;
return cacheClient.set(keyValue, { remaining: pluginSettings.limit - 1 }, pluginSettings.ttl, function(err) {
if ( err ) {
return reply(err);
}
request.plugins[hapiLimiter].remaining = pluginSettings.limit - 1;
request.plugins[hapiLimiter].reset = reset;
reply.continue();
});
}

request.plugins[hapiLimiter].remaining = value.remaining - 1;
request.plugins[hapiLimiter].reset = Date.now() + cached.ttl;

var error;
if ( request.plugins[hapiLimiter].remaining < 0 ) {
error = Boom.tooManyRequests('Rate Limit Exceeded');
error.output.headers['X-Rate-Limit-Limit'] = request.plugins[hapiLimiter].limit;
error.output.headers['X-Rate-Limit-Reset'] = request.plugins[hapiLimiter].reset;
error.output.headers['X-Rate-Limit-Remaining'] = 0;
error.reformat();
return reply(error);
}

cacheClient.set(
keyValue,
{ remaining: request.plugins[hapiLimiter].remaining },
cached.ttl, function(err) {
if ( err ) {
return reply(err);
}

reply.continue();
});
});
});

server.ext('onPostHandler', function(request, reply) {
var pluginSettings = request.route.settings.plugins;
var response;

if (
pluginSettings[hapiLimiter] &&
pluginSettings[hapiLimiter].enable
) {
response = request.response;
response.headers['X-Rate-Limit-Limit'] = request.plugins[hapiLimiter].limit;
response.headers['X-Rate-Limit-Remaining'] = request.plugins[hapiLimiter].remaining;
response.headers['X-Rate-Limit-Reset'] = request.plugins[hapiLimiter].reset;
}

reply.continue();
});

done();
};

exports.register.attributes = {
pkg: require('./package.json')
};
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "hapi-limiter",
"version": "0.0.0",
"description": "Rate limiting plugin for Hapi",
"main": "index.js",
"scripts": {
"test": "lab -t 100 -c --verbose --colors --assert code --timeout 5000 && npm run lint",
"test:reportcoverage": "lab -r lcov | ./node_modules/.bin/coveralls",
"lint": "npm run jshint && npm run jscs",
"jshint": "jshint -c node_modules/mofo-style/linters/.jshintrc test index.js",
"jscs": "jscs -c node_modules/mofo-style/linters/.jscsrc test index.js"
},
"author": "Christopher De Cairos",
"license": "MPL 2.0",
"repository": {
"type": "git",
"url": "https://github.com/cadecairos/hapi-limiter"
},
"dependencies": {
"boom": "^2.7.1",
"hoek": "^2.11.0"
},
"devDependencies": {
"async": "^1.0.0",
"catbox-memory": "^1.1.1",
"code": "^1.3.0",
"coveralls": "^2.11.2",
"hapi": "^8.4.0",
"jscs": "^1.11.3",
"jshint": "^2.6.3",
"lab": "^5.5.0",
"mofo-style": "latest",
"sinon": "^1.14.1"
},
"engines": {
"npm": "^2.5.1",
"node": "^0.12.1"
}
}
10 changes: 10 additions & 0 deletions test/configs/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
exports.offByDefault = {};
exports.defaults = {};
exports.customSettings = {
ttl: 5000,
limit: 5,
cache: {
cache: 'test-cache',
segment: 'custom-segment'
}
};
36 changes: 36 additions & 0 deletions test/configs/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
var Hoek = require('hoek');

var base = {
path: '/limited',
method: 'get',
handler: function(request, reply) {
reply();
}
};

exports.offByDefault = base;

exports.defaults = Hoek.applyToDefaults(base, {
config: {
plugins: {
'hapi-limiter': {
enable: true
}
}
}
});

exports.overrides = Hoek.applyToDefaults(base, {
config: {
plugins: {
'hapi-limiter': {
enable: true,
limit: 5,
ttl: 8000,
generateKeyFunc: function(request) {
return 'customkey';
}
}
}
}
});
Loading

0 comments on commit 17d2a73

Please sign in to comment.