Comparing version 0.2.10 to 0.2.11
@@ -81,3 +81,3 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
!(this.url.pathname === '/' && | ||
options.url[options.url.length-1] !== '/')) { | ||
options.url[options.url.length - 1] !== '/')) { | ||
this.path = this.url.pathname; | ||
@@ -121,3 +121,3 @@ } | ||
* | ||
* @param {Object} optional options with: | ||
* @param {Object} options (optional) with: | ||
* - path: resource path. | ||
@@ -128,3 +128,3 @@ * - body: a JS object that will get marshalled to JSON. | ||
* - expect: expected HTTP status code. Defaults to 204. | ||
* @callback {Function} (optional) callback of the form f(err, obj, headers). | ||
* @param {Function} callback of the form f(err, obj, headers). | ||
*/ | ||
@@ -165,3 +165,3 @@ RestClient.prototype.put = function(options, callback) { | ||
* - expect: (optional) expected HTTP status code. Defaults to 200. | ||
* @callback {Function} (optional) callback of the form function(err, headers). | ||
* @param {Function} callback of the form function(err, headers). | ||
*/ | ||
@@ -201,3 +201,3 @@ RestClient.prototype.post = function(options, callback) { | ||
* - expect: (optional) expected HTTP status code. Defaults to 200. | ||
* @callback {Function} (optional) callback of the form f(err, obj, headers). | ||
* @param {Function} callback of the form f(err, obj, headers). | ||
*/ | ||
@@ -237,3 +237,3 @@ RestClient.prototype.get = function(options, callback) { | ||
* - expect: (optional) expected HTTP status code. Defaults to 204. | ||
* @callback {Function} (optional) callback of the form function(err, headers). | ||
* @param {Function} callback of the form function(err, headers). | ||
*/ | ||
@@ -274,3 +274,3 @@ RestClient.prototype.del = function(options, callback) { | ||
* - expect: (optional) expected HTTP status code. Defaults to 204. | ||
* @callback {Function} (optional) callback of the form function(err, headers). | ||
* @param {Function} callback of the form function(err, headers). | ||
*/ | ||
@@ -461,3 +461,3 @@ RestClient.prototype.head = function(options, callback) { | ||
opts.port = (self.url.port || | ||
(self.url.protocol === "https:" ? 443 : 80)).toString(); | ||
(self.url.protocol === 'https:' ? 443 : 80)).toString(); | ||
} else { | ||
@@ -464,0 +464,0 @@ opts.socketPath = self.socketPath.toString(); |
@@ -299,3 +299,3 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
var handlers = utils.mergeFunctionArguments(args, offset); | ||
var handlers = utils.mergeFunctionArguments(args, offset); | ||
@@ -302,0 +302,0 @@ return this._mount('HEAD', url, handlers, version); |
@@ -15,3 +15,3 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
// createThrottle: require('./throttle').createThrottle, | ||
createThrottle: require('./throttle').createThrottle, | ||
@@ -18,0 +18,0 @@ HttpCodes: require('./http_codes'), |
@@ -164,3 +164,3 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
req.authorization = {}; | ||
req.username = 'anonymous'; | ||
if (!req.headers.authorization) { | ||
@@ -171,3 +171,3 @@ log.trace('No authorization header present.'); | ||
var pieces = req.headers.authorization.split(' ', 2); | ||
var pieces = req.headers.authorization.split(' ', 2); | ||
if (!pieces || pieces.length !== 2) { | ||
@@ -174,0 +174,0 @@ res.sendError(newError({ |
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
var newError = require('./error').newError; | ||
var log = require('./log'); | ||
var HttpCodes = require('./http_codes'); | ||
@@ -9,2 +12,23 @@ var RestCodes = require('./rest_codes'); | ||
/** | ||
* Creates an API rate limiter that can be plugged into the standard | ||
* restify request handling pipeline. | ||
* | ||
* This throttle gives you two options on which to throttle: | ||
* username and IP address. IP address is an exact match (i.e. a /32), | ||
* so keep that in mind if using it. In both cases, there's a blanket | ||
* policy put in place by `options.requests` and `options.seconds`. | ||
* Which obviously translates to M requetss in N seconds. On the `options` | ||
* object ip and username are treated as an XOR. | ||
* | ||
* @param {Object} options required options with: | ||
* - {Number} requests (required). | ||
* - {Number} seconds (required). | ||
* - {String} ip (optional). | ||
* - {String} username (optional). | ||
* - {Object} overrides (optional). | ||
* | ||
* @return {Function} of type f(req, res, next) to be plugged into a route. | ||
* @throws {TypeError} on bad input. | ||
*/ | ||
createThrottle: function(options) { | ||
@@ -18,4 +42,2 @@ if (!options) throw new TypeError('options is required'); | ||
var requests = options.requests; | ||
var seconds = options.seconds; | ||
var checked = new Date().getTime(); | ||
@@ -25,3 +47,7 @@ var hash = {}; | ||
return function(req, res, next) { | ||
var attr; | ||
var requests = options.requests; | ||
var seconds = options.seconds; | ||
var rate = requests / seconds; | ||
var attr = null; | ||
if (options.ip) { | ||
@@ -39,24 +65,50 @@ attr = req.connection.remoteAddress; | ||
} else { | ||
log.trace('throttle(%o): not actionable, skipping', options); | ||
return next(); | ||
} | ||
if (options.overrides) { | ||
if (options.overrides[attr] && | ||
options.overrides[attr].requests !== undefined && | ||
options.overrides[attr].seconds !== undefined) { | ||
requests = options.overrides[attr].requests; | ||
seconds = options.overrides[attr].seconds; | ||
log.trace('throttle(%o): attr=%s, using overrides r=%d, s=%d', | ||
options, attr, requests, seconds); | ||
} | ||
} | ||
if (seconds === 0) { | ||
log.trace('throttle(%o): attr=%s, seconds=0. unlimited', | ||
options, attr); | ||
return next(); | ||
} | ||
if (!hash[attr]) | ||
hash[attr] = options.requests; | ||
hash[attr] = requests + 1; | ||
var now = new Date().getTime(); | ||
var delta = now - checked; | ||
var delta = (now - checked) / 1000; // time since last check | ||
checked = now; | ||
hash[attr] += delta * (requests/seconds); | ||
if (hash[attr] > requests) { | ||
hash[attr] = requests; | ||
} else if (hash[attr] < 1.0) { | ||
if (delta > seconds) { | ||
log.trace('throttle(%o): attr=%s => resetting counter'); | ||
hash[attr] = requests + 1; | ||
return next(); | ||
} | ||
hash[attr] -= 1; | ||
log.trace('throttle(%o): attr=%s, current=%d, requests=%d', | ||
options, attr, hash[attr], requests); | ||
if (hash[attr] <= 0) { | ||
return res.sendError(newError({ | ||
httpCode: HttpCodes.Forbidden, | ||
restCode: RestCodes.RequestThrottled, | ||
message: 'You have exceeded ' + requests + 'r/' + seconds + 's' | ||
message: 'You have exceeded ' + requests + | ||
' requests per ' + seconds + ' seconds.' | ||
})); | ||
} else { | ||
hash[attr] -= 1.0; | ||
return next(); | ||
} | ||
return next(); | ||
}; | ||
@@ -63,0 +115,0 @@ } |
@@ -42,3 +42,3 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
mergeFunctionArguments: function (argv, offset) { | ||
mergeFunctionArguments: function(argv, offset) { | ||
var handlers = []; | ||
@@ -45,0 +45,0 @@ |
{ | ||
"name": "restify", | ||
"description": "REST framework specifically meant for web service APIs", | ||
"version": "0.2.10", | ||
"version": "0.2.11", | ||
"repository": { | ||
@@ -18,3 +18,3 @@ "type": "git", | ||
"scripts": { | ||
"pretest": "./node_modules/.bin/jshint lib tst", | ||
"pretest": "./node_modules/.bin/jshint lib tst && which gjslint; if [[ \"$?\" = 0 ]] ; then gjslint --nojsdoc -r . -x lib/sprintf.js -e node_modules; else echo \"Missing gjslint. Skipping lint\"; fi", | ||
"test": "./node_modules/.bin/whiskey --timeout 2000 -t \"`find tst -name *.test.js | xargs`\"" | ||
@@ -21,0 +21,0 @@ }, |
@@ -184,3 +184,3 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
var content = '\u00bd + \u00bc = \u00be'; | ||
var opts = common.newOptions(socket, '/test/' + uuid()); | ||
var opts = common.newOptions(socket, '/test/' + uuid()); | ||
opts.method = 'POST'; | ||
@@ -187,0 +187,0 @@ opts.headers['Content-Type'] = 'text/plain'; |
@@ -31,9 +31,26 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
seconds: 2, | ||
username: true | ||
username: true, | ||
overrides: { | ||
'admin': { | ||
requests: 0, | ||
seconds: 0 | ||
}, | ||
'special': { | ||
requests: 3, | ||
seconds: 1 | ||
} | ||
} | ||
}); | ||
server.get('/test/:name', throttle, function(req, res, next) { | ||
res.send(200); | ||
return next(); | ||
}); | ||
server.get('/test/:name', | ||
function(req, res, next) { | ||
req.username = req.uriParams.name; | ||
return next(); | ||
}, | ||
throttle, | ||
function(req, res, next) { | ||
res.send(200); | ||
return next(); | ||
} | ||
); | ||
@@ -47,6 +64,4 @@ server.listen(socket, function() { | ||
exports.test_ok = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/unit'); | ||
var opts = common.newOptions(socket, '/test/throttleMe'); | ||
opts.method = 'GET'; | ||
opts.headers.authorization = 'Basic ' + | ||
new Buffer(username + ':' + password, 'utf8').toString('base64'); | ||
@@ -61,4 +76,4 @@ http.request(opts, function(res) { | ||
exports.test_no_auth = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/unit'); | ||
exports.test_throttled = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/throttleMe'); | ||
opts.method = 'GET'; | ||
@@ -68,4 +83,9 @@ | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 401); | ||
test.finish(); | ||
assert.equal(res.statusCode, 403); | ||
common.checkContent(assert, res, function() { | ||
assert.ok(res.params); | ||
assert.equal(res.params.code, 'RequestThrottled'); | ||
assert.ok(res.params.message); | ||
setTimeout(function() { test.finish(); }, 1000); | ||
}); | ||
}).end(); | ||
@@ -75,11 +95,9 @@ }; | ||
exports.test_forbidden = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/unit'); | ||
exports.test_ok_after_window = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/throttleMe'); | ||
opts.method = 'GET'; | ||
opts.headers.authorization = 'Basic ' + | ||
new Buffer('...:' + password, 'utf8').toString('base64'); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 403); | ||
assert.equal(res.statusCode, 200); | ||
test.finish(); | ||
@@ -90,11 +108,14 @@ }).end(); | ||
exports.test_bad_scheme = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/unit'); | ||
exports.test_override_limited = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/special'); | ||
opts.method = 'GET'; | ||
opts.headers.authorization = uuid(); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 400); | ||
test.finish(); | ||
assert.equal(res.statusCode, 200); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
test.finish(); | ||
}).end(); | ||
}).end(); | ||
@@ -104,11 +125,14 @@ }; | ||
exports.test_bad_basic = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/unit'); | ||
exports.test_override_unlimited = function(test, assert) { | ||
var opts = common.newOptions(socket, '/test/admin'); | ||
opts.method = 'GET'; | ||
opts.headers.authorization = 'Basic ' + uuid(); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 400); | ||
test.finish(); | ||
assert.equal(res.statusCode, 200); | ||
http.request(opts, function(res) { | ||
common.checkResponse(assert, res); | ||
assert.equal(res.statusCode, 200); | ||
test.finish(); | ||
}).end(); | ||
}).end(); | ||
@@ -115,0 +139,0 @@ }; |
@@ -136,4 +136,4 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. | ||
server.get('/', function(req, res, next) { return res.send(200) ; }); | ||
server.get('1.2.4', '/', function(req, res, next) { return res.send(201) ; }); | ||
server.get('/', function(req, res, next) { return res.send(200); }); | ||
server.get('1.2.4', '/', function(req, res, next) { return res.send(201); }); | ||
server.listen(socket, function() { | ||
@@ -140,0 +140,0 @@ var opts = common.newOptions(socket, '/'); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
155875
3439