fastify-rate-limit
Advanced tools
Comparing version 2.1.1 to 2.2.0
import * as http from 'http'; | ||
import * as fastify from 'fastify'; | ||
import * as ioredis from 'ioredis'; | ||
declare namespace fastifyRateLimit { | ||
interface FastifyRateLimitOptions { | ||
global?: boolean; | ||
max?: number; | ||
@@ -11,5 +11,5 @@ timeWindow?: number; | ||
whitelist?: string[]; | ||
redis?: ioredis.Redis; | ||
redis?: any; | ||
skipOnError?: boolean; | ||
keyGenerator?: (req: fastify.FastifyRequest<any>) => string | number; | ||
keyGenerator?: (req: fastify.FastifyRequest<http.IncomingMessage>) => string | number; | ||
} | ||
@@ -25,2 +25,2 @@ } | ||
export = fastifyRateLimit; | ||
export = fastifyRateLimit; |
128
index.js
@@ -19,45 +19,119 @@ 'use strict' | ||
function rateLimitPlugin (fastify, opts, next) { | ||
const timeWindow = typeof opts.timeWindow === 'string' | ||
? ms(opts.timeWindow) | ||
: typeof opts.timeWindow === 'number' | ||
? opts.timeWindow | ||
function rateLimitPlugin (fastify, settings, next) { | ||
// create the object that will hold the "main" settings that can be shared during the build | ||
// 'global' will define, if the rate limit should be apply by default on all route. default : true | ||
const globalParams = { | ||
global: (typeof settings.global === 'boolean') ? settings.global : true | ||
} | ||
// define the global maximum of request allowed | ||
globalParams.max = (typeof settings.max === 'number' || typeof settings.max === 'function') | ||
? settings.max | ||
: 1000 | ||
// define the global Time Window | ||
globalParams.timeWindow = typeof settings.timeWindow === 'string' | ||
? ms(settings.timeWindow) | ||
: typeof settings.timeWindow === 'number' | ||
? settings.timeWindow | ||
: 1000 * 60 | ||
const store = opts.redis | ||
? new RedisStore(opts.redis, timeWindow) | ||
: new LocalStore(timeWindow, opts.cache) | ||
globalParams.whitelist = settings.whitelist || [] | ||
const keyGenerator = typeof opts.keyGenerator === 'function' | ||
? opts.keyGenerator | ||
// define the name of the app component. Related to redis, it will be use as a part of the keyname define in redis. | ||
const pluginComponent = { | ||
whitelist: globalParams.whitelist | ||
} | ||
if (settings.redis) { | ||
pluginComponent.store = new RedisStore(settings.redis, 'fastify-rate-limit-', globalParams.timeWindow) | ||
} else { | ||
pluginComponent.store = new LocalStore(globalParams.timeWindow, settings.cache, fastify) | ||
} | ||
globalParams.keyGenerator = typeof settings.keyGenerator === 'function' | ||
? settings.keyGenerator | ||
: (req) => req.raw.ip | ||
const skipOnError = opts.skipOnError === true | ||
const max = opts.max || 1000 | ||
const whitelist = opts.whitelist || [] | ||
const after = ms(timeWindow, { long: true }) | ||
// onRoute add the preHandler rate-limit function if needed | ||
fastify.addHook('onRoute', (routeOptions) => { | ||
if (routeOptions.config) { | ||
if (routeOptions.config.rateLimit && typeof routeOptions.config.rateLimit === 'object') { | ||
const current = Object.create(pluginComponent) | ||
current.store = pluginComponent.store.child(routeOptions) | ||
// if the current endpoint have a custom rateLimit configuration ... | ||
buildRouteRate(current, makeParams(routeOptions.config.rateLimit), routeOptions) | ||
} else if (routeOptions.config.rateLimit === false) { | ||
// don't apply any rate-limit | ||
} else { | ||
throw new Error('Unknown value for route rate-limit configuration') | ||
} | ||
} else if (globalParams.global) { | ||
// if the plugin is set globally ( meaning that all the route will be 'rate limited' ) | ||
// As the endpoint, does not have a custom rateLimit configuration, use the global one. | ||
buildRouteRate(pluginComponent, globalParams, routeOptions) | ||
} | ||
}) | ||
fastify.addHook('onRequest', onRateLimit) | ||
// Merge the parameters of a route with the global ones | ||
function makeParams (routeParams) { | ||
const result = Object.assign({}, globalParams, routeParams) | ||
if (typeof result.timeWindow === 'string') { | ||
result.timeWindow = ms(result.timeWindow) | ||
} | ||
return result | ||
} | ||
function onRateLimit (req, res, next) { | ||
var key = keyGenerator(req) | ||
if (whitelist.indexOf(key) > -1) { | ||
next() | ||
} | ||
function buildRouteRate (pluginComponent, params, routeOptions) { | ||
const after = ms(params.timeWindow, { long: true }) | ||
if (Array.isArray(routeOptions.preHandler)) { | ||
routeOptions.preHandler.push(preHandler) | ||
} else if (typeof routeOptions.preHandler === 'function') { | ||
routeOptions.preHandler = [routeOptions.preHandler, preHandler] | ||
} else { | ||
routeOptions.preHandler = [preHandler] | ||
} | ||
// PreHandler function that will be use for current endpoint been processed | ||
function preHandler (req, res, next) { | ||
// We retrieve the key from the generator. (can be the global one, or the one define in the endpoint) | ||
const key = params.keyGenerator(req) | ||
// whitelist doesn't apply any rate limit | ||
if (pluginComponent.whitelist.indexOf(key) > -1) { | ||
next() | ||
} else { | ||
store.incr(key, onIncr) | ||
return | ||
} | ||
// As the key is not whitelist in redis/lru, then we increment the rate-limit of the current request and we call the function "onIncr" | ||
pluginComponent.store.incr(key, onIncr) | ||
function onIncr (err, current) { | ||
if (err && skipOnError === false) return next(err) | ||
if (err && params.skipOnError === false) { | ||
return next(err) | ||
} | ||
if (current <= max) { | ||
res.header('X-RateLimit-Limit', max) | ||
res.header('X-RateLimit-Remaining', max - current) | ||
if (current <= params.max) { | ||
res.header('X-RateLimit-Limit', params.max) | ||
res.header('X-RateLimit-Remaining', params.max - current) | ||
if (typeof params.onExceeding === 'function') { | ||
params.onExceeding(req) | ||
} | ||
next() | ||
} else { | ||
if (typeof params.onExceeded === 'function') { | ||
params.onExceeded(req) | ||
} | ||
res.type('application/json').serializer(serializeError) | ||
res.code(429) | ||
.header('X-RateLimit-Limit', max) | ||
.header('X-RateLimit-Limit', params.max) | ||
.header('X-RateLimit-Remaining', 0) | ||
.header('Retry-After', timeWindow) | ||
.header('Retry-After', params.timeWindow) | ||
.send({ | ||
@@ -71,4 +145,2 @@ statusCode: 429, | ||
} | ||
next() | ||
} | ||
@@ -75,0 +147,0 @@ |
{ | ||
"name": "fastify-rate-limit", | ||
"version": "2.1.1", | ||
"version": "2.2.0", | ||
"description": "A low overhead rate limiter for your routes", | ||
@@ -8,4 +8,4 @@ "main": "index.js", | ||
"redis": "docker run -p 6379:6379 --rm redis:3.0.7", | ||
"test": "standard && tap test.js && npm run typescript", | ||
"typescript": "tsc --project ./tsconfig.json" | ||
"test": "standard && tap --cov test/*.test.js && npm run typescript", | ||
"typescript": "tsc --project ./test/types/tsconfig.json" | ||
}, | ||
@@ -28,15 +28,21 @@ "repository": { | ||
"devDependencies": { | ||
"@types/ioredis": "^4.0.10", | ||
"fastify": "^2.0.0", | ||
"ioredis": "^4.2.0", | ||
"@types/ioredis": "~4.0.11", | ||
"@types/node": "~12.0.4", | ||
"fastify": "^2.5.0", | ||
"ioredis": "^4.9.0", | ||
"standard": "^12.0.1", | ||
"tap": "^12.1.0", | ||
"typescript": "^3.3.3333" | ||
"tap": "^12.6.6", | ||
"typescript": "^3.4.2" | ||
}, | ||
"dependencies": { | ||
"fast-json-stringify": "^1.9.2", | ||
"fast-json-stringify": "^1.14.0", | ||
"fastify-plugin": "^1.3.0", | ||
"ms": "^2.1.1", | ||
"tiny-lru": "^6.0.1" | ||
}, | ||
"greenkeeper": { | ||
"ignore": [ | ||
"tap" | ||
] | ||
} | ||
} |
104
README.md
# fastify-rate-limit | ||
[](https://greenkeeper.io/) | ||
[](http://standardjs.com/) | ||
[](https://travis-ci.org/fastify/fastify-rate-limit) | ||
[](http://standardjs.com/) [](https://travis-ci.org/fastify/fastify-rate-limit) | ||
A low overhead rate limiter for your routes. Supports Fastify `2.x` versions. | ||
@@ -47,5 +47,7 @@ | ||
### Options | ||
You can pass the following options during the plugin registration, the values will be used in all the routes. | ||
You can pass the following options during the plugin registration: | ||
```js | ||
fastify.register(require('fastify-rate-limit'), { | ||
global : false, // default true | ||
max: 3, // default 1000 | ||
@@ -60,10 +62,14 @@ timeWindow: 5000, // default 1000 * 60 | ||
``` | ||
- `global` : indicates if the plugin should apply the rate limit setting to all routes within the encapsulation scope | ||
- `max`: is the maximum number of requests a single client can perform inside a timeWindow. | ||
- `timeWindow:` the duration of the time window, can be expressed in milliseconds (as a number) or as a string, see [`ms`](https://github.com/zeit/ms) too see the supported formats. | ||
- `cache`: this plugin internally uses a lru cache to handle the clients, you can change the size of the cache with this option. | ||
- `whitelist`: array of string of ips to exclude from rate limiting. | ||
- `timeWindow:` the duration of the time window. It can be expressed in milliseconds or as a string (in the [`ms`](https://github.com/zeit/ms) format) | ||
- `cache`: this plugin internally uses a lru cache to handle the clients, you can change the size of the cache with this option | ||
- `whitelist`: array of string of ips to exclude from rate limiting | ||
- `redis`: by default this plugins uses an in-memory store, which is fast but if you application works on more than one server it is useless, since the data is store locally.<br> | ||
You can pass a Redis client here and magically the issue is solved. To achieve the maximum speed, this plugins requires the use of [`ioredis`](https://github.com/luin/ioredis). | ||
You can pass a Redis client here and magically the issue is solved. To achieve the maximum speed, this plugins requires the use of [`ioredis`](https://github.com/luin/ioredis) | ||
- `skipOnError`: if `true` it will skip errors generated by the storage (eg, redis not reachable). | ||
- `keyGenerator`: a function to generate a unique identifier for each incoming request. Defaults to `(req) => req.ip`, the IP is resolved by fastify using `req.connection.remoteAddress` or `req.headers['x-forwarded-for']` if [trustProxy](https://www.fastify.io/docs/master/Server/#trustproxy) option is enabled. Use it if you want to override this behavior. Example usage: | ||
- `keyGenerator`: a function to generate a unique identifier for each incoming request. Defaults to `(req) => req.ip`, the IP is resolved by fastify using `req.connection.remoteAddress` or `req.headers['x-forwarded-for']` if [trustProxy](https://www.fastify.io/docs/master/Server/#trustproxy) option is enabled. Use it if you want to override this behavior | ||
`keyGenerator` example usage: | ||
```js | ||
@@ -81,2 +87,84 @@ fastify.register(require('fastify-rate-limit'), { | ||
### Options on the endpoint itself | ||
Rate limiting can be configured also for some routes, applying the configuration independently. | ||
For example the `whitelist` if configured: | ||
- on the plugin registration will affect all endpoints within the encapsulation scope | ||
- on the route declaration will affect only the targeted endpoint | ||
The global whitelist is configured when registering it with `fastify.register(...)`. | ||
The endpoint whitelist is set on the endpoint directly with the `{ config : { rateLimit : { whitelist : [] } } }` object. | ||
ACL checking is performed based on the value of the key from the `keyGenerator`. | ||
In this example we are checking the IP address, but it could be a whitelist of specific user identifiers (like JWT or tokens): | ||
```js | ||
const fastify = require('fastify')() | ||
fastify.register(require('fastify-rate-limit'), | ||
{ | ||
global : false, // don't apply these settings to all the routes of the context | ||
max: 3000, // default global max rate limit | ||
whitelist: ['192.168.0.10'], // global whitelist access. | ||
redis: redis, // custom connection to redis | ||
}) | ||
// add a limited route with this configuration plus the global one | ||
fastify.get('/', { | ||
config: { | ||
rateLimit: { | ||
max: 3, | ||
timeWindow: '1 minute' | ||
} | ||
} | ||
}, (req, reply) => { | ||
reply.send({ hello: 'from ... root' }) | ||
}) | ||
// add a limited route with this configuration plus the global one | ||
fastify.get('/private', { | ||
config: { | ||
rateLimit: { | ||
max: 3, | ||
timeWindow: '1 minute' | ||
} | ||
} | ||
}, (req, reply) => { | ||
reply.send({ hello: 'from ... private' }) | ||
}) | ||
// this route doesn't have any rate limit | ||
fastify.get('/public', (req, reply) => { | ||
reply.send({ hello: 'from ... public' }) | ||
}) | ||
// add a limited route with this configuration plus the global one | ||
fastify.get('/public/sub-rated-1', { | ||
config: { | ||
rateLimit: { | ||
timeWindow: '1 minute', | ||
whitelist: ['127.0.0.1'], | ||
onExceeding: function (req) { | ||
console.log('callback on exceededing ... executed before response to client') | ||
}, | ||
onExceeded: function (req) { | ||
console.log('callback on exceeded ... to black ip in security group for example, req is give as argument') | ||
} | ||
} | ||
} | ||
}, (req, reply) => { | ||
reply.send({ hello: 'from sub-rated-1 ... using default max value ... ' }) | ||
}) | ||
``` | ||
In the route creation you can override the same settings of the plugin registration plus the additionals options: | ||
- `onExceeding` : callback that will be executed each time a request is made to a route that is rate limited | ||
- `onExceeded` : callback that will be executed when a user reached the maximum number of tries. Can be useful to blacklist clients | ||
<a name="license"></a> | ||
@@ -83,0 +171,0 @@ ## License |
'use strict' | ||
const lru = require('tiny-lru') | ||
const ms = require('ms') | ||
function LocalStore (timeWindow, cache) { | ||
function LocalStore (timeWindow, cache, app) { | ||
this.lru = lru(cache || 5000) | ||
setInterval(this.lru.clear.bind(this.lru), timeWindow).unref() | ||
this.interval = setInterval(this.lru.clear.bind(this.lru), timeWindow).unref() | ||
this.app = app | ||
app.addHook('onClose', (done) => { | ||
clearInterval(this.interval) | ||
}) | ||
} | ||
@@ -16,2 +22,11 @@ | ||
LocalStore.prototype.child = function (routeOptions) { | ||
let timeWindow = routeOptions.config.rateLimit.timeWindow | ||
if (typeof timeWindow === 'string') { | ||
timeWindow = ms(timeWindow) | ||
} | ||
return new LocalStore(timeWindow, routeOptions.config.rateLimit.cache, this.app) | ||
} | ||
module.exports = LocalStore |
'use strict' | ||
const ms = require('ms') | ||
const noop = () => {} | ||
function RedisStore (redis, timeWindow) { | ||
function RedisStore (redis, key, timeWindow) { | ||
this.redis = redis | ||
this.timeWindow = timeWindow | ||
this.key = 'fastify-rate-limit-' | ||
this.key = key | ||
} | ||
@@ -26,2 +27,13 @@ | ||
RedisStore.prototype.child = function (routeOptions) { | ||
let timeWindow = routeOptions.config.rateLimit.timeWindow | ||
if (typeof timeWindow === 'string') { | ||
timeWindow = ms(timeWindow) | ||
} | ||
const child = Object.create(this) | ||
child.key = this.key + routeOptions.method + routeOptions.url + '-' | ||
child.timeWindow = timeWindow | ||
return child | ||
} | ||
module.exports = RedisStore |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
40749
14
1024
172
7
Updatedfast-json-stringify@^1.14.0