Aktueller Stand
This commit is contained in:
5
backend/node_modules/@fastify/rate-limit/.gitattributes
generated
vendored
Normal file
5
backend/node_modules/@fastify/rate-limit/.gitattributes
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set
|
||||
* text=auto
|
||||
|
||||
# Require Unix line endings
|
||||
* text eol=lf
|
||||
13
backend/node_modules/@fastify/rate-limit/.github/dependabot.yml
generated
vendored
Normal file
13
backend/node_modules/@fastify/rate-limit/.github/dependabot.yml
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
21
backend/node_modules/@fastify/rate-limit/.github/stale.yml
generated
vendored
Normal file
21
backend/node_modules/@fastify/rate-limit/.github/stale.yml
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 15
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- "discussion"
|
||||
- "feature request"
|
||||
- "bug"
|
||||
- "help wanted"
|
||||
- "plugin suggestion"
|
||||
- "good first issue"
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
24
backend/node_modules/@fastify/rate-limit/.github/workflows/ci.yml
generated
vendored
Normal file
24
backend/node_modules/@fastify/rate-limit/.github/workflows/ci.yml
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
uses: fastify/workflows/.github/workflows/plugins-ci-redis.yml@v5
|
||||
with:
|
||||
license-check: true
|
||||
lint: true
|
||||
21
backend/node_modules/@fastify/rate-limit/LICENSE
generated
vendored
Normal file
21
backend/node_modules/@fastify/rate-limit/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Fastify
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
550
backend/node_modules/@fastify/rate-limit/README.md
generated
vendored
Normal file
550
backend/node_modules/@fastify/rate-limit/README.md
generated
vendored
Normal file
@@ -0,0 +1,550 @@
|
||||
# @fastify/rate-limit
|
||||
|
||||
[](https://github.com/fastify/fastify-rate-limit/actions/workflows/ci.yml)
|
||||
[](https://www.npmjs.com/package/@fastify/rate-limit)
|
||||
[](https://github.com/neostandard/neostandard)
|
||||
|
||||
A low overhead rate limiter for your routes.
|
||||
|
||||
|
||||
## Install
|
||||
```
|
||||
npm i @fastify/rate-limit
|
||||
```
|
||||
|
||||
### Compatibility
|
||||
|
||||
| Plugin version | Fastify version |
|
||||
| -------------- | -------------------- |
|
||||
| `>=10.x` | `^5.x` |
|
||||
| `>=7.x <10.x` | `^4.x` |
|
||||
| `>=3.x <7.x` | `^3.x` |
|
||||
| `>=2.x <7.x` | `^2.x` |
|
||||
| `^1.x` | `^1.x` |
|
||||
|
||||
|
||||
Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin
|
||||
in the table above.
|
||||
See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details.
|
||||
|
||||
|
||||
## Usage
|
||||
Register the plugin and, if required, pass some custom options.<br>
|
||||
This plugin will add an `onRequest` hook to check if a client (based on their IP address) has made too many requests in the given timeWindow.
|
||||
```js
|
||||
import Fastify from 'fastify'
|
||||
|
||||
const fastify = Fastify()
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
max: 100,
|
||||
timeWindow: '1 minute'
|
||||
})
|
||||
|
||||
fastify.get('/', (request, reply) => {
|
||||
reply.send({ hello: 'world' })
|
||||
})
|
||||
|
||||
fastify.listen({ port: 3000 }, err => {
|
||||
if (err) throw err
|
||||
console.log('Server listening at http://localhost:3000')
|
||||
})
|
||||
```
|
||||
|
||||
In case a client reaches the maximum number of allowed requests, an error will be sent to the user with the status code set to `429`:
|
||||
```js
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 minute'
|
||||
}
|
||||
```
|
||||
You can change the response by providing a callback to `errorResponseBuilder` or setting a [custom error handler](https://fastify.dev/docs/latest/Reference/Server/#seterrorhandler):
|
||||
|
||||
```js
|
||||
fastify.setErrorHandler(function (error, request, reply) {
|
||||
if (error.statusCode === 429) {
|
||||
reply.code(429)
|
||||
error.message = 'You hit the rate limit! Slow down please!'
|
||||
}
|
||||
reply.send(error)
|
||||
})
|
||||
```
|
||||
|
||||
The response will have some additional headers:
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
|`x-ratelimit-limit` | how many requests the client can make
|
||||
|`x-ratelimit-remaining` | how many requests remain to the client in the timewindow
|
||||
|`x-ratelimit-reset` | how many seconds must pass before the rate limit resets
|
||||
|`retry-after` | if the max has been reached, the seconds the client must wait before they can make new requests
|
||||
|
||||
|
||||
### Preventing guessing of URLS through 404s
|
||||
|
||||
An attacker could search for valid URLs if your 404 error handling is not rate limited.
|
||||
To rate limit your 404 response, you can use a custom handler:
|
||||
|
||||
```js
|
||||
const fastify = Fastify()
|
||||
await fastify.register(rateLimit, { global: true, max: 2, timeWindow: 1000 })
|
||||
fastify.setNotFoundHandler({
|
||||
preHandler: fastify.rateLimit()
|
||||
}, function (request, reply) {
|
||||
reply.code(404).send({ hello: 'world' })
|
||||
})
|
||||
```
|
||||
|
||||
Note that you can customize the behavior of the preHandler in the same way you would for specific routes:
|
||||
|
||||
```js
|
||||
const fastify = Fastify()
|
||||
await fastify.register(rateLimit, { global: true, max: 2, timeWindow: 1000 })
|
||||
fastify.setNotFoundHandler({
|
||||
preHandler: fastify.rateLimit({
|
||||
max: 4,
|
||||
timeWindow: 500
|
||||
})
|
||||
}, function (request, reply) {
|
||||
reply.code(404).send({ hello: 'world' })
|
||||
})
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
You can pass the following options during the plugin registration:
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
global : false, // default true
|
||||
max: 3, // default 1000
|
||||
ban: 2, // default -1
|
||||
timeWindow: 5000, // default 1000 * 60
|
||||
hook: 'preHandler', // default 'onRequest'
|
||||
cache: 10000, // default 5000
|
||||
allowList: ['127.0.0.1'], // default []
|
||||
redis: new Redis({ host: '127.0.0.1' }), // default null
|
||||
nameSpace: 'teste-ratelimit-', // default is 'fastify-rate-limit-'
|
||||
continueExceeding: true, // default false
|
||||
skipOnError: true, // default false
|
||||
keyGenerator: function (request) { /* ... */ }, // default (request) => request.ip
|
||||
errorResponseBuilder: function (request, context) { /* ... */},
|
||||
enableDraftSpec: true, // default false. Uses IEFT draft header standard
|
||||
addHeadersOnExceeding: { // default show all the response headers when rate limit is not reached
|
||||
'x-ratelimit-limit': true,
|
||||
'x-ratelimit-remaining': true,
|
||||
'x-ratelimit-reset': true
|
||||
},
|
||||
addHeaders: { // default show all the response headers when rate limit is reached
|
||||
'x-ratelimit-limit': true,
|
||||
'x-ratelimit-remaining': true,
|
||||
'x-ratelimit-reset': true,
|
||||
'retry-after': true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `global` : indicates if the plugin should apply rate limiting to all routes within the encapsulation scope.
|
||||
- `max`: maximum number of requests a single client can perform inside a timeWindow. It can be an async function with the signature `async (request, key) => {}` where `request` is the Fastify request object and `key` is the value generated by the `keyGenerator`. The function **must** return a number.
|
||||
- `ban`: maximum number of 429 responses to return to a client before returning 403 responses. When the ban limit is exceeded, the context argument that is passed to `errorResponseBuilder` will have its `ban` property set to `true`. **Note:** `0` can also be passed to directly return 403 responses when a client exceeds the `max` limit.
|
||||
- `timeWindow:` the duration of the time window. It can be expressed in milliseconds, as a string (in the [`ms`](https://github.com/zeit/ms) format), or as an async function with the signature `async (request, key) => {}` where `request` is the Fastify request object and `key` is the value generated by the `keyGenerator`. The function **must** return a number.
|
||||
- `cache`: this plugin internally uses an LRU cache to handle the clients, you can change the size of the cache with this option
|
||||
- `allowList`: array of string of IPs to exclude from rate limiting. It can be a sync or async function with the signature `(request, key) => {}` where `request` is the Fastify request object and `key` is the value generated by the `keyGenerator`. If the function return a truthy value, the request will be excluded from the rate limit.
|
||||
- `redis`: by default, this plugin uses an in-memory store, but if an application runs on multiple servers, an external store will be needed. This plugin requires the use of [`ioredis`](https://github.com/redis/ioredis).<br> **Note:** the [default settings](https://github.com/redis/ioredis/blob/v4.16.0/API.md#new_Redis_new) of an ioredis instance are not optimal for rate limiting. We recommend customizing the `connectTimeout` and `maxRetriesPerRequest` parameters as shown in the [`example`](https://github.com/fastify/fastify-rate-limit/tree/main/example/example.js).
|
||||
- `nameSpace`: choose which prefix to use in the redis, default is 'fastify-rate-limit-'
|
||||
- `continueExceeding`: Renew user limitation when user sends a request to the server when still limited. This will take priority over `exponentialBackoff`
|
||||
- `store`: a custom store to track requests and rates which allows you to use your own storage mechanism (using an RDBMS, MongoDB, etc.) as well as further customizing the logic used in calculating the rate limits. A simple example is provided below as well as a more detailed example using Knex.js can be found in the [`example/`](https://github.com/fastify/fastify-rate-limit/tree/main/example) folder
|
||||
- `skipOnError`: if `true` it will skip errors generated by the storage (e.g. redis not reachable).
|
||||
- `keyGenerator`: a sync or async function to generate a unique identifier for each incoming request. Defaults to `(request) => request.ip`, the IP is resolved by fastify using `request.connection.remoteAddress` or `request.headers['x-forwarded-for']` if [trustProxy](https://fastify.dev/docs/latest/Reference/Server/#trustproxy) option is enabled. Use it if you want to override this behavior
|
||||
- `groupId`: a string to group multiple routes together introducing separate per-group rate limit. This will be added on top of the result of `keyGenerator`.
|
||||
- `errorResponseBuilder`: a function to generate a custom response object. Defaults to `(request, context) => ({statusCode: 429, error: 'Too Many Requests', message: ``Rate limit exceeded, retry in ${context.after}``})`
|
||||
- `addHeadersOnExceeding`: define which headers should be added in the response when the limit is not reached. Defaults all the headers will be shown
|
||||
- `addHeaders`: define which headers should be added in the response when the limit is reached. Defaults all the headers will be shown
|
||||
- `enableDraftSpec`: if `true` it will change the HTTP rate limit headers following the IEFT draft document. More information at [draft-ietf-httpapi-ratelimit-headers.md](https://github.com/ietf-wg-httpapi/ratelimit-headers/blob/f6a7bc7560a776ea96d800cf5ed3752d6d397b06/draft-ietf-httpapi-ratelimit-headers.md).
|
||||
- `onExceeding`: callback that will be executed before request limit has been reached.
|
||||
- `onExceeded`: callback that will be executed after request limit has been reached.
|
||||
- `onBanReach`: callback that will be executed when the ban limit has been reached.
|
||||
- `exponentialBackoff`: Renew user limitation exponentially when user sends a request to the server when still limited.
|
||||
|
||||
`keyGenerator` example usage:
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
/* ... */
|
||||
keyGenerator: function (request) {
|
||||
return request.headers['x-real-ip'] // nginx
|
||||
|| request.headers['x-client-ip'] // apache
|
||||
|| request.headers['x-forwarded-for'] // use this only if you trust the header
|
||||
|| request.session.username // you can limit based on any session value
|
||||
|| request.ip // fallback to default
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Variable `max` example usage:
|
||||
```js
|
||||
// In the same timeWindow, the max value can change based on request and/or key like this
|
||||
fastify.register(rateLimit, {
|
||||
/* ... */
|
||||
keyGenerator (request) { return request.headers['service-key'] },
|
||||
max: async (request, key) => { return key === 'pro' ? 3 : 2 },
|
||||
timeWindow: 1000
|
||||
})
|
||||
```
|
||||
|
||||
`errorResponseBuilder` example usage:
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
/* ... */
|
||||
errorResponseBuilder: function (request, context) {
|
||||
return {
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: `I only allow ${context.max} requests per ${context.after} to this Website. Try again soon.`,
|
||||
date: Date.now(),
|
||||
expiresIn: context.ttl // milliseconds
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Dynamic `allowList` example usage:
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
/* ... */
|
||||
allowList: function (request, key) {
|
||||
return request.headers['x-app-client-id'] === 'internal-usage'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Custom `hook` example usage (after authentication):
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
hook: 'preHandler',
|
||||
keyGenerator: function (request) {
|
||||
return request.userId || request.ip
|
||||
}
|
||||
})
|
||||
|
||||
fastify.decorateRequest('userId', '')
|
||||
fastify.addHook('preHandler', async function (request) {
|
||||
const { userId } = request.query
|
||||
if (userId) {
|
||||
request.userId = userId
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Custom `store` example usage:
|
||||
|
||||
NOTE: The ```timeWindow``` will always be passed as the numeric value in milliseconds into the store's constructor.
|
||||
|
||||
```js
|
||||
function CustomStore (options) {
|
||||
this.options = options
|
||||
this.current = 0
|
||||
}
|
||||
|
||||
CustomStore.prototype.incr = function (key, cb) {
|
||||
const timeWindow = this.options.timeWindow
|
||||
this.current++
|
||||
cb(null, { current: this.current, ttl: timeWindow - (this.current * 1000) })
|
||||
}
|
||||
|
||||
CustomStore.prototype.child = function (routeOptions) {
|
||||
// We create a merged copy of the current parent parameters with the specific
|
||||
// route parameters and pass them into the child store.
|
||||
const childParams = Object.assign(this.options, routeOptions)
|
||||
const store = new CustomStore(childParams)
|
||||
// Here is where you may want to do some custom calls on the store with the information
|
||||
// in routeOptions first...
|
||||
// store.setSubKey(routeOptions.method + routeOptions.url)
|
||||
return store
|
||||
}
|
||||
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
/* ... */
|
||||
store: CustomStore
|
||||
})
|
||||
```
|
||||
|
||||
The `routeOptions` object passed to the `child` method of the store will contain the same options that are detailed above for plugin registration with any specific overrides provided on the route. In addition, the following parameter is provided:
|
||||
|
||||
- `routeInfo`: The configuration of the route including `method`, `url`, `path`, and the full route `config`
|
||||
|
||||
Custom `onExceeding` example usage:
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
/* */
|
||||
onExceeding: function (req, key) {
|
||||
console.log('callback on exceeding ... executed before response to client')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Custom `onExceeded` example usage:
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
/* */
|
||||
onExceeded: function (req, key) {
|
||||
console.log('callback on exceeded ... executed before response to client')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Custom `onBanReach` example usage:
|
||||
```js
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
/* */
|
||||
ban: 10,
|
||||
onBanReach: function (req, key) {
|
||||
console.log('callback on exceeded ban limit')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Options on the endpoint itself
|
||||
|
||||
Rate limiting can also be configured at the route level, applying the configuration independently.
|
||||
|
||||
For example the `allowList` if configured:
|
||||
- on plugin registration will affect all endpoints within the encapsulation scope
|
||||
- on route declaration will affect only the targeted endpoint
|
||||
|
||||
The global allowlist is configured when registering it with `fastify.register(...)`.
|
||||
|
||||
The endpoint allowlist is set on the endpoint directly with the `{ config : { rateLimit : { allowList : [] } } }` 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 an allowlist of specific user identifiers (like JWT or tokens):
|
||||
|
||||
```js
|
||||
import Fastify from 'fastify'
|
||||
|
||||
const fastify = Fastify()
|
||||
await fastify.register(import('@fastify/rate-limit'),
|
||||
{
|
||||
global : false, // don't apply these settings to all the routes of the context
|
||||
max: 3000, // default global max rate limit
|
||||
allowList: ['192.168.0.10'], // global allowlist 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'
|
||||
}
|
||||
}
|
||||
}, (request, 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'
|
||||
}
|
||||
}
|
||||
}, (request, reply) => {
|
||||
reply.send({ hello: 'from ... private' })
|
||||
})
|
||||
|
||||
// this route doesn't have any rate limit
|
||||
fastify.get('/public', (request, 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',
|
||||
allowList: ['127.0.0.1'],
|
||||
onExceeding: function (request, key) {
|
||||
console.log('callback on exceeding ... executed before response to client')
|
||||
},
|
||||
onExceeded: function (request, key) {
|
||||
console.log('callback on exceeded ... to black ip in security group for example, request is give as argument')
|
||||
}
|
||||
}
|
||||
}
|
||||
}, (request, reply) => {
|
||||
reply.send({ hello: 'from sub-rated-1 ... using default max value ... ' })
|
||||
})
|
||||
|
||||
// group routes and add a rate limit
|
||||
fastify.get('/otp/send', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute',
|
||||
groupId:"OTP"
|
||||
}
|
||||
}
|
||||
}, (request, reply) => {
|
||||
reply.send({ hello: 'from ... grouped rate limit' })
|
||||
})
|
||||
|
||||
fastify.get('/otp/resend', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute',
|
||||
groupId:"OTP"
|
||||
}
|
||||
}
|
||||
}, (request, reply) => {
|
||||
reply.send({ hello: 'from ... grouped rate limit' })
|
||||
})
|
||||
```
|
||||
|
||||
In the route creation you can override the same settings of the plugin registration plus the following additional 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 reaches the maximum number of tries. Can be useful to blacklist clients
|
||||
|
||||
You may also want to set a global rate limiter and then disable it on some routes:
|
||||
|
||||
```js
|
||||
import Fastify from 'fastify'
|
||||
|
||||
const fastify = Fastify()
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
max: 100,
|
||||
timeWindow: '1 minute'
|
||||
})
|
||||
|
||||
// add a limited route with global config
|
||||
fastify.get('/', (request, reply) => {
|
||||
reply.send({ hello: 'from ... rate limited root' })
|
||||
})
|
||||
|
||||
// this route doesn't have any rate limit
|
||||
fastify.get('/public', {
|
||||
config: {
|
||||
rateLimit: false
|
||||
}
|
||||
}, (request, reply) => {
|
||||
reply.send({ hello: 'from ... public' })
|
||||
})
|
||||
|
||||
// add a limited route with global config and different max
|
||||
fastify.get('/private', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 9
|
||||
}
|
||||
}
|
||||
}, (request, reply) => {
|
||||
reply.send({ hello: 'from ... private and more limited' })
|
||||
})
|
||||
```
|
||||
|
||||
### Manual Rate Limit
|
||||
|
||||
A custom limiter function can be created with `fastify.createRateLimit()`, which is handy when needing to integrate with
|
||||
technologies like [GraphQL](https://graphql.org/) or [tRPC](https://trpc.io/). This function uses the global [options](#options) set
|
||||
during plugin registration, but you can override options such as `store`, `skipOnError`, `max`, `timeWindow`,
|
||||
`allowList`, `keyGenerator`, and `ban`.
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
import Fastify from 'fastify'
|
||||
|
||||
const fastify = Fastify()
|
||||
|
||||
// register with global options
|
||||
await fastify.register(import('@fastify/rate-limit'), {
|
||||
global : false,
|
||||
max: 100,
|
||||
timeWindow: '1 minute'
|
||||
})
|
||||
|
||||
// checkRateLimit will use the global options provided above when called
|
||||
const checkRateLimit = fastify.createRateLimit();
|
||||
|
||||
fastify.get("/", async (request, reply) => {
|
||||
// manually check the rate limit (using global options)
|
||||
const limit = await checkRateLimit(request);
|
||||
|
||||
if(!limit.isAllowed && limit.isExceeded) {
|
||||
return reply.code(429).send("Limit exceeded");
|
||||
}
|
||||
|
||||
return reply.send("Hello world");
|
||||
});
|
||||
|
||||
// override global max option
|
||||
const checkCustomRateLimit = fastify.createRateLimit({ max: 100 });
|
||||
|
||||
fastify.get("/custom", async (request, reply) => {
|
||||
// manually check the rate limit (using global options and overridden max option)
|
||||
const limit = await checkCustomRateLimit(request);
|
||||
|
||||
// manually handle limit exceedance
|
||||
if(!limit.isAllowed && limit.isExceeded) {
|
||||
return reply.code(429).send("Limit exceeded");
|
||||
}
|
||||
|
||||
return reply.send("Hello world");
|
||||
});
|
||||
```
|
||||
|
||||
A custom limiter function created with `fastify.createRateLimit()` only requires a `FastifyRequest` as the first parameter:
|
||||
|
||||
```js
|
||||
const checkRateLimit = fastify.createRateLimit();
|
||||
const limit = await checkRateLimit(request);
|
||||
```
|
||||
|
||||
The returned `limit` is an object containing the following properties for the `request` passed to `checkRateLimit`.
|
||||
|
||||
- `isAllowed`: if `true`, the request was excluded from rate limiting according to the configured `allowList`.
|
||||
- `key`: the generated key as returned by the `keyGenerator` function.
|
||||
|
||||
If `isAllowed` is `false` the object also contains these additional properties:
|
||||
|
||||
- `max`: the configured `max` option as a number. If a `max` function was supplied as global option or to `fastify.createRateLimit()`, this property will correspond to the function's return type for the given `request`.
|
||||
- `timeWindow`: the configured `timeWindow` option in milliseconds. If a function was supplied to `timeWindow`, similar to the `max` property above, this property will be equal to the function's return type.
|
||||
- `remaining`: the remaining amount of requests before the limit is exceeded.
|
||||
- `ttl`: the remaining time until the limit will be reset in milliseconds.
|
||||
- `ttlInSeconds`: `ttl` in seconds.
|
||||
- `isExceeded`: `true` if the limit was exceeded.
|
||||
- `isBanned`: `true` if the request was banned according to the `ban` option.
|
||||
|
||||
### Examples of Custom Store
|
||||
|
||||
These examples show an overview of the `store` feature and you should take inspiration from it and tweak as you need:
|
||||
|
||||
- [Knex-SQLite](./example/example-knex.js)
|
||||
- [Knex-MySQL](./example/example-knex-mysql.js)
|
||||
- [Sequelize-PostgreSQL](./example/example-sequelize.js)
|
||||
|
||||
### IETF Draft Spec Headers
|
||||
|
||||
The response will have the following headers if `enableDraftSpec` is `true`:
|
||||
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
|`ratelimit-limit` | how many requests the client can make
|
||||
|`ratelimit-remaining` | how many requests remain to the client in the timewindow
|
||||
|`ratelimit-reset` | how many seconds must pass before the rate limit resets
|
||||
|`retry-after` | contains the same value in time as `ratelimit-reset`
|
||||
|
||||
### Contribute
|
||||
To run tests locally, you need a Redis instance that you can launch with this command:
|
||||
```
|
||||
npm run redis
|
||||
```
|
||||
|
||||
<a name="license"></a>
|
||||
## License
|
||||
|
||||
Licensed under [MIT](./LICENSE).
|
||||
6
backend/node_modules/@fastify/rate-limit/eslint.config.js
generated
vendored
Normal file
6
backend/node_modules/@fastify/rate-limit/eslint.config.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = require('neostandard')({
|
||||
ignores: require('neostandard').resolveIgnoresFromGitignore(),
|
||||
ts: true
|
||||
})
|
||||
116
backend/node_modules/@fastify/rate-limit/example/example-auto-pipeline.mjs
generated
vendored
Normal file
116
backend/node_modules/@fastify/rate-limit/example/example-auto-pipeline.mjs
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
'use strict'
|
||||
|
||||
import Redis from 'ioredis'
|
||||
import Fastify from 'fastify'
|
||||
|
||||
const redis = new Redis({
|
||||
enableAutoPipelining: true,
|
||||
connectionName: 'my-connection-name',
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
connectTimeout: 500,
|
||||
maxRetriesPerRequest: 1
|
||||
})
|
||||
|
||||
const fastify = Fastify()
|
||||
|
||||
await fastify.register(import('../index.js'),
|
||||
{
|
||||
global: false,
|
||||
max: 3000, // default max rate limit
|
||||
// timeWindow: 1000*60,
|
||||
// cache: 10000,
|
||||
allowList: ['127.0.0.2'], // global allowList access ( ACL based on the key from the keyGenerator)
|
||||
redis, // connection to redis
|
||||
skipOnError: false // default false
|
||||
// keyGenerator: function(req) { /* ... */ }, // default (req) => req.raw.ip
|
||||
})
|
||||
|
||||
fastify.get('/', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... root' })
|
||||
})
|
||||
|
||||
fastify.get('/private', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
allowList: ['127.0.2.1', '127.0.3.1'],
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... private' })
|
||||
})
|
||||
|
||||
fastify.get('/public', (_req, reply) => {
|
||||
reply.send({ hello: 'from ... public' })
|
||||
})
|
||||
|
||||
fastify.get('/public/sub-rated-1', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
timeWindow: '1 minute',
|
||||
allowList: ['127.0.2.1'],
|
||||
onExceeding: function () {
|
||||
console.log('callback on exceededing ... executed before response to client. req is give as argument')
|
||||
},
|
||||
onExceeded: function () {
|
||||
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 ... ' })
|
||||
})
|
||||
|
||||
fastify.get('/public/sub-rated-2', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute',
|
||||
onExceeding: function () {
|
||||
console.log('callback on exceededing ... executed before response to client. req is give as argument')
|
||||
},
|
||||
onExceeded: function () {
|
||||
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-2' })
|
||||
})
|
||||
|
||||
fastify.get('/home', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 200,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'toto' })
|
||||
})
|
||||
|
||||
fastify.get('/customerrormessage', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: '1 minute',
|
||||
errorResponseBuilder: (_req, context) => ({ code: 429, timeWindow: context.after, limit: context.max })
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'toto' })
|
||||
})
|
||||
|
||||
fastify.listen({ port: 3000 }, err => {
|
||||
if (err) throw err
|
||||
console.log('Server listening at http://localhost:3000')
|
||||
})
|
||||
124
backend/node_modules/@fastify/rate-limit/example/example-knex-mysql.js
generated
vendored
Normal file
124
backend/node_modules/@fastify/rate-limit/example/example-knex-mysql.js
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
'use strict'
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// Example of a custom store using Knex.js and MySQL.
|
||||
//
|
||||
// Assumes you have access to a configured knex object.
|
||||
//
|
||||
// Note that the rate check should place a read lock on the row.
|
||||
// For MySQL see:
|
||||
// https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html
|
||||
// https://blog.nodeswat.com/concurrency-mysql-and-node-js-a-journey-of-discovery-31281e53572e
|
||||
//
|
||||
// Below is an example table to store rate limits that must be created
|
||||
// in the database first.
|
||||
//
|
||||
// exports.up = async knex => {
|
||||
// await knex.schema.createTable('rate_limits', table => {
|
||||
// table.string('source').notNullable()
|
||||
// table.string('route').notNullable()
|
||||
// table.integer('count').unsigned()
|
||||
// table.bigInteger ('ttl')
|
||||
// table.primary(['route', 'source'])
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// exports.down = async knex => {
|
||||
// await knex.schema.dropTable('rate_limits')
|
||||
// }
|
||||
//
|
||||
// CREATE TABLE `rate_limits` (
|
||||
// `source` varchar(255) NOT NULL,
|
||||
// `route` varchar(255) NOT NULL,
|
||||
// `count` int unsigned DEFAULT NULL,
|
||||
// `ttl` int unsigned DEFAULT NULL,
|
||||
// PRIMARY KEY (`route`,`source`)
|
||||
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
function KnexStore (options) {
|
||||
this.options = options
|
||||
this.route = ''
|
||||
}
|
||||
|
||||
KnexStore.prototype.routeKey = function (route) {
|
||||
if (route) this.route = route
|
||||
return route
|
||||
}
|
||||
|
||||
KnexStore.prototype.incr = async function (key, cb) {
|
||||
const now = (new Date()).getTime()
|
||||
const ttl = now + this.options.timeWindow
|
||||
const max = this.options.max
|
||||
const cond = { route: this.route, source: key }
|
||||
const trx = await knex.transaction()
|
||||
try {
|
||||
// NOTE: MySQL syntax FOR UPDATE for read lock on counter stats in row
|
||||
const row = await trx('rate_limits')
|
||||
.whereRaw('route = ? AND source = ? FOR UPDATE', [cond.route || '', cond.source]) // Create read lock
|
||||
const d = row[0]
|
||||
if (d && d.ttl > now) {
|
||||
// Optimization - no need to UPDATE if max has been reached.
|
||||
if (d.count < max) {
|
||||
await trx
|
||||
.raw('UPDATE rate_limits SET count = ? WHERE route = ? AND source = ?', [d.count + 1, cond.route, key])
|
||||
}
|
||||
// If we were already at max no need to UPDATE but we must still send d.count + 1 to trigger rate limit.
|
||||
process.nextTick(cb, null, { current: d.count + 1, ttl: d.ttl })
|
||||
} else {
|
||||
await trx
|
||||
.raw('INSERT INTO rate_limits(route, source, count, ttl) VALUES(?,?,1,?) ON DUPLICATE KEY UPDATE count = 1, ttl = ?', [cond.route, key, d?.ttl || ttl, ttl])
|
||||
process.nextTick(cb, null, { current: 1, ttl: d?.ttl || ttl })
|
||||
}
|
||||
await trx.commit()
|
||||
} catch (err) {
|
||||
await trx.rollback()
|
||||
// TODO: Handle as desired
|
||||
fastify.log.error(err)
|
||||
process.nextTick(cb, err, { current: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
KnexStore.prototype.child = function (routeOptions = {}) {
|
||||
// NOTE: Optionally override and set global: false here for route specific
|
||||
// options, which then allows you to use `global: true` should you
|
||||
// wish to during initial registration below.
|
||||
const options = { ...this.options, ...routeOptions, global: false }
|
||||
const store = new KnexStore(options)
|
||||
store.routeKey(routeOptions.routeInfo.method + routeOptions.routeInfo.url)
|
||||
return store
|
||||
}
|
||||
|
||||
fastify.register(require('../../fastify-rate-limit'),
|
||||
{
|
||||
global: false,
|
||||
max: 10,
|
||||
store: KnexStore,
|
||||
skipOnError: false
|
||||
}
|
||||
)
|
||||
|
||||
fastify.get('/', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 10,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... root' })
|
||||
})
|
||||
|
||||
fastify.get('/private', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... private' })
|
||||
})
|
||||
|
||||
fastify.get('/public', (_req, reply) => {
|
||||
reply.send({ hello: 'from ... public' })
|
||||
})
|
||||
118
backend/node_modules/@fastify/rate-limit/example/example-knex.js
generated
vendored
Normal file
118
backend/node_modules/@fastify/rate-limit/example/example-knex.js
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
'use strict'
|
||||
|
||||
// Example of a Custom Store using Knex.js ORM for SQLite database
|
||||
// Below is an example table to store rate limits that must be created
|
||||
// in the database first
|
||||
//
|
||||
// CREATE TABLE "RateLimits" (
|
||||
// "Route" TEXT,
|
||||
// "Source" TEXT,
|
||||
// "Count" INTEGER,
|
||||
// "TTL" NUMERIC,
|
||||
// PRIMARY KEY("Source")
|
||||
// );
|
||||
//
|
||||
// CREATE UNIQUE INDEX "idx_uniq_route_source" ON "RateLimits" (Route, Source);
|
||||
//
|
||||
const Knex = require('knex')
|
||||
const fastify = require('fastify')()
|
||||
|
||||
const knex = Knex({
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: './db.sqlite'
|
||||
}
|
||||
})
|
||||
|
||||
function KnexStore (options) {
|
||||
this.options = options
|
||||
this.route = ''
|
||||
}
|
||||
|
||||
KnexStore.prototype.routeKey = function (route) {
|
||||
if (route) {
|
||||
this.route = route
|
||||
} else {
|
||||
return route
|
||||
}
|
||||
}
|
||||
|
||||
KnexStore.prototype.incr = function (key, cb) {
|
||||
const now = (new Date()).getTime()
|
||||
const ttl = now + this.options.timeWindow
|
||||
knex.transaction(function (trx) {
|
||||
trx
|
||||
.where({ Route: this.route, Source: key })
|
||||
.then(d => {
|
||||
if (d.TTL > now) {
|
||||
trx
|
||||
.raw(`UPDATE RateLimits SET Count = 1 WHERE Route='${this.route}' AND Source='${key}'`)
|
||||
.then(() => {
|
||||
cb(null, { current: 1, ttl: d.TTL })
|
||||
})
|
||||
.catch(err => {
|
||||
cb(err, { current: 0 })
|
||||
})
|
||||
} else {
|
||||
trx
|
||||
.raw(`INSERT INTO RateLimits(Route, Source, Count, TTL) VALUES('${this.route}', '${key}',1,${d.TTL || ttl}) ON CONFLICT(Route, Source) DO UPDATE SET Count=Count+1,TTL=${ttl}`)
|
||||
.then(() => {
|
||||
cb(null, { current: d.Count ? d.Count + 1 : 1, ttl: d.TTL || ttl })
|
||||
})
|
||||
.catch(err => {
|
||||
cb(err, { current: 0 })
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
cb(err, { current: 0 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
KnexStore.prototype.child = function (routeOptions) {
|
||||
const options = Object.assign(this.options, routeOptions)
|
||||
const store = new KnexStore(options)
|
||||
store.routeKey(routeOptions.routeInfo.method + routeOptions.routeInfo.url)
|
||||
return store
|
||||
}
|
||||
|
||||
fastify.register(require('../../fastify-rate-limit'),
|
||||
{
|
||||
global: false,
|
||||
max: 10,
|
||||
store: KnexStore,
|
||||
skipOnError: false
|
||||
}
|
||||
)
|
||||
|
||||
fastify.get('/', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 10,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... root' })
|
||||
})
|
||||
|
||||
fastify.get('/private', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... private' })
|
||||
})
|
||||
|
||||
fastify.get('/public', (_req, reply) => {
|
||||
reply.send({ hello: 'from ... public' })
|
||||
})
|
||||
|
||||
fastify.listen({ port: 3000 }, err => {
|
||||
if (err) throw err
|
||||
console.log('Server listening at http://localhost:3000')
|
||||
})
|
||||
185
backend/node_modules/@fastify/rate-limit/example/example-sequelize.js
generated
vendored
Normal file
185
backend/node_modules/@fastify/rate-limit/example/example-sequelize.js
generated
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
'use strict'
|
||||
|
||||
// Example of a Custom Store using Sequelize ORM for PostgreSQL database
|
||||
|
||||
// Sequelize Migration for "RateLimits" table
|
||||
//
|
||||
// module.exports = {
|
||||
// up: (queryInterface, { TEXT, INTEGER, BIGINT }) => {
|
||||
// return queryInterface.createTable(
|
||||
// 'RateLimits',
|
||||
// {
|
||||
// Route: {
|
||||
// type: TEXT,
|
||||
// allowNull: false
|
||||
// },
|
||||
// Source: {
|
||||
// type: TEXT,
|
||||
// allowNull: false,
|
||||
// primaryKey: true
|
||||
// },
|
||||
// Count: {
|
||||
// type: INTEGER,
|
||||
// allowNull: false
|
||||
// },
|
||||
// TTL: {
|
||||
// type: BIGINT,
|
||||
// allowNull: false
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// freezeTableName: true,
|
||||
// timestamps: false,
|
||||
// uniqueKeys: {
|
||||
// unique_tag: {
|
||||
// customIndex: true,
|
||||
// fields: ['Route', 'Source']
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// },
|
||||
// down: queryInterface => {
|
||||
// return queryInterface.dropTable('RateLimits')
|
||||
// }
|
||||
// }
|
||||
|
||||
const fastify = require('fastify')()
|
||||
const Sequelize = require('sequelize')
|
||||
|
||||
const databaseUri = 'postgres://username:password@localhost:5432/fastify-rate-limit-example'
|
||||
const sequelize = new Sequelize(databaseUri)
|
||||
// OR
|
||||
// const sequelize = new Sequelize('database', 'username', 'password');
|
||||
|
||||
// Sequelize Model for "RateLimits" table
|
||||
//
|
||||
const RateLimits = sequelize.define(
|
||||
'RateLimits',
|
||||
{
|
||||
Route: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
Source: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
Count: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
TTL: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
{
|
||||
freezeTableName: true,
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['Route', 'Source']
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
function RateLimiterStore (options) {
|
||||
this.options = options
|
||||
this.route = ''
|
||||
}
|
||||
|
||||
RateLimiterStore.prototype.routeKey = function routeKey (route) {
|
||||
if (route) this.route = route
|
||||
return route
|
||||
}
|
||||
|
||||
RateLimiterStore.prototype.incr = async function incr (key, cb) {
|
||||
const now = new Date().getTime()
|
||||
const ttl = now + this.options.timeWindow
|
||||
const cond = { Route: this.route, Source: key }
|
||||
|
||||
const RateLimit = await RateLimits.findOne({ where: cond })
|
||||
|
||||
if (RateLimit && parseInt(RateLimit.TTL, 10) > now) {
|
||||
try {
|
||||
await RateLimit.update({ Count: RateLimit.Count + 1 }, cond)
|
||||
cb(null, {
|
||||
current: RateLimit.Count + 1,
|
||||
ttl: RateLimit.TTL
|
||||
})
|
||||
} catch (err) {
|
||||
cb(err, {
|
||||
current: 0
|
||||
})
|
||||
}
|
||||
} else {
|
||||
sequelize.query(
|
||||
`INSERT INTO "RateLimits"("Route", "Source", "Count", "TTL")
|
||||
VALUES('${this.route}', '${key}', 1,
|
||||
${RateLimit?.TTL || ttl})
|
||||
ON CONFLICT("Route", "Source") DO UPDATE SET "Count"=1, "TTL"=${ttl}`
|
||||
)
|
||||
.then(() => {
|
||||
cb(null, {
|
||||
current: 1,
|
||||
ttl: RateLimit?.TTL || ttl
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
cb(err, {
|
||||
current: 0
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
RateLimiterStore.prototype.child = function child (routeOptions = {}) {
|
||||
const options = Object.assign(this.options, routeOptions)
|
||||
const store = new RateLimiterStore(options)
|
||||
store.routeKey(routeOptions.routeInfo.method + routeOptions.routeInfo.url)
|
||||
return store
|
||||
}
|
||||
|
||||
fastify.register(require('../../fastify-rate-limit'),
|
||||
{
|
||||
global: false,
|
||||
max: 10,
|
||||
store: RateLimiterStore,
|
||||
skipOnError: false
|
||||
}
|
||||
)
|
||||
|
||||
fastify.get('/', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 10,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... root' })
|
||||
})
|
||||
|
||||
fastify.get('/private', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... private' })
|
||||
})
|
||||
|
||||
fastify.get('/public', (_req, reply) => {
|
||||
reply.send({ hello: 'from ... public' })
|
||||
})
|
||||
|
||||
fastify.listen({ port: 3000 }, err => {
|
||||
if (err) throw err
|
||||
console.log('Server listening at http://localhost:3000')
|
||||
})
|
||||
25
backend/node_modules/@fastify/rate-limit/example/example-simple.mjs
generated
vendored
Normal file
25
backend/node_modules/@fastify/rate-limit/example/example-simple.mjs
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import fastify from 'fastify'
|
||||
import fastifyRateLimit from '../index.js'
|
||||
|
||||
const server = fastify()
|
||||
|
||||
await server.register(fastifyRateLimit, {
|
||||
global: true,
|
||||
max: 10000,
|
||||
timeWindow: '1 minute'
|
||||
})
|
||||
|
||||
server.get('/', (_request, reply) => {
|
||||
reply.send('Hello, world!')
|
||||
})
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
await server.listen({ port: 3000 })
|
||||
console.log('Server is running on port 3000')
|
||||
} catch (error) {
|
||||
console.error('Error starting server:', error)
|
||||
}
|
||||
}
|
||||
|
||||
start()
|
||||
113
backend/node_modules/@fastify/rate-limit/example/example.js
generated
vendored
Normal file
113
backend/node_modules/@fastify/rate-limit/example/example.js
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
'use strict'
|
||||
|
||||
const Redis = require('ioredis')
|
||||
const redis = new Redis({
|
||||
connectionName: 'my-connection-name',
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
connectTimeout: 500,
|
||||
maxRetriesPerRequest: 1
|
||||
})
|
||||
|
||||
const fastify = require('fastify')()
|
||||
|
||||
fastify.register(require('../../fastify-rate-limit'),
|
||||
{
|
||||
global: false,
|
||||
max: 3000, // default max rate limit
|
||||
// timeWindow: 1000*60,
|
||||
// cache: 10000,
|
||||
allowList: ['127.0.0.2'], // global allowList access ( ACL based on the key from the keyGenerator)
|
||||
redis, // connection to redis
|
||||
skipOnError: false // default false
|
||||
// keyGenerator: function(req) { /* ... */ }, // default (req) => req.raw.ip
|
||||
})
|
||||
|
||||
fastify.get('/', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... root' })
|
||||
})
|
||||
|
||||
fastify.get('/private', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
allowList: ['127.0.2.1', '127.0.3.1'],
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'from ... private' })
|
||||
})
|
||||
|
||||
fastify.get('/public', (_req, reply) => {
|
||||
reply.send({ hello: 'from ... public' })
|
||||
})
|
||||
|
||||
fastify.get('/public/sub-rated-1', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
timeWindow: '1 minute',
|
||||
allowList: ['127.0.2.1'],
|
||||
onExceeding: function () {
|
||||
console.log('callback on exceededing ... executed before response to client. req is give as argument')
|
||||
},
|
||||
onExceeded: function () {
|
||||
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 ... ' })
|
||||
})
|
||||
|
||||
fastify.get('/public/sub-rated-2', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 3,
|
||||
timeWindow: '1 minute',
|
||||
onExceeding: function () {
|
||||
console.log('callback on exceededing ... executed before response to client. req is give as argument')
|
||||
},
|
||||
onExceeded: function () {
|
||||
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-2' })
|
||||
})
|
||||
|
||||
fastify.get('/home', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 200,
|
||||
timeWindow: '1 minute'
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'toto' })
|
||||
})
|
||||
|
||||
fastify.get('/customerrormessage', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: '1 minute',
|
||||
errorResponseBuilder: (_req, context) => ({ code: 429, timeWindow: context.after, limit: context.max })
|
||||
}
|
||||
}
|
||||
}, (_req, reply) => {
|
||||
reply.send({ hello: 'toto' })
|
||||
})
|
||||
|
||||
fastify.listen({ port: 3000 }, err => {
|
||||
if (err) throw err
|
||||
console.log('Server listening at http://localhost:3000')
|
||||
})
|
||||
342
backend/node_modules/@fastify/rate-limit/index.js
generated
vendored
Normal file
342
backend/node_modules/@fastify/rate-limit/index.js
generated
vendored
Normal file
@@ -0,0 +1,342 @@
|
||||
'use strict'
|
||||
|
||||
const fp = require('fastify-plugin')
|
||||
const { parse, format } = require('@lukeed/ms')
|
||||
|
||||
const LocalStore = require('./store/LocalStore')
|
||||
const RedisStore = require('./store/RedisStore')
|
||||
|
||||
const defaultMax = 1000
|
||||
const defaultTimeWindow = 60000
|
||||
const defaultHook = 'onRequest'
|
||||
|
||||
const defaultHeaders = {
|
||||
rateLimit: 'x-ratelimit-limit',
|
||||
rateRemaining: 'x-ratelimit-remaining',
|
||||
rateReset: 'x-ratelimit-reset',
|
||||
retryAfter: 'retry-after'
|
||||
}
|
||||
|
||||
const draftSpecHeaders = {
|
||||
rateLimit: 'ratelimit-limit',
|
||||
rateRemaining: 'ratelimit-remaining',
|
||||
rateReset: 'ratelimit-reset',
|
||||
retryAfter: 'retry-after'
|
||||
}
|
||||
|
||||
const defaultOnFn = () => {}
|
||||
|
||||
const defaultKeyGenerator = (req) => req.ip
|
||||
|
||||
const defaultErrorResponse = (_req, context) => {
|
||||
const err = new Error(`Rate limit exceeded, retry in ${context.after}`)
|
||||
err.statusCode = context.statusCode
|
||||
return err
|
||||
}
|
||||
|
||||
async function fastifyRateLimit (fastify, settings) {
|
||||
const globalParams = {
|
||||
global: (typeof settings.global === 'boolean') ? settings.global : true
|
||||
}
|
||||
|
||||
if (typeof settings.enableDraftSpec === 'boolean' && settings.enableDraftSpec) {
|
||||
globalParams.enableDraftSpec = true
|
||||
globalParams.labels = draftSpecHeaders
|
||||
} else {
|
||||
globalParams.enableDraftSpec = false
|
||||
globalParams.labels = defaultHeaders
|
||||
}
|
||||
|
||||
globalParams.addHeaders = Object.assign({
|
||||
[globalParams.labels.rateLimit]: true,
|
||||
[globalParams.labels.rateRemaining]: true,
|
||||
[globalParams.labels.rateReset]: true,
|
||||
[globalParams.labels.retryAfter]: true
|
||||
}, settings.addHeaders)
|
||||
|
||||
globalParams.addHeadersOnExceeding = Object.assign({
|
||||
[globalParams.labels.rateLimit]: true,
|
||||
[globalParams.labels.rateRemaining]: true,
|
||||
[globalParams.labels.rateReset]: true
|
||||
}, settings.addHeadersOnExceeding)
|
||||
|
||||
// Global maximum allowed requests
|
||||
if (Number.isFinite(settings.max) && settings.max >= 0) {
|
||||
globalParams.max = Math.trunc(settings.max)
|
||||
} else if (
|
||||
typeof settings.max === 'function'
|
||||
) {
|
||||
globalParams.max = settings.max
|
||||
} else {
|
||||
globalParams.max = defaultMax
|
||||
}
|
||||
|
||||
// Global time window
|
||||
if (Number.isFinite(settings.timeWindow) && settings.timeWindow >= 0) {
|
||||
globalParams.timeWindow = Math.trunc(settings.timeWindow)
|
||||
} else if (typeof settings.timeWindow === 'string') {
|
||||
globalParams.timeWindow = parse(settings.timeWindow)
|
||||
} else if (
|
||||
typeof settings.timeWindow === 'function'
|
||||
) {
|
||||
globalParams.timeWindow = settings.timeWindow
|
||||
} else {
|
||||
globalParams.timeWindow = defaultTimeWindow
|
||||
}
|
||||
|
||||
globalParams.hook = settings.hook || defaultHook
|
||||
globalParams.allowList = settings.allowList || settings.whitelist || null
|
||||
globalParams.ban = Number.isFinite(settings.ban) && settings.ban >= 0 ? Math.trunc(settings.ban) : -1
|
||||
globalParams.onBanReach = typeof settings.onBanReach === 'function' ? settings.onBanReach : defaultOnFn
|
||||
globalParams.onExceeding = typeof settings.onExceeding === 'function' ? settings.onExceeding : defaultOnFn
|
||||
globalParams.onExceeded = typeof settings.onExceeded === 'function' ? settings.onExceeded : defaultOnFn
|
||||
globalParams.continueExceeding = typeof settings.continueExceeding === 'boolean' ? settings.continueExceeding : false
|
||||
globalParams.exponentialBackoff = typeof settings.exponentialBackoff === 'boolean' ? settings.exponentialBackoff : false
|
||||
|
||||
globalParams.keyGenerator = typeof settings.keyGenerator === 'function'
|
||||
? settings.keyGenerator
|
||||
: defaultKeyGenerator
|
||||
|
||||
if (typeof settings.errorResponseBuilder === 'function') {
|
||||
globalParams.errorResponseBuilder = settings.errorResponseBuilder
|
||||
globalParams.isCustomErrorMessage = true
|
||||
} else {
|
||||
globalParams.errorResponseBuilder = defaultErrorResponse
|
||||
globalParams.isCustomErrorMessage = false
|
||||
}
|
||||
|
||||
globalParams.skipOnError = typeof settings.skipOnError === 'boolean' ? settings.skipOnError : false
|
||||
|
||||
const pluginComponent = {
|
||||
rateLimitRan: Symbol('fastify.request.rateLimitRan'),
|
||||
store: null
|
||||
}
|
||||
|
||||
if (settings.store) {
|
||||
const Store = settings.store
|
||||
pluginComponent.store = new Store(globalParams)
|
||||
} else {
|
||||
if (settings.redis) {
|
||||
pluginComponent.store = new RedisStore(globalParams.continueExceeding, globalParams.exponentialBackoff, settings.redis, settings.nameSpace)
|
||||
} else {
|
||||
pluginComponent.store = new LocalStore(globalParams.continueExceeding, globalParams.exponentialBackoff, settings.cache)
|
||||
}
|
||||
}
|
||||
|
||||
fastify.decorateRequest(pluginComponent.rateLimitRan, false)
|
||||
|
||||
if (!fastify.hasDecorator('createRateLimit')) {
|
||||
fastify.decorate('createRateLimit', (options) => {
|
||||
const args = createLimiterArgs(pluginComponent, globalParams, options)
|
||||
return (req) => applyRateLimit.apply(this, args.concat(req))
|
||||
})
|
||||
}
|
||||
|
||||
if (!fastify.hasDecorator('rateLimit')) {
|
||||
fastify.decorate('rateLimit', (options) => {
|
||||
const args = createLimiterArgs(pluginComponent, globalParams, options)
|
||||
return rateLimitRequestHandler(...args)
|
||||
})
|
||||
}
|
||||
|
||||
fastify.addHook('onRoute', (routeOptions) => {
|
||||
if (routeOptions.config?.rateLimit != null) {
|
||||
if (typeof routeOptions.config.rateLimit === 'object') {
|
||||
const newPluginComponent = Object.create(pluginComponent)
|
||||
const mergedRateLimitParams = mergeParams(globalParams, routeOptions.config.rateLimit, { routeInfo: routeOptions })
|
||||
newPluginComponent.store = pluginComponent.store.child(mergedRateLimitParams)
|
||||
|
||||
addRouteRateHook(newPluginComponent, mergedRateLimitParams, routeOptions)
|
||||
} else if (routeOptions.config.rateLimit !== false) {
|
||||
throw new Error('Unknown value for route rate-limit configuration')
|
||||
}
|
||||
} else if (globalParams.global) {
|
||||
// As the endpoint does not have a custom configuration, use the global one
|
||||
addRouteRateHook(pluginComponent, globalParams, routeOptions)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function mergeParams (...params) {
|
||||
const result = Object.assign({}, ...params)
|
||||
|
||||
if (Number.isFinite(result.timeWindow) && result.timeWindow >= 0) {
|
||||
result.timeWindow = Math.trunc(result.timeWindow)
|
||||
} else if (typeof result.timeWindow === 'string') {
|
||||
result.timeWindow = parse(result.timeWindow)
|
||||
} else if (typeof result.timeWindow !== 'function') {
|
||||
result.timeWindow = defaultTimeWindow
|
||||
}
|
||||
|
||||
if (Number.isFinite(result.max) && result.max >= 0) {
|
||||
result.max = Math.trunc(result.max)
|
||||
} else if (typeof result.max !== 'function') {
|
||||
result.max = defaultMax
|
||||
}
|
||||
|
||||
if (Number.isFinite(result.ban) && result.ban >= 0) {
|
||||
result.ban = Math.trunc(result.ban)
|
||||
} else {
|
||||
result.ban = -1
|
||||
}
|
||||
|
||||
if (result.groupId !== undefined && typeof result.groupId !== 'string') {
|
||||
throw new Error('groupId must be a string')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function createLimiterArgs (pluginComponent, globalParams, options) {
|
||||
if (typeof options === 'object') {
|
||||
const newPluginComponent = Object.create(pluginComponent)
|
||||
const mergedRateLimitParams = mergeParams(globalParams, options, { routeInfo: {} })
|
||||
newPluginComponent.store = newPluginComponent.store.child(mergedRateLimitParams)
|
||||
return [newPluginComponent, mergedRateLimitParams]
|
||||
}
|
||||
|
||||
return [pluginComponent, globalParams]
|
||||
}
|
||||
|
||||
function addRouteRateHook (pluginComponent, params, routeOptions) {
|
||||
const hook = params.hook
|
||||
const hookHandler = rateLimitRequestHandler(pluginComponent, params)
|
||||
if (Array.isArray(routeOptions[hook])) {
|
||||
routeOptions[hook].push(hookHandler)
|
||||
} else if (typeof routeOptions[hook] === 'function') {
|
||||
routeOptions[hook] = [routeOptions[hook], hookHandler]
|
||||
} else {
|
||||
routeOptions[hook] = [hookHandler]
|
||||
}
|
||||
}
|
||||
|
||||
async function applyRateLimit (pluginComponent, params, req) {
|
||||
const { store } = pluginComponent
|
||||
|
||||
// Retrieve the key from the generator (the global one or the one defined in the endpoint)
|
||||
let key = await params.keyGenerator(req)
|
||||
const groupId = req.routeOptions.config?.rateLimit?.groupId
|
||||
|
||||
if (groupId) {
|
||||
key += groupId
|
||||
}
|
||||
|
||||
// Don't apply any rate limiting if in the allow list
|
||||
if (params.allowList) {
|
||||
if (typeof params.allowList === 'function') {
|
||||
if (await params.allowList(req, key)) {
|
||||
return {
|
||||
isAllowed: true,
|
||||
key
|
||||
}
|
||||
}
|
||||
} else if (params.allowList.indexOf(key) !== -1) {
|
||||
return {
|
||||
isAllowed: true,
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const max = typeof params.max === 'number' ? params.max : await params.max(req, key)
|
||||
const timeWindow = typeof params.timeWindow === 'number' ? params.timeWindow : await params.timeWindow(req, key)
|
||||
let current = 0
|
||||
let ttl = 0
|
||||
let ttlInSeconds = 0
|
||||
|
||||
// We increment the rate limit for the current request
|
||||
try {
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
store.incr(key, (err, res) => {
|
||||
err ? reject(err) : resolve(res)
|
||||
}, timeWindow, max)
|
||||
})
|
||||
|
||||
current = res.current
|
||||
ttl = res.ttl
|
||||
ttlInSeconds = Math.ceil(res.ttl / 1000)
|
||||
} catch (err) {
|
||||
if (!params.skipOnError) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isAllowed: false,
|
||||
key,
|
||||
max,
|
||||
timeWindow,
|
||||
remaining: Math.max(0, max - current),
|
||||
ttl,
|
||||
ttlInSeconds,
|
||||
isExceeded: current > max,
|
||||
isBanned: params.ban !== -1 && current - max > params.ban
|
||||
}
|
||||
}
|
||||
|
||||
function rateLimitRequestHandler (pluginComponent, params) {
|
||||
const { rateLimitRan } = pluginComponent
|
||||
|
||||
return async (req, res) => {
|
||||
if (req[rateLimitRan]) {
|
||||
return
|
||||
}
|
||||
|
||||
req[rateLimitRan] = true
|
||||
|
||||
const rateLimit = await applyRateLimit(pluginComponent, params, req)
|
||||
if (rateLimit.isAllowed) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
key,
|
||||
max,
|
||||
remaining,
|
||||
ttl,
|
||||
ttlInSeconds,
|
||||
isExceeded,
|
||||
isBanned
|
||||
} = rateLimit
|
||||
|
||||
if (!isExceeded) {
|
||||
if (params.addHeadersOnExceeding[params.labels.rateLimit]) { res.header(params.labels.rateLimit, max) }
|
||||
if (params.addHeadersOnExceeding[params.labels.rateRemaining]) { res.header(params.labels.rateRemaining, remaining) }
|
||||
if (params.addHeadersOnExceeding[params.labels.rateReset]) { res.header(params.labels.rateReset, ttlInSeconds) }
|
||||
|
||||
params.onExceeding(req, key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
params.onExceeded(req, key)
|
||||
|
||||
if (params.addHeaders[params.labels.rateLimit]) { res.header(params.labels.rateLimit, max) }
|
||||
if (params.addHeaders[params.labels.rateRemaining]) { res.header(params.labels.rateRemaining, 0) }
|
||||
if (params.addHeaders[params.labels.rateReset]) { res.header(params.labels.rateReset, ttlInSeconds) }
|
||||
if (params.addHeaders[params.labels.retryAfter]) { res.header(params.labels.retryAfter, ttlInSeconds) }
|
||||
|
||||
const respCtx = {
|
||||
statusCode: 429,
|
||||
ban: false,
|
||||
max,
|
||||
ttl,
|
||||
after: format(ttlInSeconds * 1000, true)
|
||||
}
|
||||
|
||||
if (isBanned) {
|
||||
respCtx.statusCode = 403
|
||||
respCtx.ban = true
|
||||
params.onBanReach(req, key)
|
||||
}
|
||||
|
||||
throw params.errorResponseBuilder(req, respCtx)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = fp(fastifyRateLimit, {
|
||||
fastify: '5.x',
|
||||
name: '@fastify/rate-limit'
|
||||
})
|
||||
module.exports.default = fastifyRateLimit
|
||||
module.exports.fastifyRateLimit = fastifyRateLimit
|
||||
86
backend/node_modules/@fastify/rate-limit/package.json
generated
vendored
Normal file
86
backend/node_modules/@fastify/rate-limit/package.json
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "@fastify/rate-limit",
|
||||
"version": "10.3.0",
|
||||
"description": "A low overhead rate limiter for your routes",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"types": "types/index.d.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"redis": "docker run -p 6379:6379 --name rate-limit-redis -d --rm redis",
|
||||
"test": "npm run test:unit && npm run test:typescript",
|
||||
"test:unit": "c8 --100 node --test",
|
||||
"test:typescript": "tsd"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/fastify/fastify-rate-limit.git"
|
||||
},
|
||||
"keywords": [
|
||||
"fastify",
|
||||
"rate",
|
||||
"limit"
|
||||
],
|
||||
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Matteo Collina",
|
||||
"email": "hello@matteocollina.com"
|
||||
},
|
||||
{
|
||||
"name": "Manuel Spigolon",
|
||||
"email": "behemoth89@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Gürgün Dayıoğlu",
|
||||
"email": "hey@gurgun.day",
|
||||
"url": "https://heyhey.to/G"
|
||||
},
|
||||
{
|
||||
"name": "Frazer Smith",
|
||||
"email": "frazer.dev@icloud.com",
|
||||
"url": "https://github.com/fdawgs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/fastify/fastify-rate-limit/issues"
|
||||
},
|
||||
"homepage": "https://github.com/fastify/fastify-rate-limit#readme",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"@fastify/pre-commit": "^2.1.0",
|
||||
"@sinonjs/fake-timers": "^14.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"c8": "^10.1.2",
|
||||
"eslint": "^9.17.0",
|
||||
"fastify": "^5.0.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"knex": "^3.1.0",
|
||||
"neostandard": "^0.12.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tsd": "^0.32.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lukeed/ms": "^2.0.2",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"toad-cache": "^3.7.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
50
backend/node_modules/@fastify/rate-limit/store/LocalStore.js
generated
vendored
Normal file
50
backend/node_modules/@fastify/rate-limit/store/LocalStore.js
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
'use strict'
|
||||
|
||||
const { LruMap: Lru } = require('toad-cache')
|
||||
|
||||
function LocalStore (continueExceeding, exponentialBackoff, cache = 5000) {
|
||||
this.continueExceeding = continueExceeding
|
||||
this.exponentialBackoff = exponentialBackoff
|
||||
this.lru = new Lru(cache)
|
||||
}
|
||||
|
||||
LocalStore.prototype.incr = function (ip, cb, timeWindow, max) {
|
||||
const nowInMs = Date.now()
|
||||
let current = this.lru.get(ip)
|
||||
|
||||
if (!current) {
|
||||
// Item doesn't exist
|
||||
current = { current: 1, ttl: timeWindow, iterationStartMs: nowInMs }
|
||||
} else if (current.iterationStartMs + timeWindow <= nowInMs) {
|
||||
// Item has expired
|
||||
current.current = 1
|
||||
current.ttl = timeWindow
|
||||
current.iterationStartMs = nowInMs
|
||||
} else {
|
||||
// Item is alive
|
||||
++current.current
|
||||
|
||||
// Reset TLL if max has been exceeded and `continueExceeding` is enabled
|
||||
if (this.continueExceeding && current.current > max) {
|
||||
current.ttl = timeWindow
|
||||
current.iterationStartMs = nowInMs
|
||||
} else if (this.exponentialBackoff && current.current > max) {
|
||||
// Handle exponential backoff
|
||||
const backoffExponent = current.current - max - 1
|
||||
const ttl = timeWindow * (2 ** backoffExponent)
|
||||
current.ttl = Number.isSafeInteger(ttl) ? ttl : Number.MAX_SAFE_INTEGER
|
||||
current.iterationStartMs = nowInMs
|
||||
} else {
|
||||
current.ttl = timeWindow - (nowInMs - current.iterationStartMs)
|
||||
}
|
||||
}
|
||||
|
||||
this.lru.set(ip, current)
|
||||
cb(null, current)
|
||||
}
|
||||
|
||||
LocalStore.prototype.child = function (routeOptions) {
|
||||
return new LocalStore(routeOptions.continueExceeding, routeOptions.exponentialBackoff, routeOptions.cache)
|
||||
}
|
||||
|
||||
module.exports = LocalStore
|
||||
58
backend/node_modules/@fastify/rate-limit/store/RedisStore.js
generated
vendored
Normal file
58
backend/node_modules/@fastify/rate-limit/store/RedisStore.js
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
'use strict'
|
||||
|
||||
const lua = `
|
||||
-- Key to operate on
|
||||
local key = KEYS[1]
|
||||
-- Time window for the TTL
|
||||
local timeWindow = tonumber(ARGV[1])
|
||||
-- Max requests
|
||||
local max = tonumber(ARGV[2])
|
||||
-- Flag to determine if TTL should be reset after exceeding
|
||||
local continueExceeding = ARGV[3] == 'true'
|
||||
--Flag to determine if exponential backoff should be applied
|
||||
local exponentialBackoff = ARGV[4] == 'true'
|
||||
|
||||
--Max safe integer
|
||||
local MAX_SAFE_INTEGER = (2^53) - 1
|
||||
|
||||
-- Increment the key's value
|
||||
local current = redis.call('INCR', key)
|
||||
|
||||
if current == 1 or (continueExceeding and current > max) then
|
||||
redis.call('PEXPIRE', key, timeWindow)
|
||||
elseif exponentialBackoff and current > max then
|
||||
local backoffExponent = current - max - 1
|
||||
timeWindow = math.min(timeWindow * (2 ^ backoffExponent), MAX_SAFE_INTEGER)
|
||||
redis.call('PEXPIRE', key, timeWindow)
|
||||
else
|
||||
timeWindow = redis.call('PTTL', key)
|
||||
end
|
||||
|
||||
return {current, timeWindow}
|
||||
`
|
||||
|
||||
function RedisStore (continueExceeding, exponentialBackoff, redis, key = 'fastify-rate-limit-') {
|
||||
this.continueExceeding = continueExceeding
|
||||
this.exponentialBackoff = exponentialBackoff
|
||||
this.redis = redis
|
||||
this.key = key
|
||||
|
||||
if (!this.redis.rateLimit) {
|
||||
this.redis.defineCommand('rateLimit', {
|
||||
numberOfKeys: 1,
|
||||
lua
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
RedisStore.prototype.incr = function (ip, cb, timeWindow, max) {
|
||||
this.redis.rateLimit(this.key + ip, timeWindow, max, this.continueExceeding, this.exponentialBackoff, (err, result) => {
|
||||
err ? cb(err, null) : cb(null, { current: result[0], ttl: result[1] })
|
||||
})
|
||||
}
|
||||
|
||||
RedisStore.prototype.child = function (routeOptions) {
|
||||
return new RedisStore(routeOptions.continueExceeding, routeOptions.exponentialBackoff, this.redis, `${this.key}${routeOptions.routeInfo.method}${routeOptions.routeInfo.url}-`)
|
||||
}
|
||||
|
||||
module.exports = RedisStore
|
||||
224
backend/node_modules/@fastify/rate-limit/test/create-rate-limit.test.js
generated
vendored
Normal file
224
backend/node_modules/@fastify/rate-limit/test/create-rate-limit.test.js
generated
vendored
Normal file
@@ -0,0 +1,224 @@
|
||||
'use strict'
|
||||
|
||||
const { test, mock } = require('node:test')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../index')
|
||||
|
||||
test('With global rate limit options', async t => {
|
||||
t.plan(8)
|
||||
const clock = mock.timers
|
||||
clock.enable(0)
|
||||
const fastify = Fastify()
|
||||
await fastify.register(rateLimit, {
|
||||
global: false,
|
||||
max: 2,
|
||||
timeWindow: 1000
|
||||
})
|
||||
|
||||
const checkRateLimit = fastify.createRateLimit()
|
||||
|
||||
fastify.get('/', async (req, reply) => {
|
||||
const limit = await checkRateLimit(req)
|
||||
return limit
|
||||
})
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 1,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: false,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 0,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: false,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 0,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: true,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
clock.tick(1100)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 1,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: false,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
clock.reset()
|
||||
})
|
||||
|
||||
test('With custom rate limit options', async t => {
|
||||
t.plan(10)
|
||||
const clock = mock.timers
|
||||
clock.enable(0)
|
||||
const fastify = Fastify()
|
||||
await fastify.register(rateLimit, {
|
||||
global: false,
|
||||
max: 5,
|
||||
timeWindow: 1000
|
||||
})
|
||||
|
||||
const checkRateLimit = fastify.createRateLimit({
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
ban: 1
|
||||
})
|
||||
|
||||
fastify.get('/', async (req, reply) => {
|
||||
const limit = await checkRateLimit(req)
|
||||
return limit
|
||||
})
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 1,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: false,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 0,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: false,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
// should be exceeded now
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 0,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: true,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
// should be banned now
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 0,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: true,
|
||||
isBanned: true
|
||||
})
|
||||
|
||||
clock.tick(1100)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: false,
|
||||
key: '127.0.0.1',
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
remaining: 1,
|
||||
ttl: 1000,
|
||||
ttlInSeconds: 1,
|
||||
isExceeded: false,
|
||||
isBanned: false
|
||||
})
|
||||
|
||||
clock.reset()
|
||||
})
|
||||
|
||||
test('With allow list', async t => {
|
||||
t.plan(2)
|
||||
const clock = mock.timers
|
||||
clock.enable(0)
|
||||
const fastify = Fastify()
|
||||
await fastify.register(rateLimit, {
|
||||
global: false,
|
||||
max: 5,
|
||||
timeWindow: 1000
|
||||
})
|
||||
|
||||
const checkRateLimit = fastify.createRateLimit({
|
||||
allowList: ['127.0.0.1'],
|
||||
max: 2,
|
||||
timeWindow: 1000
|
||||
})
|
||||
|
||||
fastify.get('/', async (req, reply) => {
|
||||
const limit = await checkRateLimit(req)
|
||||
return limit
|
||||
})
|
||||
|
||||
const res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
|
||||
// expect a different return type because isAllowed is true
|
||||
t.assert.deepStrictEqual(res.json(), {
|
||||
isAllowed: true,
|
||||
key: '127.0.0.1'
|
||||
})
|
||||
})
|
||||
232
backend/node_modules/@fastify/rate-limit/test/exponential-backoff.test.js
generated
vendored
Normal file
232
backend/node_modules/@fastify/rate-limit/test/exponential-backoff.test.js
generated
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
'use strict'
|
||||
const { test } = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../index')
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
test('Exponential Backoff', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin with exponentialBackoff set to true in routeConfig
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
|
||||
|
||||
fastify.get(
|
||||
'/expoential-backoff',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 500,
|
||||
exponentialBackoff: true
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'exponential backoff applied!'
|
||||
)
|
||||
|
||||
// Test
|
||||
const res = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
const res2 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
|
||||
assert.deepStrictEqual(res2.statusCode, 200)
|
||||
assert.deepStrictEqual(res2.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res2.headers['x-ratelimit-remaining'], '0')
|
||||
|
||||
const res3 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
|
||||
assert.deepStrictEqual(res3.statusCode, 429)
|
||||
assert.deepStrictEqual(res3.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res3.headers['x-ratelimit-remaining'], '0')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res3.payload)
|
||||
)
|
||||
|
||||
const res4 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
|
||||
assert.deepStrictEqual(res4.statusCode, 429)
|
||||
assert.deepStrictEqual(res4.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res4.headers['x-ratelimit-remaining'], '0')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res4.payload)
|
||||
)
|
||||
|
||||
// Wait for the window to reset
|
||||
await sleep(1000)
|
||||
const res5 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
|
||||
assert.deepStrictEqual(res5.statusCode, 200)
|
||||
assert.deepStrictEqual(res5.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res5.headers['x-ratelimit-remaining'], '1')
|
||||
})
|
||||
|
||||
test('Global Exponential Backoff', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin with exponentialBackoff set to true in routeConfig
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 500, exponentialBackoff: true })
|
||||
|
||||
fastify.get(
|
||||
'/expoential-backoff-global',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 500
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'exponential backoff applied!'
|
||||
)
|
||||
|
||||
// Test
|
||||
let res
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 2 seconds'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 4 seconds'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
})
|
||||
|
||||
test('MAx safe Exponential Backoff', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin with exponentialBackoff set to true in routeConfig
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 500, exponentialBackoff: true })
|
||||
|
||||
fastify.get(
|
||||
'/expoential-backoff-global',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: '285421 years'
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'exponential backoff applied!'
|
||||
)
|
||||
|
||||
// Test
|
||||
let res
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 285421 years'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 285421 years'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 285421 years'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 285421 years'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
})
|
||||
120
backend/node_modules/@fastify/rate-limit/test/github-issues/issue-207.test.js
generated
vendored
Normal file
120
backend/node_modules/@fastify/rate-limit/test/github-issues/issue-207.test.js
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
'use strict'
|
||||
|
||||
const { test, mock } = require('node:test')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../../index')
|
||||
|
||||
test('issue #207 - when continueExceeding is true and the store is local then it should reset the rate-limit', async (t) => {
|
||||
const clock = mock.timers
|
||||
clock.enable()
|
||||
const fastify = Fastify()
|
||||
|
||||
await fastify.register(rateLimit, {
|
||||
global: false
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 1,
|
||||
timeWindow: 5000,
|
||||
continueExceeding: true
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
return 'hello!'
|
||||
}
|
||||
)
|
||||
|
||||
const firstOkResponse = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
const firstRateLimitResponse = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
clock.tick(3000)
|
||||
|
||||
const secondRateLimitWithResettingTheRateLimitTimer = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
// after this the total time passed is 6s which WITHOUT `continueExceeding` the next request should be OK
|
||||
clock.tick(3000)
|
||||
|
||||
const thirdRateLimitWithResettingTheRateLimitTimer = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
// After this the rate limiter should allow for new requests
|
||||
clock.tick(5000)
|
||||
|
||||
const okResponseAfterRateLimitCompleted = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
t.assert.deepStrictEqual(firstOkResponse.statusCode, 200)
|
||||
|
||||
t.assert.deepStrictEqual(firstRateLimitResponse.statusCode, 429)
|
||||
t.assert.deepStrictEqual(
|
||||
firstRateLimitResponse.headers['x-ratelimit-limit'],
|
||||
'1'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
firstRateLimitResponse.headers['x-ratelimit-remaining'],
|
||||
'0'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
firstRateLimitResponse.headers['x-ratelimit-reset'],
|
||||
'5'
|
||||
)
|
||||
|
||||
t.assert.deepStrictEqual(
|
||||
secondRateLimitWithResettingTheRateLimitTimer.statusCode,
|
||||
429
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'],
|
||||
'1'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
secondRateLimitWithResettingTheRateLimitTimer.headers[
|
||||
'x-ratelimit-remaining'
|
||||
],
|
||||
'0'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'],
|
||||
'5'
|
||||
)
|
||||
|
||||
t.assert.deepStrictEqual(
|
||||
thirdRateLimitWithResettingTheRateLimitTimer.statusCode,
|
||||
429
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'],
|
||||
'1'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
thirdRateLimitWithResettingTheRateLimitTimer.headers[
|
||||
'x-ratelimit-remaining'
|
||||
],
|
||||
'0'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'],
|
||||
'5'
|
||||
)
|
||||
|
||||
t.assert.deepStrictEqual(okResponseAfterRateLimitCompleted.statusCode, 200)
|
||||
clock.reset(0)
|
||||
})
|
||||
87
backend/node_modules/@fastify/rate-limit/test/github-issues/issue-215.test.js
generated
vendored
Normal file
87
backend/node_modules/@fastify/rate-limit/test/github-issues/issue-215.test.js
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
'use strict'
|
||||
|
||||
const { test, mock } = require('node:test')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../../index')
|
||||
|
||||
test('issue #215 - when using local store, 2nd user should not be rate limited when the time window is passed for the 1st user', async (t) => {
|
||||
t.plan(5)
|
||||
const clock = mock.timers
|
||||
clock.enable()
|
||||
const fastify = Fastify()
|
||||
|
||||
await fastify.register(rateLimit, {
|
||||
global: false
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 1,
|
||||
timeWindow: 5000,
|
||||
continueExceeding: false
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello!'
|
||||
)
|
||||
|
||||
const user1FirstRequest = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET',
|
||||
remoteAddress: '1.1.1.1'
|
||||
})
|
||||
|
||||
// Waiting for the time to pass to make the 2nd user start in a different start point
|
||||
clock.tick(3000)
|
||||
|
||||
const user2FirstRequest = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET',
|
||||
remoteAddress: '2.2.2.2'
|
||||
})
|
||||
|
||||
const user2SecondRequestAndShouldBeRateLimited = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET',
|
||||
remoteAddress: '2.2.2.2'
|
||||
})
|
||||
|
||||
// After this the total time passed for the 1st user is 6s and for the 2nd user only 3s
|
||||
clock.tick(3000)
|
||||
|
||||
const user2ThirdRequestAndShouldStillBeRateLimited = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET',
|
||||
remoteAddress: '2.2.2.2'
|
||||
})
|
||||
|
||||
// After this the total time passed for the 2nd user is 5.1s - he should not be rate limited
|
||||
clock.tick(2100)
|
||||
|
||||
const user2OkResponseAfterRateLimitCompleted = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET',
|
||||
remoteAddress: '2.2.2.2'
|
||||
})
|
||||
|
||||
t.assert.deepStrictEqual(user1FirstRequest.statusCode, 200)
|
||||
t.assert.deepStrictEqual(user2FirstRequest.statusCode, 200)
|
||||
|
||||
t.assert.deepStrictEqual(
|
||||
user2SecondRequestAndShouldBeRateLimited.statusCode,
|
||||
429
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
user2ThirdRequestAndShouldStillBeRateLimited.statusCode,
|
||||
429
|
||||
)
|
||||
|
||||
t.assert.deepStrictEqual(
|
||||
user2OkResponseAfterRateLimitCompleted.statusCode,
|
||||
200
|
||||
)
|
||||
clock.reset()
|
||||
})
|
||||
74
backend/node_modules/@fastify/rate-limit/test/github-issues/issue-284.test.js
generated
vendored
Normal file
74
backend/node_modules/@fastify/rate-limit/test/github-issues/issue-284.test.js
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
'use strict'
|
||||
|
||||
const { test, mock } = require('node:test')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../../index')
|
||||
|
||||
test("issue #284 - don't set the reply code automatically", async (t) => {
|
||||
const clock = mock.timers
|
||||
clock.enable()
|
||||
const fastify = Fastify()
|
||||
|
||||
await fastify.register(rateLimit, {
|
||||
global: false
|
||||
})
|
||||
|
||||
fastify.setErrorHandler((err, _req, res) => {
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(err.statusCode, 429)
|
||||
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 1,
|
||||
timeWindow: 5000,
|
||||
continueExceeding: true
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
return 'hello!'
|
||||
}
|
||||
)
|
||||
|
||||
const firstOkResponse = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
const firstRateLimitResponse = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
// After this the rate limiter should allow for new requests
|
||||
clock.tick(5000)
|
||||
|
||||
const okResponseAfterRateLimitCompleted = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
t.assert.deepStrictEqual(firstOkResponse.statusCode, 200)
|
||||
|
||||
t.assert.deepStrictEqual(firstRateLimitResponse.statusCode, 302)
|
||||
t.assert.deepStrictEqual(
|
||||
firstRateLimitResponse.headers['x-ratelimit-limit'],
|
||||
'1'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
firstRateLimitResponse.headers['x-ratelimit-remaining'],
|
||||
'0'
|
||||
)
|
||||
t.assert.deepStrictEqual(
|
||||
firstRateLimitResponse.headers['x-ratelimit-reset'],
|
||||
'5'
|
||||
)
|
||||
|
||||
t.assert.deepStrictEqual(okResponseAfterRateLimitCompleted.statusCode, 200)
|
||||
clock.reset(0)
|
||||
})
|
||||
1451
backend/node_modules/@fastify/rate-limit/test/global-rate-limit.test.js
generated
vendored
Normal file
1451
backend/node_modules/@fastify/rate-limit/test/global-rate-limit.test.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
252
backend/node_modules/@fastify/rate-limit/test/group-rate-limit.test.js
generated
vendored
Normal file
252
backend/node_modules/@fastify/rate-limit/test/group-rate-limit.test.js
generated
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
'use strict'
|
||||
const { test } = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../index')
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
test('GroupId from routeConfig', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin with groupId in routeConfig
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
|
||||
|
||||
fastify.get(
|
||||
'/routeWithGroupId',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 500,
|
||||
groupId: 'group1' // groupId specified in routeConfig
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello from route with groupId!'
|
||||
)
|
||||
|
||||
// Test: Request should have the correct groupId in response
|
||||
const res = await fastify.inject({ url: '/routeWithGroupId', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
})
|
||||
|
||||
test('GroupId from routeOptions', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin with groupId in routeOptions
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
|
||||
|
||||
fastify.get(
|
||||
'/routeWithGroupIdFromOptions',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 500
|
||||
// groupId not specified here
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello from route with groupId from options!'
|
||||
)
|
||||
|
||||
// Test: Request should have the correct groupId from routeOptions
|
||||
const res = await fastify.inject({ url: '/routeWithGroupIdFromOptions', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
})
|
||||
|
||||
test('No groupId provided', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin without groupId
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
|
||||
|
||||
// Route without groupId
|
||||
fastify.get(
|
||||
'/noGroupId',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 500
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello from no groupId route!'
|
||||
)
|
||||
|
||||
let res
|
||||
|
||||
// Test without groupId
|
||||
res = await fastify.inject({ url: '/noGroupId', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
res = await fastify.inject({ url: '/noGroupId', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
|
||||
res = await fastify.inject({ url: '/noGroupId', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
})
|
||||
|
||||
test('With multiple routes and custom groupId', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
|
||||
|
||||
// Route 1 with groupId 'group1'
|
||||
fastify.get(
|
||||
'/route1',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 500,
|
||||
groupId: 'group1'
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello from route 1!'
|
||||
)
|
||||
|
||||
// Route 2 with groupId 'group2'
|
||||
fastify.get(
|
||||
'/route2',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
groupId: 'group2'
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello from route 2!'
|
||||
)
|
||||
|
||||
let res
|
||||
|
||||
// Test Route 1
|
||||
res = await fastify.inject({ url: '/route1', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
res = await fastify.inject({ url: '/route1', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
|
||||
res = await fastify.inject({ url: '/route1', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Test Route 2
|
||||
res = await fastify.inject({ url: '/route2', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
res = await fastify.inject({ url: '/route2', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
|
||||
res = await fastify.inject({ url: '/route2', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 429)
|
||||
assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Wait for the window to reset
|
||||
await sleep(1000)
|
||||
|
||||
// After reset, Route 1 should succeed again
|
||||
res = await fastify.inject({ url: '/route1', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
// Route 2 should also succeed after the reset
|
||||
res = await fastify.inject({ url: '/route2', method: 'GET' })
|
||||
assert.deepStrictEqual(res.statusCode, 200)
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
})
|
||||
|
||||
test('Invalid groupId type', async () => {
|
||||
const fastify = Fastify()
|
||||
|
||||
// Register rate limit plugin with a route having an invalid groupId
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
|
||||
|
||||
try {
|
||||
fastify.get(
|
||||
'/invalidGroupId',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
groupId: 123 // Invalid groupId type
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello with invalid groupId!'
|
||||
)
|
||||
assert.fail('should throw')
|
||||
console.log('HER')
|
||||
} catch (err) {
|
||||
assert.deepStrictEqual(err.message, 'groupId must be a string')
|
||||
}
|
||||
})
|
||||
18
backend/node_modules/@fastify/rate-limit/test/local-store-close.test.js
generated
vendored
Normal file
18
backend/node_modules/@fastify/rate-limit/test/local-store-close.test.js
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('node:test')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../index')
|
||||
|
||||
test('Fastify close on local store', async (t) => {
|
||||
t.plan(1)
|
||||
const fastify = Fastify()
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
|
||||
let counter = 1
|
||||
fastify.addHook('onClose', (_instance, done) => {
|
||||
counter++
|
||||
done()
|
||||
})
|
||||
await fastify.close()
|
||||
t.assert.deepStrictEqual(counter, 2)
|
||||
})
|
||||
116
backend/node_modules/@fastify/rate-limit/test/not-found-handler-rate-limited.test.js
generated
vendored
Normal file
116
backend/node_modules/@fastify/rate-limit/test/not-found-handler-rate-limited.test.js
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('node:test')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../index')
|
||||
|
||||
test('Set not found handler can be rate limited', async (t) => {
|
||||
t.plan(18)
|
||||
|
||||
const fastify = Fastify()
|
||||
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
|
||||
t.assert.ok(fastify.rateLimit)
|
||||
|
||||
fastify.setNotFoundHandler(
|
||||
{
|
||||
preHandler: fastify.rateLimit()
|
||||
},
|
||||
function (_request, reply) {
|
||||
t.assert.ok('Error handler has been called')
|
||||
reply.status(404).send(new Error('Not found'))
|
||||
}
|
||||
)
|
||||
|
||||
let res
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 404)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 404)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
t.assert.deepStrictEqual(JSON.parse(res.payload), {
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
})
|
||||
})
|
||||
|
||||
test('Set not found handler can be rate limited with specific options', async (t) => {
|
||||
t.plan(28)
|
||||
|
||||
const fastify = Fastify()
|
||||
|
||||
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
|
||||
t.assert.ok(fastify.rateLimit)
|
||||
|
||||
fastify.setNotFoundHandler(
|
||||
{
|
||||
preHandler: fastify.rateLimit({
|
||||
max: 4,
|
||||
timeWindow: 2000
|
||||
})
|
||||
},
|
||||
function (_request, reply) {
|
||||
t.assert.ok('Error handler has been called')
|
||||
reply.status(404).send(new Error('Not found'))
|
||||
}
|
||||
)
|
||||
|
||||
let res
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 404)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '3')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
|
||||
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 404)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
|
||||
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 404)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
|
||||
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 404)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
|
||||
|
||||
res = await fastify.inject('/not-found')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
|
||||
t.assert.deepStrictEqual(JSON.parse(res.payload), {
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 2 seconds'
|
||||
})
|
||||
})
|
||||
753
backend/node_modules/@fastify/rate-limit/test/redis-rate-limit.test.js
generated
vendored
Normal file
753
backend/node_modules/@fastify/rate-limit/test/redis-rate-limit.test.js
generated
vendored
Normal file
@@ -0,0 +1,753 @@
|
||||
'use strict'
|
||||
|
||||
const { test, describe } = require('node:test')
|
||||
const Redis = require('ioredis')
|
||||
const Fastify = require('fastify')
|
||||
const rateLimit = require('../index')
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const REDIS_HOST = '127.0.0.1'
|
||||
|
||||
describe('Global rate limit', () => {
|
||||
test('With redis store', async (t) => {
|
||||
t.plan(21)
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
redis
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.strictEqual(res.statusCode, 200)
|
||||
t.assert.ok(res)
|
||||
t.assert.strictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
await sleep(100)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Not using fake timers here as we use an external Redis that would not be effected by this
|
||||
await sleep(1100)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('With redis store (ban)', async (t) => {
|
||||
t.plan(19)
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
max: 1,
|
||||
ban: 1,
|
||||
timeWindow: 1000,
|
||||
redis
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 403)
|
||||
t.assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Not using fake timers here as we use an external Redis that would not be effected by this
|
||||
await sleep(1100)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('Skip on redis error', async (t) => {
|
||||
t.plan(9)
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
redis,
|
||||
skipOnError: true
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
|
||||
})
|
||||
|
||||
test('Throw on redis error', async (t) => {
|
||||
t.plan(5)
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
redis,
|
||||
skipOnError: false
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 500)
|
||||
t.assert.deepStrictEqual(
|
||||
res.body,
|
||||
'{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}'
|
||||
)
|
||||
})
|
||||
|
||||
test('When continue exceeding is on (Redis)', async (t) => {
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
|
||||
await fastify.register(rateLimit, {
|
||||
redis,
|
||||
max: 1,
|
||||
timeWindow: 5000,
|
||||
continueExceeding: true
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
const first = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
const second = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
t.assert.deepStrictEqual(first.statusCode, 200)
|
||||
|
||||
t.assert.deepStrictEqual(second.statusCode, 429)
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('Redis with continueExceeding should not always return the timeWindow as ttl', async (t) => {
|
||||
t.plan(19)
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
max: 2,
|
||||
timeWindow: 3000,
|
||||
continueExceeding: true,
|
||||
redis
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3')
|
||||
|
||||
// After this sleep, we should not see `x-ratelimit-reset === 3` anymore
|
||||
await sleep(1000)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '3')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 3 seconds'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Not using fake timers here as we use an external Redis that would not be effected by this
|
||||
await sleep(1000)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('When use a custom nameSpace', async (t) => {
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
|
||||
await fastify.register(rateLimit, {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
redis,
|
||||
nameSpace: 'my-namespace:',
|
||||
keyGenerator: (req) => req.headers['x-my-header']
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
const allowListHeader = {
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
'x-my-header': 'custom name space'
|
||||
}
|
||||
}
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject(allowListHeader)
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject(allowListHeader)
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject(allowListHeader)
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Not using fake timers here as we use an external Redis that would not be effected by this
|
||||
await sleep(1100)
|
||||
|
||||
res = await fastify.inject(allowListHeader)
|
||||
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('With redis store and exponential backoff', async (t) => {
|
||||
t.plan(20)
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
redis,
|
||||
exponentialBackoff: true
|
||||
})
|
||||
|
||||
fastify.get('/', async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
// First attempt over the limit should have the normal timeWindow (1000ms)
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(
|
||||
res.headers['content-type'],
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Second attempt over the limit should have doubled timeWindow (2000ms)
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '2')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 2 seconds'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Route rate limit', () => {
|
||||
test('With redis store', async t => {
|
||||
t.plan(19)
|
||||
const fastify = Fastify()
|
||||
const redis = new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
global: false,
|
||||
redis
|
||||
})
|
||||
|
||||
fastify.get('/', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 1000
|
||||
},
|
||||
someOtherPlugin: {
|
||||
someValue: 1
|
||||
}
|
||||
}
|
||||
}, async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.strictEqual(res.statusCode, 200)
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.strictEqual(res.statusCode, 200)
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.strictEqual(res.statusCode, 429)
|
||||
t.assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
t.assert.strictEqual(res.headers['retry-after'], '1')
|
||||
t.assert.deepStrictEqual({
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
}, JSON.parse(res.payload))
|
||||
|
||||
// Not using fake timers here as we use an external Redis that would not be effected by this
|
||||
await sleep(1100)
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.strictEqual(res.statusCode, 200)
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('Throw on redis error', async (t) => {
|
||||
t.plan(6)
|
||||
const fastify = Fastify()
|
||||
const redis = new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
redis,
|
||||
global: false
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
skipOnError: false
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello!'
|
||||
)
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 500)
|
||||
t.assert.deepStrictEqual(
|
||||
res.body,
|
||||
'{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}'
|
||||
)
|
||||
})
|
||||
|
||||
test('Skip on redis error', async (t) => {
|
||||
t.plan(9)
|
||||
const fastify = Fastify()
|
||||
const redis = new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
redis,
|
||||
global: false
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 2,
|
||||
timeWindow: 1000,
|
||||
skipOnError: true
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello!'
|
||||
)
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
|
||||
})
|
||||
|
||||
test('When continue exceeding is on (Redis)', async (t) => {
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
|
||||
await fastify.register(rateLimit, {
|
||||
global: false,
|
||||
redis
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
timeWindow: 5000,
|
||||
max: 1,
|
||||
continueExceeding: true
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello!'
|
||||
)
|
||||
|
||||
const first = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
const second = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
t.assert.deepStrictEqual(first.statusCode, 200)
|
||||
|
||||
t.assert.deepStrictEqual(second.statusCode, 429)
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('When continue exceeding is off under route (Redis)', async (t) => {
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
|
||||
await fastify.register(rateLimit, {
|
||||
global: false,
|
||||
continueExceeding: true,
|
||||
redis
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
config: {
|
||||
rateLimit: {
|
||||
timeWindow: 5000,
|
||||
max: 1,
|
||||
continueExceeding: false
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => 'hello!'
|
||||
)
|
||||
|
||||
const first = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
const second = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
await sleep(2000)
|
||||
|
||||
const third = await fastify.inject({
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
t.assert.deepStrictEqual(first.statusCode, 200)
|
||||
|
||||
t.assert.deepStrictEqual(second.statusCode, 429)
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5')
|
||||
|
||||
t.assert.deepStrictEqual(third.statusCode, 429)
|
||||
t.assert.deepStrictEqual(third.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(third.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(third.headers['x-ratelimit-reset'], '3')
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
|
||||
test('Route-specific exponential backoff with redis store', async (t) => {
|
||||
t.plan(17)
|
||||
const fastify = Fastify()
|
||||
const redis = await new Redis({ host: REDIS_HOST })
|
||||
await fastify.register(rateLimit, {
|
||||
global: false,
|
||||
redis
|
||||
})
|
||||
|
||||
fastify.get('/', {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: 1,
|
||||
timeWindow: 1000,
|
||||
exponentialBackoff: true
|
||||
}
|
||||
}
|
||||
}, async () => 'hello!')
|
||||
|
||||
let res
|
||||
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 200)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
|
||||
|
||||
// First attempt over the limit should have the normal timeWindow (1000ms)
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 1 second'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Second attempt over the limit should have doubled timeWindow (2000ms)
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
||||
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '2')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 2 seconds'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
// Third attempt over the limit should have quadrupled timeWindow (4000ms)
|
||||
res = await fastify.inject('/')
|
||||
t.assert.deepStrictEqual(res.statusCode, 429)
|
||||
t.assert.deepStrictEqual(res.headers['retry-after'], '4')
|
||||
t.assert.deepStrictEqual(
|
||||
{
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, retry in 4 seconds'
|
||||
},
|
||||
JSON.parse(res.payload)
|
||||
)
|
||||
|
||||
await redis.flushall()
|
||||
await redis.quit()
|
||||
})
|
||||
})
|
||||
1769
backend/node_modules/@fastify/rate-limit/test/route-rate-limit.test.js
generated
vendored
Normal file
1769
backend/node_modules/@fastify/rate-limit/test/route-rate-limit.test.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
169
backend/node_modules/@fastify/rate-limit/types/index.d.ts
generated
vendored
Normal file
169
backend/node_modules/@fastify/rate-limit/types/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
/// <reference types='node' />
|
||||
|
||||
import {
|
||||
ContextConfigDefault,
|
||||
FastifyPluginCallback,
|
||||
FastifyRequest,
|
||||
FastifySchema,
|
||||
preHandlerAsyncHookHandler,
|
||||
RouteGenericInterface,
|
||||
RouteOptions
|
||||
} from 'fastify'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider> {
|
||||
createRateLimit(options?: fastifyRateLimit.CreateRateLimitOptions): (req: FastifyRequest) => Promise<
|
||||
| {
|
||||
isAllowed: true
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
isAllowed: false
|
||||
key: string
|
||||
max: number
|
||||
timeWindow: number
|
||||
remaining: number
|
||||
ttl: number
|
||||
ttlInSeconds: number
|
||||
isExceeded: boolean
|
||||
isBanned: boolean
|
||||
}
|
||||
>
|
||||
|
||||
rateLimit<
|
||||
RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
|
||||
ContextConfig = ContextConfigDefault,
|
||||
SchemaCompiler extends FastifySchema = FastifySchema
|
||||
>(options?: fastifyRateLimit.RateLimitOptions): preHandlerAsyncHookHandler<
|
||||
RawServer,
|
||||
RawRequest,
|
||||
RawReply,
|
||||
RouteGeneric,
|
||||
ContextConfig,
|
||||
SchemaCompiler,
|
||||
TypeProvider,
|
||||
Logger
|
||||
>;
|
||||
}
|
||||
interface FastifyContextConfig {
|
||||
rateLimit?: fastifyRateLimit.RateLimitOptions | false;
|
||||
}
|
||||
}
|
||||
|
||||
type FastifyRateLimit = FastifyPluginCallback<fastifyRateLimit.RateLimitPluginOptions>
|
||||
|
||||
declare namespace fastifyRateLimit {
|
||||
|
||||
export interface FastifyRateLimitOptions { }
|
||||
|
||||
export interface errorResponseBuilderContext {
|
||||
statusCode: number;
|
||||
ban: boolean;
|
||||
after: string;
|
||||
max: number;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
export interface FastifyRateLimitStoreCtor {
|
||||
new(options: FastifyRateLimitOptions): FastifyRateLimitStore;
|
||||
}
|
||||
|
||||
export interface FastifyRateLimitStore {
|
||||
incr(
|
||||
key: string,
|
||||
callback: (
|
||||
error: Error | null,
|
||||
result?: { current: number; ttl: number }
|
||||
) => void
|
||||
): void;
|
||||
child(
|
||||
routeOptions: RouteOptions & { path: string; prefix: string }
|
||||
): FastifyRateLimitStore;
|
||||
}
|
||||
|
||||
interface DefaultAddHeaders {
|
||||
'x-ratelimit-limit'?: boolean;
|
||||
'x-ratelimit-remaining'?: boolean;
|
||||
'x-ratelimit-reset'?: boolean;
|
||||
'retry-after'?: boolean;
|
||||
}
|
||||
|
||||
interface DraftSpecAddHeaders {
|
||||
'ratelimit-limit'?: boolean;
|
||||
'ratelimit-remaining'?: boolean;
|
||||
'ratelimit-reset'?: boolean;
|
||||
'retry-after'?: boolean;
|
||||
}
|
||||
|
||||
interface DefaultAddHeadersOnExceeding {
|
||||
'x-ratelimit-limit'?: boolean;
|
||||
'x-ratelimit-remaining'?: boolean;
|
||||
'x-ratelimit-reset'?: boolean;
|
||||
}
|
||||
|
||||
interface DraftSpecAddHeadersOnExceeding {
|
||||
'ratelimit-limit'?: boolean;
|
||||
'ratelimit-remaining'?: boolean;
|
||||
'ratelimit-reset'?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateRateLimitOptions {
|
||||
store?: FastifyRateLimitStoreCtor;
|
||||
skipOnError?: boolean;
|
||||
max?:
|
||||
| number
|
||||
| ((req: FastifyRequest, key: string) => number)
|
||||
| ((req: FastifyRequest, key: string) => Promise<number>);
|
||||
timeWindow?:
|
||||
| number
|
||||
| string
|
||||
| ((req: FastifyRequest, key: string) => number)
|
||||
| ((req: FastifyRequest, key: string) => Promise<number>);
|
||||
/**
|
||||
* @deprecated Use `allowList` property
|
||||
*/
|
||||
whitelist?: string[] | ((req: FastifyRequest, key: string) => boolean);
|
||||
allowList?: string[] | ((req: FastifyRequest, key: string) => boolean | Promise<boolean>);
|
||||
keyGenerator?: (req: FastifyRequest) => string | number | Promise<string | number>;
|
||||
ban?: number;
|
||||
}
|
||||
|
||||
export type RateLimitHook =
|
||||
| 'onRequest'
|
||||
| 'preParsing'
|
||||
| 'preValidation'
|
||||
| 'preHandler'
|
||||
|
||||
export interface RateLimitOptions extends CreateRateLimitOptions {
|
||||
hook?: RateLimitHook;
|
||||
cache?: number;
|
||||
continueExceeding?: boolean;
|
||||
onBanReach?: (req: FastifyRequest, key: string) => void;
|
||||
groupId?: string;
|
||||
errorResponseBuilder?: (
|
||||
req: FastifyRequest,
|
||||
context: errorResponseBuilderContext
|
||||
) => object;
|
||||
enableDraftSpec?: boolean;
|
||||
onExceeding?: (req: FastifyRequest, key: string) => void;
|
||||
onExceeded?: (req: FastifyRequest, key: string) => void;
|
||||
exponentialBackoff?: boolean;
|
||||
|
||||
}
|
||||
|
||||
export interface RateLimitPluginOptions extends RateLimitOptions {
|
||||
global?: boolean;
|
||||
cache?: number;
|
||||
redis?: any;
|
||||
nameSpace?: string;
|
||||
addHeaders?: DefaultAddHeaders | DraftSpecAddHeaders;
|
||||
addHeadersOnExceeding?:
|
||||
| DefaultAddHeadersOnExceeding
|
||||
| DraftSpecAddHeadersOnExceeding;
|
||||
}
|
||||
export const fastifyRateLimit: FastifyRateLimit
|
||||
export { fastifyRateLimit as default }
|
||||
}
|
||||
|
||||
declare function fastifyRateLimit (...params: Parameters<FastifyRateLimit>): ReturnType<FastifyRateLimit>
|
||||
export = fastifyRateLimit
|
||||
277
backend/node_modules/@fastify/rate-limit/types/index.test-d.ts
generated
vendored
Normal file
277
backend/node_modules/@fastify/rate-limit/types/index.test-d.ts
generated
vendored
Normal file
@@ -0,0 +1,277 @@
|
||||
import fastify, {
|
||||
FastifyInstance,
|
||||
FastifyRequest,
|
||||
preHandlerAsyncHookHandler,
|
||||
RequestGenericInterface,
|
||||
RouteOptions
|
||||
} from 'fastify'
|
||||
import * as http2 from 'node:http2'
|
||||
import IORedis from 'ioredis'
|
||||
import pino from 'pino'
|
||||
import fastifyRateLimit, {
|
||||
CreateRateLimitOptions,
|
||||
errorResponseBuilderContext,
|
||||
FastifyRateLimitOptions,
|
||||
FastifyRateLimitStore,
|
||||
RateLimitPluginOptions
|
||||
} from '..'
|
||||
import { expectAssignable, expectType } from 'tsd'
|
||||
|
||||
class CustomStore implements FastifyRateLimitStore {
|
||||
options: FastifyRateLimitOptions
|
||||
|
||||
constructor (options: FastifyRateLimitOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
incr (
|
||||
_key: string,
|
||||
_callback: (
|
||||
error: Error | null,
|
||||
result?: { current: number; ttl: number }
|
||||
) => void
|
||||
) {}
|
||||
|
||||
child (_routeOptions: RouteOptions & { path: string; prefix: string }) {
|
||||
return <CustomStore>(<FastifyRateLimitOptions>{})
|
||||
}
|
||||
}
|
||||
|
||||
const appWithImplicitHttp = fastify()
|
||||
const options1: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
max: 3,
|
||||
timeWindow: 5000,
|
||||
cache: 10000,
|
||||
allowList: ['127.0.0.1'],
|
||||
redis: new IORedis({ host: '127.0.0.1' }),
|
||||
skipOnError: true,
|
||||
ban: 10,
|
||||
continueExceeding: false,
|
||||
keyGenerator: (req: FastifyRequest<RequestGenericInterface>) => req.ip,
|
||||
groupId: '42',
|
||||
errorResponseBuilder: (
|
||||
req: FastifyRequest<RequestGenericInterface>,
|
||||
context: errorResponseBuilderContext
|
||||
) => {
|
||||
if (context.ban) {
|
||||
return {
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `You can not access this service as you have sent too many requests that exceed your rate limit. Your IP: ${req.ip} and Limit: ${context.max}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: `You hit the rate limit, please slow down! You can retry in ${context.after}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
addHeadersOnExceeding: {
|
||||
'x-ratelimit-limit': false,
|
||||
'x-ratelimit-remaining': false,
|
||||
'x-ratelimit-reset': false
|
||||
},
|
||||
addHeaders: {
|
||||
'x-ratelimit-limit': false,
|
||||
'x-ratelimit-remaining': false,
|
||||
'x-ratelimit-reset': false,
|
||||
'retry-after': false
|
||||
},
|
||||
onExceeding: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => ({}),
|
||||
onExceeded: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => ({}),
|
||||
onBanReach: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => ({})
|
||||
}
|
||||
const options2: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 42,
|
||||
allowList: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => false,
|
||||
timeWindow: 5000,
|
||||
hook: 'preParsing'
|
||||
}
|
||||
|
||||
const options3: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 42,
|
||||
timeWindow: 5000,
|
||||
store: CustomStore,
|
||||
hook: 'preValidation'
|
||||
}
|
||||
|
||||
const options4: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => Promise.resolve(42),
|
||||
timeWindow: 5000,
|
||||
store: CustomStore,
|
||||
hook: 'preHandler'
|
||||
}
|
||||
|
||||
const options5: RateLimitPluginOptions = {
|
||||
max: 3,
|
||||
timeWindow: 5000,
|
||||
cache: 10000,
|
||||
redis: new IORedis({ host: '127.0.0.1' }),
|
||||
nameSpace: 'my-namespace'
|
||||
}
|
||||
|
||||
const options6: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
allowList: async (_req, _key) => true,
|
||||
keyGenerator: async (_req) => '',
|
||||
timeWindow: 5000,
|
||||
store: CustomStore,
|
||||
hook: 'preHandler'
|
||||
}
|
||||
|
||||
const options7: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 42,
|
||||
timeWindow: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 5000,
|
||||
store: CustomStore,
|
||||
hook: 'preValidation'
|
||||
}
|
||||
|
||||
const options8: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 42,
|
||||
timeWindow: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => Promise.resolve(5000),
|
||||
store: CustomStore,
|
||||
hook: 'preValidation'
|
||||
}
|
||||
|
||||
const options9: RateLimitPluginOptions = {
|
||||
global: true,
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => Promise.resolve(42),
|
||||
timeWindow: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 5000,
|
||||
store: CustomStore,
|
||||
hook: 'preValidation',
|
||||
exponentialBackoff: true
|
||||
}
|
||||
|
||||
appWithImplicitHttp.register(fastifyRateLimit, options1)
|
||||
appWithImplicitHttp.register(fastifyRateLimit, options2)
|
||||
appWithImplicitHttp.register(fastifyRateLimit, options5)
|
||||
appWithImplicitHttp.register(fastifyRateLimit, options9)
|
||||
|
||||
appWithImplicitHttp.register(fastifyRateLimit, options3).then(() => {
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit())
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options1))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options2))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options3))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options4))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options5))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options6))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options7))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options8))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options9))
|
||||
// The following test is dependent on https://github.com/fastify/fastify/pull/2929
|
||||
// appWithImplicitHttp.setNotFoundHandler({
|
||||
// preHandler: appWithImplicitHttp.rateLimit()
|
||||
// }, function (request:FastifyRequest<RequestGenericInterface>, reply: FastifyReply<ReplyGenericInterface>) {
|
||||
// reply.status(404).send(new Error('Not found'))
|
||||
// })
|
||||
})
|
||||
|
||||
appWithImplicitHttp.get('/', { config: { rateLimit: { max: 10, timeWindow: '60s' } } }, () => { return 'limited' })
|
||||
|
||||
const appWithHttp2: FastifyInstance<
|
||||
http2.Http2Server,
|
||||
http2.Http2ServerRequest,
|
||||
http2.Http2ServerResponse
|
||||
> = fastify({ http2: true })
|
||||
|
||||
appWithHttp2.register(fastifyRateLimit, options1)
|
||||
appWithHttp2.register(fastifyRateLimit, options2)
|
||||
appWithHttp2.register(fastifyRateLimit, options3)
|
||||
appWithHttp2.register(fastifyRateLimit, options5)
|
||||
appWithHttp2.register(fastifyRateLimit, options6)
|
||||
appWithHttp2.register(fastifyRateLimit, options7)
|
||||
appWithHttp2.register(fastifyRateLimit, options8)
|
||||
appWithHttp2.register(fastifyRateLimit, options9)
|
||||
|
||||
appWithHttp2.get('/public', {
|
||||
config: {
|
||||
rateLimit: false
|
||||
}
|
||||
}, (_request, reply) => {
|
||||
reply.send({ hello: 'from ... public' })
|
||||
})
|
||||
|
||||
expectAssignable<errorResponseBuilderContext>({
|
||||
statusCode: 429,
|
||||
ban: true,
|
||||
after: '123',
|
||||
max: 1000,
|
||||
ttl: 123
|
||||
})
|
||||
|
||||
const appWithCustomLogger = fastify({
|
||||
loggerInstance: pino(),
|
||||
}).withTypeProvider()
|
||||
|
||||
appWithCustomLogger.register(fastifyRateLimit, options1)
|
||||
|
||||
appWithCustomLogger.route({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
preHandler: appWithCustomLogger.rateLimit({}),
|
||||
handler: () => {},
|
||||
})
|
||||
|
||||
const options10: CreateRateLimitOptions = {
|
||||
store: CustomStore,
|
||||
skipOnError: true,
|
||||
max: 0,
|
||||
timeWindow: 5000,
|
||||
allowList: ['127.0.0.1'],
|
||||
keyGenerator: (req: FastifyRequest<RequestGenericInterface>) => req.ip,
|
||||
ban: 10
|
||||
}
|
||||
|
||||
appWithImplicitHttp.register(fastifyRateLimit, { global: false })
|
||||
const checkRateLimit = appWithImplicitHttp.createRateLimit(options10)
|
||||
appWithImplicitHttp.route({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
handler: async (req, _reply) => {
|
||||
const limit = await checkRateLimit(req)
|
||||
expectType<{
|
||||
isAllowed: true;
|
||||
key: string;
|
||||
} | {
|
||||
isAllowed: false;
|
||||
key: string;
|
||||
max: number;
|
||||
timeWindow: number;
|
||||
remaining: number;
|
||||
ttl: number;
|
||||
ttlInSeconds: number;
|
||||
isExceeded: boolean;
|
||||
isBanned: boolean;
|
||||
}>(limit)
|
||||
},
|
||||
})
|
||||
|
||||
const options11: CreateRateLimitOptions = {
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 42,
|
||||
timeWindow: '10s',
|
||||
allowList: (_req: FastifyRequest<RequestGenericInterface>) => true,
|
||||
keyGenerator: (_req: FastifyRequest<RequestGenericInterface>) => 42,
|
||||
}
|
||||
|
||||
const options12: CreateRateLimitOptions = {
|
||||
max: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => Promise.resolve(42),
|
||||
timeWindow: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => 5000,
|
||||
allowList: (_req: FastifyRequest<RequestGenericInterface>) => Promise.resolve(true),
|
||||
keyGenerator: (_req: FastifyRequest<RequestGenericInterface>) => Promise.resolve(42),
|
||||
}
|
||||
|
||||
const options13: CreateRateLimitOptions = {
|
||||
timeWindow: (_req: FastifyRequest<RequestGenericInterface>, _key: string) => Promise.resolve(5000),
|
||||
keyGenerator: (_req: FastifyRequest<RequestGenericInterface>) => Promise.resolve('key'),
|
||||
}
|
||||
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options11))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options12))
|
||||
expectType<preHandlerAsyncHookHandler>(appWithImplicitHttp.rateLimit(options13))
|
||||
Reference in New Issue
Block a user