express-throttle
Advanced tools
Comparing version 1.3.0 to 1.3.2
@@ -10,4 +10,3 @@ "use strict"; | ||
MemoryStore.prototype.get = function(key, callback) { | ||
var entry = this.cache.get(key); | ||
callback(null, entry); | ||
callback(null, this.cache.get(key)); | ||
}; | ||
@@ -14,0 +13,0 @@ |
@@ -6,7 +6,9 @@ "use strict"; | ||
var options = require("./options"); | ||
// Default token storage (memory-bounded LRU cache) | ||
var MemoryStore = require("./memory-store"); | ||
function Throttle(options) { | ||
var opts = parse_options(options); | ||
function Throttle(opts) { | ||
opts = options.parse(opts); | ||
var refill; | ||
@@ -18,10 +20,10 @@ | ||
var bucket1 = Math.floor((bucket.mtime - bucket.window_start) / bucket.period); | ||
var bucket2 = Math.floor((t - bucket.window_start) / bucket.period); | ||
var window1 = Math.floor((bucket.mtime - bucket.window_start) / bucket_settings.period); | ||
var window2 = Math.floor((t - bucket.window_start) / bucket_settings.period); | ||
if (bucket1 == bucket2) { | ||
if (window1 == window2) { | ||
return 0; | ||
} else { | ||
bucket.window_start = t; | ||
return bucket.size; | ||
return bucket_settings.size; | ||
} | ||
@@ -36,10 +38,12 @@ }; | ||
var bucket_size = opts.burst || opts.rate.amount; | ||
var bucket_settings = { | ||
"size": opts.burst || opts.rate.amount, | ||
"period": opts.rate.period, | ||
"refill": refill | ||
}; | ||
var store = opts.store || new MemoryStore(10000); | ||
// key function, used to identify the client we are going to throttle | ||
var key_func = opts.key || function(req) { | ||
return req.headers["x-forwarded-for"] || req.connection.remoteAddress; | ||
}; | ||
var key_func = opts.key || function(req) { return req.ip; }; | ||
var cost_func; | ||
@@ -74,4 +78,4 @@ | ||
var t = Date.now(); | ||
bucket = bucket || create_bucket(bucket_size, opts.rate.period, t); | ||
var is_allowed = update_bucket(bucket, refill, cost, t); | ||
bucket = bucket || create_bucket(bucket_settings, t); | ||
var is_allowed = update_bucket(bucket, bucket_settings, cost, t); | ||
@@ -93,109 +97,19 @@ store.set(key, bucket, function(err) { | ||
function parse_options(options) { | ||
if (typeof(options) == "string") { | ||
options = { "rate": options }; | ||
} | ||
if (typeof(options) != "object") { | ||
throw new Error("options needs to be an object."); | ||
} else { | ||
options = shallow_clone(options); | ||
} | ||
if (typeof(options.rate) != "string") { | ||
throw new Error("'rate' needs to be a string (e.g 3/s, 5/2min, 10/day)."); | ||
} | ||
options.rate = parse_rate(options.rate); | ||
if (options.burst && typeof(options.burst) != "number") { | ||
throw new Error("'burst' needs to be a number."); | ||
} | ||
if (options.key && typeof(options.key) != "function") { | ||
throw new Error("'key' needs to be a function."); | ||
} | ||
if (options.cost && !(typeof(options.cost) == "number" || typeof(options.cost) == "function")) { | ||
throw new Error("'cost' needs to be a number or function."); | ||
} | ||
if (options.on_allowed && typeof(options.on_allowed) != "function") { | ||
throw new Error("'on_allowed' needs to be a function."); | ||
} | ||
if (options.on_throttled && typeof(options.on_throttled) != "function") { | ||
throw new Error("'on_throttled' needs to be a function."); | ||
} | ||
return options; | ||
} | ||
function shallow_clone(obj) { | ||
var clone = {}; | ||
for (var key in obj) { | ||
clone[key] = obj[key]; | ||
} | ||
return clone; | ||
} | ||
var RATE_PATTERN = /^(\d+)\/(\d+)?(ms|s|sec|second|m|min|minute|h|hour|d|day)(:fixed)?$/; | ||
function parse_rate(rate) { | ||
var parsed_rate = rate.match(RATE_PATTERN); | ||
if (!parsed_rate) { | ||
throw new Error("invalid rate (e.g 3/s, 5/2min, 10/day)."); | ||
} | ||
var numerator = parseInt(parsed_rate[1], 10); | ||
var denominator = parseInt(parsed_rate[2] || 1, 10); | ||
var time_unit = parsed_rate[3]; | ||
var fixed = parsed_rate[4] == ":fixed"; | ||
function create_bucket(settings, ctime) { | ||
return { | ||
"amount": numerator, | ||
"period": denominator * time_unit_to_ms(time_unit), | ||
"fixed": fixed | ||
}; | ||
} | ||
function time_unit_to_ms(time_unit) { | ||
switch (time_unit) { | ||
case "ms": | ||
return 1; | ||
case "s": case "sec": case "second": | ||
return 1000; | ||
case "m": case "min": case "minute": | ||
return 60 * 1000; | ||
case "h": case "hour": | ||
return 60 * 60 * 1000; | ||
case "d": case "day": | ||
return 24 * 60 * 60 * 1000; | ||
} | ||
} | ||
function create_bucket(size, period, ctime) { | ||
return { | ||
// max tokens this bucket will have (static) | ||
"size": size, | ||
// time in ms it takes to replenish all tokens (static) | ||
"period": period, | ||
// current token count | ||
"tokens": size, | ||
"tokens": settings.size, | ||
// last modification time | ||
"mtime": ctime, | ||
// reset time (time left in this period) | ||
"rtime": ctime + period | ||
"rtime": ctime + settings.period | ||
}; | ||
} | ||
function update_bucket(bucket, refill, cost, t) { | ||
function update_bucket(bucket, settings, cost, t) { | ||
// Apply the refill first so it doesn't cancel out with the tokens we are | ||
// about to drain | ||
bucket.tokens = clamp_max(bucket.tokens + refill(bucket, t), bucket.size); | ||
bucket.tokens = clamp_max(bucket.tokens + settings.refill(bucket, t), settings.size); | ||
bucket.mtime = t; | ||
bucket.rtime = Math.abs(bucket.period - t % bucket.period); | ||
bucket.rtime = Math.abs(settings.period - t % settings.period); | ||
@@ -202,0 +116,0 @@ if (bucket.tokens >= cost) { |
{ | ||
"name": "express-throttle", | ||
"version": "1.3.0", | ||
"version": "1.3.2", | ||
"description": "Request throttling middleware for Express", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "istanbul cover test/throttle.js", | ||
"test": "tap test/*.js --coverage", | ||
"lint": "eslint lib test" | ||
@@ -34,6 +34,5 @@ }, | ||
"express": "^4.14.0", | ||
"istanbul": "^0.4.4", | ||
"supertest": "^1.2.0", | ||
"tape": "^4.6.0" | ||
"tap": "^6.1.1" | ||
} | ||
} |
# express-throttle | ||
Request throttling middleware for Express framework | ||
[![Build Status](https://travis-ci.org/GlurG/express-throttle.svg?branch=master)](https://travis-ci.org/GlurG/express-throttle) | ||
## Installation | ||
@@ -52,3 +54,3 @@ | ||
``` | ||
By default, throttling is done on a per ip-address basis (respecting the X-Forwarded-For header). This can be configured by providing a custom key-function: | ||
By default, throttling is done on a per ip-address basis (see [this link](http://expressjs.com/en/api.html#req.ip) about how the ip address is extracted from the request). This can be configured by providing a custom key-function: | ||
```js | ||
@@ -182,4 +184,2 @@ var options = { | ||
{ | ||
"size": Number, (max number of tokens this bucket can have) | ||
"period": Number, (time in ms it takes to replenish all tokens) | ||
"tokens": Number, (current number of tokens) | ||
@@ -200,3 +200,3 @@ "mtime": Number, (last modification time) | ||
function(req) { | ||
return req.headers["x-forwarded-for"] || req.connection.remoteAddress; | ||
return req.ip; // http://expressjs.com/en/api.html#req.ip | ||
} | ||
@@ -203,0 +203,0 @@ ``` |
"use strict"; | ||
var test = require("tape"); | ||
var tap = require("tap"); | ||
var express = require("express"); | ||
@@ -10,4 +10,4 @@ var request = require("supertest"); | ||
function close_to(value, target, delta = 0.001) { | ||
return Math.abs(value - target) <= delta; | ||
function close_to(value, target, delta) { | ||
return Math.abs(value - target) <= (delta || 0.001); | ||
} | ||
@@ -25,91 +25,5 @@ | ||
test("fail to init...", t => { | ||
t.test("...without options", st => { | ||
st.throws(throttle, new Error); | ||
st.end(); | ||
}); | ||
tap.test("passthrough...", function(t) { | ||
t.plan(3); | ||
t.test("...with first argument not being a string or object", st => { | ||
st.throws(() => throttle(5), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with invalid rate string (float not allowed)", st => { | ||
st.throws(() => throttle("1.0/h"), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with invalid rate string (float not allowed)", st => { | ||
st.throws(() => throttle("1/2.0h"), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with invalid rate option", st => { | ||
st.throws(() => throttle("10/m:test"), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with empty option object", st => { | ||
st.throws(() => throttle({}), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with 'burst' not being a number", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "burst": "5" }), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with 'key' not being a function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "key": 1 }), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with 'cost' not being a number or function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "cost": "5" }), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with 'on_allowed' not being a function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "on_allowed": "test" }), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with 'on_throttled' not being a function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "on_throttled": "test" }), new Error); | ||
st.end(); | ||
}); | ||
}); | ||
test("init with...", t => { | ||
t.test("...rate", st => { | ||
st.doesNotThrow(() => throttle("1/200ms")); | ||
st.doesNotThrow(() => throttle("1/s")); | ||
st.doesNotThrow(() => throttle("1/2sec")); | ||
st.doesNotThrow(() => throttle("1/second")); | ||
st.doesNotThrow(() => throttle("1/m")); | ||
st.doesNotThrow(() => throttle("1/3min")); | ||
st.doesNotThrow(() => throttle("1/minute")); | ||
st.doesNotThrow(() => throttle("1/4h")); | ||
st.doesNotThrow(() => throttle("1/hour")); | ||
st.doesNotThrow(() => throttle("1/d")); | ||
st.doesNotThrow(() => throttle("1/5day")); | ||
st.doesNotThrow(() => throttle("1/m:fixed")); | ||
st.end(); | ||
}); | ||
t.test("...options object", st => { | ||
st.doesNotThrow(() => throttle({ | ||
"rate": "1/s", | ||
"burst": 5, | ||
"key": () => true, | ||
"cost": () => true, | ||
"on_allowed": () => true, | ||
"on_throttled": () => true | ||
})); | ||
st.end(); | ||
}); | ||
}); | ||
test("passthrough...", t => { | ||
function verify(st, end) { | ||
@@ -125,6 +39,6 @@ return function(err, res) { | ||
t.test("...2 requests with enough gap @ rate 5/s", st => { | ||
t.test("...2 requests with enough gap @ rate 5/s", function(st) { | ||
var app = create_app({ "rate": "5/s", "burst": 1 }); | ||
request(app).get("/").end(verify(st)); | ||
setTimeout(() => { | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
@@ -134,6 +48,6 @@ }, 250); // add 50ms to allow some margin for error | ||
t.test("...2 requests with enough gap @ rate 5/2s", st => { | ||
t.test("...2 requests with enough gap @ rate 5/2s", function(st) { | ||
var app = create_app({ "rate": "5/2s", "burst": 1 }); | ||
request(app).get("/").end(verify(st)); | ||
setTimeout(() => { | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
@@ -143,6 +57,6 @@ }, 450); | ||
t.test("...2 requests with enough gap @ rate 5/s:fixed", st => { | ||
t.test("...2 requests with enough gap @ rate 5/s:fixed", function(st) { | ||
var app = create_app({ "rate": "5/s:fixed", "burst": 1 }); | ||
request(app).get("/").end(verify(st)); | ||
setTimeout(() => { | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
@@ -153,3 +67,5 @@ }, 1050); | ||
test("throttle...", t => { | ||
tap.test("throttle...", function(t) { | ||
t.plan(3); | ||
function verify(st, end) { | ||
@@ -165,6 +81,6 @@ return function(err, res) { | ||
t.test("...2 requests without enough gap @ rate 5/s", st => { | ||
t.test("...2 requests without enough gap @ rate 5/s", function(st) { | ||
var app = create_app({ "rate": "5/s", "burst": 1 }); | ||
request(app).get("/").end(() => true); | ||
setTimeout(() => { | ||
request(app).get("/").end(function() {}); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
@@ -174,6 +90,6 @@ }, 150); | ||
t.test("...2 requests without enough gap @ rate 5/2s", st => { | ||
t.test("...2 requests without enough gap @ rate 5/2s", function(st) { | ||
var app = create_app({ "rate": "5/2s", "burst": 1 }); | ||
request(app).get("/").end(() => true); | ||
setTimeout(() => { | ||
request(app).get("/").end(function() {}); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
@@ -183,6 +99,6 @@ }, 350); | ||
t.test("...2 requests without enough gap @ rate 5/s:fixed", st => { | ||
t.test("...2 requests without enough gap @ rate 5/s:fixed", function(st) { | ||
var app = create_app({ "rate": "5/s:fixed", "burst": 1 }); | ||
request(app).get("/").end(() => true); | ||
setTimeout(() => { | ||
request(app).get("/").end(function() {}); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
@@ -193,4 +109,6 @@ }, 950); | ||
test("custom store...", t => { | ||
t.test("...that fails to retrieve", st => { | ||
tap.test("custom store...", function(t) { | ||
t.plan(3); | ||
t.test("...that fails to retrieve", function(st) { | ||
function FailStore() { } | ||
@@ -209,6 +127,6 @@ FailStore.prototype.get = function(key, callback) { | ||
request(app).get("/").end(() => st.end()); | ||
request(app).get("/").end(function() { st.end(); }); | ||
}); | ||
t.test("...that fails to save", st => { | ||
t.test("...that fails to save", function(st) { | ||
function FailStore() { } | ||
@@ -227,12 +145,12 @@ FailStore.prototype.get = function(key, callback) { callback(null, {}); }; | ||
request(app).get("/").end(() => st.end()); | ||
request(app).get("/").end(function() { st.end(); }); | ||
}); | ||
t.test("...that works", st => { | ||
t.test("...that works", function(st) { | ||
var store = new MemoryStore(); | ||
var app = create_app({ "rate": "1/s", "store": store }); | ||
request(app).get("/").end((err, res) => { | ||
request(app).get("/").end(function(err, res) { | ||
st.equal(res.status, 200); | ||
store.get(res.body, (err, bucket) => { | ||
store.get(res.body, function(err, bucket) { | ||
st.ok(bucket); | ||
@@ -245,18 +163,4 @@ st.end(); | ||
test("respect x-forwarded-for header", t => { | ||
tap.test("custom key function", function(t) { | ||
var store = new MemoryStore(); | ||
var proxy_ip = "123.123.123.123"; | ||
var app = create_app({ "rate": "1/s", "store": store }); | ||
request(app).get("/").set("x-forwarded-for", proxy_ip).end((err, res) => { | ||
t.equal(res.status, 200); | ||
store.get(proxy_ip, (err, bucket) => { | ||
t.ok(bucket); | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
test("custom key function", t => { | ||
var store = new MemoryStore(); | ||
var custom_key = "custom_key"; | ||
@@ -269,5 +173,5 @@ var app = create_app({ | ||
request(app).get("/").end((err, res) => { | ||
request(app).get("/").end(function(err, res) { | ||
t.equal(res.status, 200); | ||
store.get(custom_key, (err, bucket) => { | ||
store.get(custom_key, function(err, bucket) { | ||
t.ok(bucket); | ||
@@ -279,3 +183,3 @@ t.end(); | ||
test("custom cost value", t => { | ||
tap.test("custom cost value", function(t) { | ||
var store = new MemoryStore(); | ||
@@ -289,8 +193,8 @@ var app = create_app({ | ||
request(app).get("/").end((err, res) => { | ||
store.get(res.body, (err, bucket) => { | ||
request(app).get("/").end(function(err, res) { | ||
store.get(res.body, function(err, bucket) { | ||
t.equal(res.status, 200); | ||
t.assert(close_to(bucket.tokens, 2)); | ||
request(app).get("/").end((err, res) => { | ||
request(app).get("/").end(function(err, res) { | ||
t.equal(res.status, 429); | ||
@@ -303,3 +207,3 @@ t.end(); | ||
test("custom cost function passthrough", t => { | ||
tap.test("custom cost function passthrough", function(t) { | ||
var app = express(); | ||
@@ -323,13 +227,13 @@ var store = new MemoryStore(); | ||
request(app).get("/yes").end((err, res) => { | ||
store.get(res.body, (err, bucket) => { | ||
request(app).get("/yes").end(function(err, res) { | ||
store.get(res.body, function(err, bucket) { | ||
t.equal(res.status, 200); | ||
t.assert(close_to(bucket.tokens, 5)); | ||
request(app).get("/no").end((err, res) => { | ||
store.get(res.body, (err, bucket) => { | ||
request(app).get("/no").end(function(err, res) { | ||
store.get(res.body, function(err, bucket) { | ||
t.equal(res.status, 200); | ||
t.assert(close_to(bucket.tokens, 2)); | ||
request(app).get("/no").end((err, res) => { | ||
request(app).get("/no").end(function(err, res) { | ||
t.equal(res.status, 429); | ||
@@ -344,3 +248,3 @@ t.end(); | ||
test("custom on_allowed function", t => { | ||
tap.test("custom on_allowed function", function(t) { | ||
var app = create_app({ | ||
@@ -353,3 +257,3 @@ "rate": "1/s", | ||
request(app).get("/").end((err, res) => { | ||
request(app).get("/").end(function(err, res) { | ||
t.equal(res.status, 201); | ||
@@ -361,3 +265,3 @@ t.assert(close_to(res.body.tokens, 0)); | ||
test("custom on_throttled function", t => { | ||
tap.test("custom on_throttled function", function(t) { | ||
var app = create_app({ | ||
@@ -370,4 +274,4 @@ "rate": "1/s", | ||
request(app).get("/").end(() => true); | ||
request(app).get("/").end((err, res) => { | ||
request(app).get("/").end(function() {}); | ||
request(app).get("/").end(function(err, res) { | ||
t.equal(res.status, 503); | ||
@@ -374,0 +278,0 @@ t.assert(close_to(res.body.tokens, 0)); |
Sorry, the diff of this file is not supported yet
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
26436
3
13
492