ac-ratelimiter 1.0.10 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +3 -2
- package/.github/workflows/node.js.yml +8 -2
- package/README.md +59 -16
- package/index.js +175 -156
- package/package.json +7 -7
- package/test/test.js +324 -219
package/.eslintrc.js
CHANGED
|
@@ -5,9 +5,9 @@ name: Node.js CI
|
|
|
5
5
|
|
|
6
6
|
on:
|
|
7
7
|
push:
|
|
8
|
-
branches: [ master ]
|
|
8
|
+
branches: [ master, develop ]
|
|
9
9
|
pull_request:
|
|
10
|
-
branches: [ master ]
|
|
10
|
+
branches: [ master, develop ]
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
13
|
build:
|
|
@@ -18,6 +18,7 @@ jobs:
|
|
|
18
18
|
matrix:
|
|
19
19
|
node-version: [16.x, 18.x]
|
|
20
20
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
21
|
+
redis-version: [6]
|
|
21
22
|
|
|
22
23
|
steps:
|
|
23
24
|
- uses: actions/checkout@v3
|
|
@@ -26,5 +27,10 @@ jobs:
|
|
|
26
27
|
with:
|
|
27
28
|
node-version: ${{ matrix.node-version }}
|
|
28
29
|
|
|
30
|
+
- name: Start Redis
|
|
31
|
+
uses: supercharge/redis-github-action@1.4.0
|
|
32
|
+
with:
|
|
33
|
+
redis-version: ${{ matrix.redis-version }}
|
|
34
|
+
|
|
29
35
|
- run: yarn install
|
|
30
36
|
- run: yarn run test
|
package/README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# ac-ratelimiter
|
|
2
2
|
|
|
3
|
-
This tool provides rate-limiter
|
|
3
|
+
This tool provides rate-limiter that can be used as middleware with ExpressJs.
|
|
4
|
+
|
|
5
|
+
For huge production load it is recommended that you use it with Redis. However, for smaller applications you can use the build in Node Cache.
|
|
4
6
|
|
|
5
7
|
[](https://github.com/AdmiralCloud/ac-ratelimiter/actions/workflows/node.js.yml)
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Breaking changes for version 2
|
|
10
|
+
Version 2 is a complete re-write of this module. It is now a class and uses async/await.
|
|
8
11
|
|
|
9
12
|
```
|
|
13
|
+
// Migration example
|
|
14
|
+
|
|
15
|
+
// Version 1
|
|
10
16
|
const acrl = require('ac-ratelimiter')
|
|
11
17
|
|
|
12
18
|
const init = {
|
|
@@ -26,37 +32,73 @@ acrl.limiter(req, {}, err => {
|
|
|
26
32
|
return res.json({ status: _.get(err, 'status') })
|
|
27
33
|
})
|
|
28
34
|
|
|
35
|
+
|
|
36
|
+
// Version 2
|
|
37
|
+
const acrl = require('ac-ratelimiter')
|
|
38
|
+
|
|
39
|
+
const init = {
|
|
40
|
+
routes: [
|
|
41
|
+
{ route: 'user/find', throttleLimit: 1, limit: 2, expires: 3, delay: 250 },
|
|
42
|
+
],
|
|
43
|
+
redis: REDIS INSTANCE
|
|
44
|
+
logger: winston.log INSTANCE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rateLimiter = new acrl(init)
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await rateLimiter.limiter(req)
|
|
51
|
+
}
|
|
52
|
+
catch(e) {
|
|
53
|
+
// e.status === 900 => throttling is active
|
|
54
|
+
// e.status === 429 => limiter is active
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Without any dependencies
|
|
62
|
+
This example initiates the rate limiter with NodeCache (instead of Redis) and console.log (instead of Winston). Default limits are 150 requests within 3 seconds. Starting at 50 request, requests will be throttled by 250ms.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
const acrl = require('ac-ratelimiter')
|
|
66
|
+
|
|
67
|
+
const rateLimiter = new acrl()
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await rateLimiter.limiter(req)
|
|
71
|
+
}
|
|
72
|
+
catch(e) {
|
|
73
|
+
// e.status === 900 => throttling is active
|
|
74
|
+
// e.status === 429 => limiter is active
|
|
75
|
+
}
|
|
76
|
+
|
|
29
77
|
```
|
|
30
78
|
|
|
31
79
|
## Prerequisites
|
|
32
80
|
|
|
33
81
|
### Init
|
|
34
|
-
The ac-ratelimiter
|
|
82
|
+
The ac-ratelimiter can use Redis as storage for the rate-limiter keys. By default and to run out-of-the-box it uses Node Cache.
|
|
35
83
|
|
|
36
84
|
Additionally, for logging purposes, we use Winston. But you can also use any other logger that provides logging for "warn" and "error".
|
|
37
85
|
|
|
38
86
|
Last but not least, provide an array of objects with rate limiter instructions. Each object has the following properties:
|
|
39
|
-
Property | Type |
|
|
87
|
+
Property | Type | Defaults | Remarks
|
|
40
88
|
---|---|---|---|
|
|
41
|
-
|
|
42
|
-
throttleLimit |
|
|
89
|
+
routes | string | | A combination of controller and action (express) or any other identifier you can provide
|
|
90
|
+
throttleLimit | 50 | 20 | Number of calls before throttling starts
|
|
43
91
|
delay | integer | 250 | Number of milliseconds a throttle request is delayed (on purpose)
|
|
44
|
-
limit | integer |
|
|
92
|
+
limit | integer | 150 | Number of calls before the limiter kicks in
|
|
45
93
|
expires | integer | 3 | Number of seconds before the rate-limiter resets
|
|
46
94
|
|
|
47
95
|
|
|
48
|
-
The init function can take additional parameters like
|
|
49
|
-
+ environment
|
|
50
|
-
+ debugMode
|
|
51
96
|
|
|
52
97
|
### RateLimiter
|
|
53
98
|
The actual rateLimiter function takes two arguments, the Express request object (req) and an options object with the following optional properties:
|
|
54
99
|
|
|
55
100
|
Property | Type | Example | Remarks
|
|
56
101
|
---|---|---|---|
|
|
57
|
-
knownIP | Object | { name: 'AdmiralCloud' } | Display known IPs with their name when limiter is active
|
|
58
|
-
link | String | myLink | Display and locks the limiter to a given link (as identifier)
|
|
59
|
-
token | String | myToken | Locks the limiter to a given token/user session
|
|
60
102
|
name | String | myName | Identifier for the route - falls back to controller/action
|
|
61
103
|
redisKey | String | myKey | Optional RedisKey to use for rate limiter
|
|
62
104
|
fallbackRoute | String | fbroute | Optional fallback route identifier
|
|
@@ -73,12 +115,13 @@ Both values might be retrieved prior to the rate limiter so there is no need to
|
|
|
73
115
|
|
|
74
116
|
# Links
|
|
75
117
|
- [Website](https://www.admiralcloud.com/)
|
|
76
|
-
- [Twitter (@admiralcloud)](https://twitter.com/admiralcloud)
|
|
77
118
|
- [Facebook](https://www.facebook.com/MediaAssetManagement/)
|
|
78
119
|
|
|
79
120
|
# Run tests
|
|
80
|
-
|
|
121
|
+
```
|
|
122
|
+
yarn run test
|
|
123
|
+
```
|
|
81
124
|
|
|
82
125
|
## License
|
|
83
126
|
|
|
84
|
-
[MIT License](https://opensource.org/licenses/MIT) Copyright © 2009-present, AdmiralCloud, Mark Poepping
|
|
127
|
+
[MIT License](https://opensource.org/licenses/MIT) Copyright © 2009-present, AdmiralCloud AG, Mark Poepping
|
package/index.js
CHANGED
|
@@ -1,188 +1,207 @@
|
|
|
1
|
-
|
|
2
|
-
* Copyright mmpro film- und medienproduktion GmbH and other Node contributors
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const async = require('async');
|
|
7
|
-
const _ = require('lodash');
|
|
1
|
+
const { setTimeout } = require('timers/promises')
|
|
8
2
|
|
|
3
|
+
const NodeCache = require('node-cache')
|
|
9
4
|
const acts = require('ac-ip')
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
6
|
+
class ACError extends Error {
|
|
7
|
+
constructor(message, options = {}) {
|
|
8
|
+
super(message)
|
|
9
|
+
|
|
10
|
+
if (Error.captureStackTrace) {
|
|
11
|
+
Error.captureStackTrace(this, ACError)
|
|
12
|
+
}
|
|
13
|
+
// info
|
|
14
|
+
this.code = options.code || -1
|
|
15
|
+
this.errorMessage = message
|
|
16
|
+
// show other properties of options object
|
|
17
|
+
for (const [key, value] of Object.entries(options)) {
|
|
18
|
+
this[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class RateLimiter {
|
|
24
|
+
constructor({ redisInstance, logger = console, routes = [], knownIPs = [], ignorePrivateIps } = {}) {
|
|
25
|
+
// make sure only one instance exists!
|
|
26
|
+
if (RateLimiter._instance) {
|
|
27
|
+
return RateLimiter._instance
|
|
30
28
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
RateLimiter._instance = this;
|
|
30
|
+
|
|
31
|
+
this.environment = process.env.NODE_ENV || 'development'
|
|
32
|
+
if(redisInstance) {
|
|
33
|
+
this.redisInstance = redisInstance
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.cache = new NodeCache()
|
|
34
37
|
}
|
|
35
|
-
|
|
38
|
+
|
|
39
|
+
this.limits = {
|
|
40
|
+
expires: 3,
|
|
41
|
+
limit: 150,
|
|
42
|
+
throttleLimit: 50,
|
|
43
|
+
delay: 250
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.logger = logger
|
|
47
|
+
this.routes = routes
|
|
48
|
+
this.knownIPs = knownIPs
|
|
49
|
+
this.ignorePrivateIps = ignorePrivateIps
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const action = _.get(params, 'action', 'action')
|
|
42
|
-
const clientId = _.get(params, 'clientId', 'clientId')
|
|
43
|
-
const identifier = _.get(params, 'identifier')
|
|
44
|
-
|
|
45
|
-
let redisKey = _.get(params, 'redisKey', (environment + ':rateLimiter:' + clientId + ':' + ip + ':' + controller + ':' + action))
|
|
46
|
-
if (identifier) redisKey += ':' + identifier
|
|
47
|
-
return redisKey
|
|
52
|
+
whichStorage() {
|
|
53
|
+
if (this.redisInstance) return 'Redis'
|
|
54
|
+
else return 'NodeCache'
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
// private
|
|
58
|
+
prepareRedisKey({ ip, controller = 'controller', action = 'action', clientId = 'clientId', identifier = 'identifier', redisKey }) {
|
|
59
|
+
let rateLimiterKey = redisKey || (this.environment + ':rateLimiter:' + clientId + ':' + ip + ':' + controller + ':' + action)
|
|
60
|
+
if (identifier) rateLimiterKey += ':' + identifier
|
|
61
|
+
return rateLimiterKey
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async limiter(req, {
|
|
65
|
+
ip = req?.determinedIP || acts.determineIP(req),
|
|
66
|
+
clientId = 'clientId',
|
|
67
|
+
identifier = 'identifier',
|
|
68
|
+
redisKey,
|
|
69
|
+
expires,
|
|
70
|
+
limit,
|
|
71
|
+
throttleLimit,
|
|
72
|
+
delay,
|
|
73
|
+
fallbackRoute = 'default',
|
|
74
|
+
name,
|
|
75
|
+
debugMode,
|
|
76
|
+
rateLimitCounter
|
|
77
|
+
}) {
|
|
78
|
+
|
|
79
|
+
if (this.ignorePrivateIps && acts.isPrivate(ip)) return
|
|
80
|
+
|
|
81
|
+
const logIdentifier = typeof identifier === 'string' && identifier.replace(/(\w{1,4})-(\w{1,4})/g, 'xxxx')
|
|
82
|
+
const knownIP = this.knownIPs.find(({ knownIP }) => knownIP === ip)
|
|
83
|
+
|
|
84
|
+
const controller = req?.options?.controller || 'controller'
|
|
85
|
+
const action = req?.options?.action || 'action'
|
|
86
|
+
const currentRoute = name || `${controller}/${action}`
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
const rateLimiterKey = this.prepareRedisKey({
|
|
63
90
|
ip,
|
|
64
91
|
controller,
|
|
65
92
|
action,
|
|
66
93
|
clientId,
|
|
67
94
|
identifier,
|
|
68
|
-
redisKey
|
|
95
|
+
redisKey
|
|
69
96
|
})
|
|
70
97
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
const rateLogger = ({ type, rateLimitCounter, currentLimit }) => {
|
|
99
|
+
this.logger.warn('-'.repeat(80))
|
|
100
|
+
if (debugMode) this.logger.warn('DEBUG MODE - DEBUG MODE - DEBUG MODE')
|
|
101
|
+
this.logger.warn('%s | %s | %s | %s | Counter %s/%s', 'ACRateLimiter'.padEnd(15), type.padEnd(12), currentRoute.padEnd(32), (ip + ' ' + (knownIP?.name || '')).padEnd(16), rateLimitCounter, currentLimit)
|
|
102
|
+
if (logIdentifier) this.logger.warn('%s | Identifier: %s', ' '.padEnd(15), logIdentifier)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let settings = this.routes.find(item => {
|
|
106
|
+
if (ip && clientId && currentRoute && item.ip === ip && item.clientId === clientId && item.route === currentRoute ) return item
|
|
107
|
+
else if (ip && currentRoute && item.ip === ip && item.route === currentRoute && !item.clientId ) return item
|
|
108
|
+
else if (clientId && currentRoute && item.clientId === clientId && item.route === currentRoute && !item.ip) return item
|
|
109
|
+
else if (currentRoute && item.route === currentRoute && !item.clientId && !item.ip) return item
|
|
77
110
|
})
|
|
78
111
|
if (!settings) {
|
|
79
112
|
// check fallback route
|
|
80
|
-
settings = routes.find(item => {
|
|
113
|
+
settings = this.routes.find(item => {
|
|
81
114
|
if (item.route === fallbackRoute && !item.clientId && !item.ip) return item
|
|
82
115
|
})
|
|
83
116
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const rateLogger = (params) => {
|
|
92
|
-
const type = _.get(params, 'type')
|
|
93
|
-
logger.warn(_.repeat('-', 80))
|
|
94
|
-
if (debugMode) logger.warn('DEBUG MODE - DEBUG MODE - DEBUG MODE')
|
|
95
|
-
logger.warn('%s | %s | %s | %s | Counter %s/%s', _.padEnd('ACRateLimiter', 15), _.padEnd(type, 12), _.padEnd(route, 32), _.padEnd((ip + ' ' + _.get(knownIP, 'name', '')), 16), rateLimitCounter, limit)
|
|
96
|
-
if (logIdentifier) logger.warn('%s | Identifier: %s', _.padEnd(' ', 15), logIdentifier)
|
|
117
|
+
|
|
118
|
+
let current = {
|
|
119
|
+
expires,
|
|
120
|
+
limit,
|
|
121
|
+
throttleLimit,
|
|
122
|
+
delay
|
|
97
123
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// silently ignore
|
|
106
|
-
return done()
|
|
107
|
-
}
|
|
108
|
-
rateLimitCounter = result
|
|
109
|
-
return done()
|
|
110
|
-
})
|
|
111
|
-
},
|
|
112
|
-
processLimits: (done) => {
|
|
113
|
-
if (rateLimitCounter === 1 && rateLimitCounter < limit) {
|
|
114
|
-
// key has never been set before - set expire time and return
|
|
115
|
-
redis.expire(redisKey, expires, done)
|
|
116
|
-
}
|
|
117
|
-
else if (rateLimitCounter > limit) {
|
|
118
|
-
// only log every 10th entry
|
|
119
|
-
if (rateLimitCounter === limit || rateLimitCounter % 10 === 0) {
|
|
120
|
-
rateLogger({ type: 'Blocking' })
|
|
121
|
-
}
|
|
122
|
-
// debug mode shows the rate limiting but does not limit
|
|
123
|
-
if (debugMode) return done()
|
|
124
|
-
return done({ message: 'tooManyRequestsFromThisIP', status: 429, logging: false, counter: rateLimitCounter, additionalInfo: { expires } })
|
|
125
|
-
}
|
|
126
|
-
else if (throttleLimit && rateLimitCounter > limit * 0.9) {
|
|
127
|
-
// at 90 percent delay with expire time, to avoid limit kicking in
|
|
128
|
-
rateLogger({ type: 'Final Throttling' })
|
|
129
|
-
_.delay(() => {
|
|
130
|
-
// do not "taint" process time when deliberately throttline
|
|
131
|
-
if (req._startTime) req._startTime += expires * 1000
|
|
132
|
-
|
|
133
|
-
return done({ message: 'finalThrottlingActive_requestsIsDelayed', status: 900, additionalInfo: { expires } })
|
|
134
|
-
}, expires * 1000)
|
|
135
|
-
}
|
|
136
|
-
else if (throttleLimit && rateLimitCounter > throttleLimit) {
|
|
137
|
-
// log the first throttling and every 50th entry
|
|
138
|
-
if (rateLimitCounter === (throttleLimit + 1) || rateLimitCounter % 50 === 0) {
|
|
139
|
-
rateLogger({ type: 'Throttling' })
|
|
140
|
-
}
|
|
141
|
-
_.delay(() => {
|
|
142
|
-
// do not "taint" process time when deliberately throttline
|
|
143
|
-
if (req._startTime) req._startTime += delay
|
|
144
|
-
|
|
145
|
-
return done({ message: 'throttlingActive_requestsIsDelayed', status: 900, additionalInfo: { expires } })
|
|
146
|
-
}, delay)
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
return done()
|
|
150
|
-
}
|
|
124
|
+
|
|
125
|
+
const props = ['expires', 'limit', 'throttleLimit', 'delay']
|
|
126
|
+
props.forEach(prop => {
|
|
127
|
+
if (!Number.isFinite(current[prop])) {
|
|
128
|
+
// use from setting
|
|
129
|
+
if (Number.isFinite(settings?.[prop])) current[prop] = settings[prop]
|
|
130
|
+
else current[prop] = this.limits[prop]
|
|
151
131
|
}
|
|
152
|
-
}, err => {
|
|
153
|
-
return cb(err, { ip, controller, action, counter: rateLimitCounter, knownIPName: _.get(knownIP, 'name', '-'), identifier: logIdentifier })
|
|
154
132
|
})
|
|
155
|
-
}
|
|
156
133
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
134
|
+
if (rateLimitCounter) {
|
|
135
|
+
// if rateLimitCounter is sent with the request, then don't fetch it again
|
|
136
|
+
}
|
|
137
|
+
else if (this.redisInstance) {
|
|
138
|
+
// use Redis instance for rate limiting
|
|
139
|
+
rateLimitCounter = await this.redisInstance.incr(rateLimiterKey)
|
|
140
|
+
if (rateLimitCounter === 1 && rateLimitCounter < current.limit) {
|
|
141
|
+
// key has never been set before - set expire time and return
|
|
142
|
+
await this.redisInstance.expire(redisKey, expires)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// use Node cache (memory) for rate limiting - please see README before using in production!
|
|
147
|
+
rateLimitCounter = this.cache.get(rateLimiterKey)
|
|
148
|
+
let ts = this.cache.getTtl(rateLimiterKey) // ts in ms when the key will expire
|
|
149
|
+
rateLimitCounter = rateLimitCounter + 1 || 1
|
|
150
|
+
if (ts === undefined || ts === 0) {
|
|
151
|
+
// first entry - set expiration
|
|
152
|
+
this.cache.set(rateLimiterKey, rateLimitCounter, current?.expires )
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// update but use the existing timestamp
|
|
156
|
+
const newTTL = ts - new Date().getTime()
|
|
157
|
+
this.cache.set(rateLimiterKey, rateLimitCounter, Math.ceil(newTTL/1000))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (debugMode) {
|
|
162
|
+
console.log('Route %s | Current Counter %s | Throttle %s | Limit %s | Delay %s | Expires %s', currentRoute, rateLimitCounter, current.throttleLimit, current.limit, current.delay, current.expires)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (rateLimitCounter > current.limit) {
|
|
166
|
+
// only log every 10th entry
|
|
167
|
+
if (rateLimitCounter === current.limit || rateLimitCounter % 10 === 0) {
|
|
168
|
+
rateLogger({ type: 'Blocking', rateLimitCounter, currentLimit: current.limit })
|
|
169
|
+
}
|
|
170
|
+
throw new ACError('tooManyRequestsFromThisIP', { status: 429, logging: false, counter: rateLimitCounter, additionalInfo: { expires: current.expires } })
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
else if (current.throttleLimit && rateLimitCounter > current.limit * 0.9) {
|
|
174
|
+
// at 90 percent delay with expire time, to avoid limit kicking in
|
|
175
|
+
rateLogger({ type: 'Final Throttling', rateLimitCounter, currentLimit: current.limit })
|
|
176
|
+
await setTimeout(current.expires * 1000)
|
|
177
|
+
// do not "taint" process time when deliberately throttline
|
|
178
|
+
if (req._startTime) req._startTime += current.expires * 1000
|
|
179
|
+
throw new ACError('finalThrottlingActive_requestsIsDelayed', { status: 900, additionalInfo: { counter: rateLimitCounter, expires: current.expires } })
|
|
180
|
+
}
|
|
181
|
+
else if (current.throttleLimit && rateLimitCounter > current.throttleLimit) {
|
|
182
|
+
// log the first throttling and every 50th entry
|
|
183
|
+
if (rateLimitCounter === (throttleLimit + 1) || rateLimitCounter % 50 === 0) {
|
|
184
|
+
rateLogger({ type: 'Throttling', rateLimitCounter, currentLimit: current.limit })
|
|
185
|
+
}
|
|
186
|
+
await setTimeout(current.delay)
|
|
187
|
+
// do not "taint" process time when deliberately throttline
|
|
188
|
+
if (req._startTime) req._startTime += current.delay
|
|
189
|
+
throw new ACError('throttlingActive_requestsIsDelayed', { status: 900, additionalInfo: { counter: rateLimitCounter, expires: current.expires } })
|
|
190
|
+
}
|
|
177
191
|
}
|
|
178
192
|
|
|
193
|
+
async updateLimiter({ routes, knownIPs, ignorePrivateIps }) {
|
|
194
|
+
if (Array.isArray(routes)) this.routes = routes
|
|
195
|
+
if (Array.isArray(knownIPs)) this.knownIPs = knownIPs
|
|
196
|
+
if (typeof ignorePrivateIps === 'boolean') this.ignorePrivateIps = ignorePrivateIps
|
|
197
|
+
}
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
prepareRedisKey,
|
|
184
|
-
resetLimiter
|
|
199
|
+
async resetLimiter() {
|
|
200
|
+
// reset completely
|
|
201
|
+
this.cache.flushAll()
|
|
185
202
|
}
|
|
186
203
|
|
|
204
|
+
|
|
187
205
|
}
|
|
188
|
-
|
|
206
|
+
|
|
207
|
+
module.exports = RateLimiter
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ac-ratelimiter",
|
|
3
3
|
"description": "Simple ratelimiter for express",
|
|
4
|
-
"version": "
|
|
5
|
-
"author": "Mark Poepping",
|
|
4
|
+
"version": "2.0.0-beta.1",
|
|
5
|
+
"author": "Mark Poepping (www.admiralcloud.com)",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git://github.com/admiralcloud/ac-ratelimiter"
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"homepage": "https://www.admiralcloud.com",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ac-ip": "^3.0.1",
|
|
13
|
-
"
|
|
14
|
-
"lodash": "^4.17.21"
|
|
13
|
+
"node-cache": "^5.1.2"
|
|
15
14
|
},
|
|
16
15
|
"devDependencies": {
|
|
17
|
-
"ac-semantic-release": "^0.3.
|
|
16
|
+
"ac-semantic-release": "^0.3.5",
|
|
18
17
|
"chai": "^4.3.7",
|
|
19
|
-
"eslint": "8.
|
|
18
|
+
"eslint": "8.35.0",
|
|
19
|
+
"ioredis": "^5.3.1",
|
|
20
20
|
"mocha": "^10.2.0"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"test-jenkins": "JUNIT_REPORT_PATH=./report.xml mocha --slow 1000 --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_name='RATELIMITER'"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
28
|
+
"node": ">=16.0.0"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/test/test.js
CHANGED
|
@@ -1,298 +1,403 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const { expect } = require('chai')
|
|
2
|
+
const { setTimeout } = require('timers/promises')
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const ratelimiterModule = require('../index')
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const init = {
|
|
9
|
-
routes: [
|
|
10
|
-
{ route: 'user/find', throttleLimit: 1, limit: 2, expires: 3, delay: 250 },
|
|
11
|
-
],
|
|
12
|
-
redis: {
|
|
13
|
-
incr: (key, cb) => {
|
|
14
|
-
redisStoreSimulator[key] = redisStoreSimulator[key] || 0
|
|
15
|
-
redisStoreSimulator[key] += 1
|
|
16
|
-
//console.log('Redis incr', key, redisStoreSimulator[key])
|
|
17
|
-
return cb(null, redisStoreSimulator[key])
|
|
18
|
-
},
|
|
19
|
-
expire: (key, expires, cb) => {
|
|
20
|
-
//console.log('Redis expires', key)
|
|
21
|
-
setTimeout(() => {
|
|
22
|
-
_.unset(redisStoreSimulator, key)
|
|
23
|
-
}, expires * 1000)
|
|
24
|
-
return cb()
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
logger: {
|
|
28
|
-
warn: () => {
|
|
29
|
-
// just for simulation
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
6
|
+
const Redis = require('ioredis')
|
|
7
|
+
const redis = new Redis()
|
|
33
8
|
|
|
34
9
|
let req = {
|
|
35
10
|
options: {
|
|
36
11
|
controller: 'user',
|
|
37
|
-
action: 'find'
|
|
12
|
+
action: 'find',
|
|
38
13
|
},
|
|
39
14
|
determinedIP: '1.2.3.4'
|
|
40
15
|
}
|
|
41
16
|
|
|
17
|
+
|
|
18
|
+
let initOptions = {
|
|
19
|
+
routes: [
|
|
20
|
+
{ route: 'user/find', throttleLimit: 1, limit: 2, expires: 3, delay: 250 },
|
|
21
|
+
]
|
|
22
|
+
}
|
|
42
23
|
let options = {}
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
it('init tests', done => {
|
|
48
|
-
ratelimiter.init(init)
|
|
49
|
-
return done()
|
|
50
|
-
})
|
|
25
|
+
const ratelimiter = new ratelimiterModule(initOptions)
|
|
26
|
+
const ratelimiterRedis = new ratelimiterModule({ redisInstance: redis, routes: initOptions.routes })
|
|
27
|
+
|
|
51
28
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
29
|
+
describe('Use NodeCache', () => {
|
|
30
|
+
|
|
31
|
+
describe('Test section #1', function () {
|
|
32
|
+
describe('RATE LIMITER TEST', function() {
|
|
33
|
+
this.timeout(5000)
|
|
34
|
+
|
|
35
|
+
it('Reset Limiter', async() => {
|
|
36
|
+
await ratelimiter.resetLimiter()
|
|
56
37
|
})
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
expect(
|
|
61
|
-
return done()
|
|
38
|
+
|
|
39
|
+
it('should not trigger - req #1', async() => {
|
|
40
|
+
let result = await ratelimiter.limiter(req, options)
|
|
41
|
+
expect(result).eql(undefined)
|
|
62
42
|
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
43
|
+
it('should still not trigger - req #2 - but throw 900', async() => {
|
|
44
|
+
try {
|
|
45
|
+
await ratelimiter.limiter(req, options)
|
|
46
|
+
}
|
|
47
|
+
catch(e) {
|
|
48
|
+
expect(e).to.have.property('message', 'finalThrottlingActive_requestsIsDelayed')
|
|
49
|
+
expect(e).to.have.property('status', 900)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
it('should not trigger - req #3 - rate limiter is reset', async() => {
|
|
53
|
+
let result = await ratelimiter.limiter(req, options)
|
|
54
|
+
expect(result).eql(undefined)
|
|
68
55
|
})
|
|
69
56
|
})
|
|
70
57
|
})
|
|
71
|
-
})
|
|
72
58
|
|
|
73
59
|
|
|
74
|
-
describe('Test section #2 - immediate limiter', function () {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
]
|
|
82
|
-
ratelimiter.init(init)
|
|
83
|
-
return done()
|
|
84
|
-
})
|
|
60
|
+
describe('Test section #2 - immediate limiter', function () {
|
|
61
|
+
describe('RATE LIMITER TEST', function() {
|
|
62
|
+
this.timeout(5000)
|
|
63
|
+
|
|
64
|
+
it('Reset Limiter', async() => {
|
|
65
|
+
await ratelimiter.resetLimiter()
|
|
66
|
+
})
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
it('Update settings', async() => {
|
|
69
|
+
await ratelimiter.updateLimiter({
|
|
70
|
+
routes: [
|
|
71
|
+
{ route: 'user/find', limit: 0, expires: 3, delay: 250 },
|
|
72
|
+
]
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should trigger immediately', async() => {
|
|
77
|
+
req.determinedIP = '4.1.4.1'
|
|
78
|
+
try {
|
|
79
|
+
await ratelimiter.limiter(req, options)
|
|
80
|
+
}
|
|
81
|
+
catch(e) {
|
|
82
|
+
expect(e).to.be.an('error')
|
|
83
|
+
expect(e).to.have.property('message', 'tooManyRequestsFromThisIP')
|
|
84
|
+
expect(e).to.have.property('status', 429)
|
|
85
|
+
}
|
|
90
86
|
})
|
|
91
87
|
})
|
|
92
88
|
})
|
|
93
|
-
})
|
|
94
89
|
|
|
95
90
|
|
|
96
|
-
describe('Test section #3 - no throttling', function () {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{ route: 'user/find', throttleLimit: 0, limit: 2, expires: 3, delay: 0 },
|
|
103
|
-
]
|
|
104
|
-
ratelimiter.init(init)
|
|
105
|
-
return done()
|
|
106
|
-
})
|
|
91
|
+
describe('Test section #3 - no throttling', function () {
|
|
92
|
+
describe('RATE LIMITER TEST', function() {
|
|
93
|
+
this.timeout(5000)
|
|
94
|
+
it('Reset Limiter', async() => {
|
|
95
|
+
await ratelimiter.resetLimiter()
|
|
96
|
+
})
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
it('Update settings', async() => {
|
|
99
|
+
await ratelimiter.updateLimiter({
|
|
100
|
+
routes: [
|
|
101
|
+
{ route: 'user/find', throttleLimit: 0, limit: 2, expires: 3, delay: 0 },
|
|
102
|
+
]
|
|
103
|
+
})
|
|
112
104
|
})
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
|
|
106
|
+
it('should not trigger req #1', async() => {
|
|
107
|
+
req.determinedIP = '2.3.4.1'
|
|
108
|
+
let result = await ratelimiter.limiter(req, options)
|
|
109
|
+
expect(result).eql(undefined)
|
|
118
110
|
})
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
it('should still not trigger - delayed req #2', async() => {
|
|
112
|
+
try {
|
|
113
|
+
await ratelimiter.limiter(req, options)
|
|
114
|
+
}
|
|
115
|
+
catch(e) {
|
|
116
|
+
expect(e).to.have.property('message', 'finalThrottlingActive_requestsIsDelayed')
|
|
117
|
+
expect(e).to.have.property('status', 900)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
it('should trigger the limiter', async() => {
|
|
121
|
+
try {
|
|
122
|
+
await ratelimiter.limiter(req, options)
|
|
123
|
+
}
|
|
124
|
+
catch(e) {
|
|
125
|
+
expect(e).to.have.property('message', 'tooManyRequestsFromThisIP')
|
|
126
|
+
expect(e).to.have.property('status', 429)
|
|
127
|
+
}
|
|
124
128
|
})
|
|
125
129
|
})
|
|
126
130
|
})
|
|
127
|
-
})
|
|
128
131
|
|
|
129
|
-
describe('Test section #4 - routes with clientId', function() {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
]
|
|
147
|
-
ratelimiter.init(init)
|
|
148
|
-
return done()
|
|
149
|
-
})
|
|
132
|
+
describe('Test section #4 - routes with clientId', function() {
|
|
133
|
+
describe('RATE LIMITER TEST', function() {
|
|
134
|
+
this.timeout(5000)
|
|
135
|
+
let req = {
|
|
136
|
+
options: {
|
|
137
|
+
controller: 'customer',
|
|
138
|
+
action: 'find'
|
|
139
|
+
},
|
|
140
|
+
determinedIP: '1.2.3.4'
|
|
141
|
+
}
|
|
142
|
+
let options = {
|
|
143
|
+
clientId: 'abc',
|
|
144
|
+
routes: [
|
|
145
|
+
{ route: 'customer/find', clientId: 'abc', throttleLimit: 1, limit: 2, expires: 3, delay: 250 },
|
|
146
|
+
{ route: 'customer/find', throttleLimit: 3, limit: 10, expires: 3, delay: 250 },
|
|
147
|
+
]
|
|
148
|
+
}
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
expect(err).eql(null)
|
|
154
|
-
return done()
|
|
150
|
+
it('Reset Limiter', async() => {
|
|
151
|
+
await ratelimiter.resetLimiter()
|
|
155
152
|
})
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect(
|
|
160
|
-
return done()
|
|
153
|
+
|
|
154
|
+
it('should not trigger - req #1', async() => {
|
|
155
|
+
let result = await ratelimiter.limiter(req, options)
|
|
156
|
+
expect(result).eql(undefined)
|
|
161
157
|
})
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
it('should still not trigger but delay - req #2', async() => {
|
|
159
|
+
try {
|
|
160
|
+
await ratelimiter.limiter(req, options)
|
|
161
|
+
}
|
|
162
|
+
catch(e) {
|
|
163
|
+
expect(e).to.have.property('message', 'finalThrottlingActive_requestsIsDelayed')
|
|
164
|
+
expect(e).to.have.property('status', 900)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
it('should still not trigger as ratelimiter is reset', async() => {
|
|
168
|
+
let result = await ratelimiter.limiter(req, options)
|
|
169
|
+
expect(result).eql(undefined)
|
|
167
170
|
})
|
|
168
|
-
})
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
it('Now make request without clientId - should throttler after 3 requests', async() => {
|
|
173
|
+
options = {
|
|
174
|
+
routes: [
|
|
175
|
+
{ route: 'customer/find', clientId: 'abc', throttleLimit: 1, limit: 2, expires: 3, delay: 250 },
|
|
176
|
+
{ route: 'customer/find', throttleLimit: 3, limit: 10, expires: 3, delay: 250 },
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
})
|
|
174
180
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
expect(
|
|
178
|
-
return done()
|
|
181
|
+
it('should not trigger #1', async() => {
|
|
182
|
+
let result = await ratelimiter.limiter(req, options)
|
|
183
|
+
expect(result).eql(undefined)
|
|
179
184
|
})
|
|
180
|
-
})
|
|
181
185
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect(
|
|
185
|
-
return done()
|
|
186
|
+
it('should not trigger #2', async() => {
|
|
187
|
+
let result = await ratelimiter.limiter(req, options)
|
|
188
|
+
expect(result).eql(undefined)
|
|
186
189
|
})
|
|
187
|
-
})
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
expect(
|
|
192
|
-
return done()
|
|
191
|
+
it('should not trigger #3', async() => {
|
|
192
|
+
let result = await ratelimiter.limiter(req, options)
|
|
193
|
+
expect(result).eql(undefined)
|
|
193
194
|
})
|
|
194
|
-
})
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
it('should delay request #4', async() => {
|
|
197
|
+
try {
|
|
198
|
+
await ratelimiter.limiter(req, options)
|
|
199
|
+
}
|
|
200
|
+
catch(e) {
|
|
201
|
+
expect(e).to.have.property('message', 'throttlingActive_requestsIsDelayed')
|
|
202
|
+
expect(e).to.have.property('status', 900)
|
|
203
|
+
}
|
|
200
204
|
})
|
|
201
205
|
})
|
|
202
206
|
})
|
|
203
|
-
})
|
|
204
207
|
|
|
208
|
+
describe('Test section #5 - no limiting', function () {
|
|
209
|
+
describe('RATE LIMITER TEST', function() {
|
|
210
|
+
this.timeout(5000)
|
|
211
|
+
let req = {
|
|
212
|
+
options: {
|
|
213
|
+
controller: 'search',
|
|
214
|
+
action: 'search'
|
|
215
|
+
},
|
|
216
|
+
determinedIP: '1.2.3.4'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
it('Reset Limiter', async() => {
|
|
220
|
+
await ratelimiter.resetLimiter()
|
|
221
|
+
})
|
|
205
222
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
options: {
|
|
211
|
-
controller: 'search',
|
|
212
|
-
action: 'search'
|
|
213
|
-
},
|
|
214
|
-
determinedIP: '1.2.3.4'
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
it('init tests', done => {
|
|
218
|
-
ratelimiter.init(init)
|
|
219
|
-
return done()
|
|
220
|
-
})
|
|
223
|
+
it('should not trigger #1', async() => {
|
|
224
|
+
let result = await ratelimiter.limiter(req, options)
|
|
225
|
+
expect(result).eql(undefined)
|
|
226
|
+
})
|
|
221
227
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
expect(
|
|
225
|
-
return done()
|
|
228
|
+
it('should not trigger #2', async() => {
|
|
229
|
+
let result = await ratelimiter.limiter(req, options)
|
|
230
|
+
expect(result).eql(undefined)
|
|
226
231
|
})
|
|
227
|
-
})
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
expect(
|
|
232
|
-
|
|
233
|
+
it('should not trigger #3', async() => {
|
|
234
|
+
let result = await ratelimiter.limiter(req, options)
|
|
235
|
+
expect(result).eql(undefined)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should not trigger #4', async() => {
|
|
239
|
+
let result = await ratelimiter.limiter(req, options)
|
|
240
|
+
expect(result).eql(undefined)
|
|
233
241
|
})
|
|
234
|
-
})
|
|
235
242
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
expect(
|
|
239
|
-
return done()
|
|
243
|
+
it('should not trigger #5', async() => {
|
|
244
|
+
let result = await ratelimiter.limiter(req, options)
|
|
245
|
+
expect(result).eql(undefined)
|
|
240
246
|
})
|
|
241
247
|
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('Test section #6 - ignore ignorePrivateIps', function () {
|
|
251
|
+
describe('RATE LIMITER TEST - ignorePrivateIps', function() {
|
|
252
|
+
this.timeout(5000)
|
|
242
253
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
let req = {
|
|
255
|
+
options: {
|
|
256
|
+
controller: 'user',
|
|
257
|
+
action: 'find'
|
|
258
|
+
},
|
|
259
|
+
determinedIP: '4.1.4.1'
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
it('Reset Limiter', async() => {
|
|
263
|
+
await ratelimiter.resetLimiter()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('Update settings', async() => {
|
|
267
|
+
await ratelimiter.updateLimiter({
|
|
268
|
+
routes: [
|
|
269
|
+
{ route: 'user/find', limit: 0, expires: 3, delay: 250 }
|
|
270
|
+
],
|
|
271
|
+
ignorePrivateIps: true
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('should trigger immediately', async() => {
|
|
276
|
+
try {
|
|
277
|
+
await ratelimiter.limiter(req, options)
|
|
278
|
+
}
|
|
279
|
+
catch(e) {
|
|
280
|
+
expect(e).to.have.property('message', 'tooManyRequestsFromThisIP')
|
|
281
|
+
expect(e).to.have.property('status', 429)
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('should not trigger', async() => {
|
|
286
|
+
req.determinedIP = '127.0.0.1'
|
|
287
|
+
let result = await ratelimiter.limiter(req, options)
|
|
288
|
+
expect(result).eql(undefined)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('should not trigger - IPv6', async() => {
|
|
292
|
+
req.determinedIP = '::ffff:127.0.0.1'
|
|
293
|
+
let result = await ratelimiter.limiter(req, options)
|
|
294
|
+
expect(result).eql(undefined)
|
|
247
295
|
})
|
|
248
296
|
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('Test section #7 - send rateLimitCounter with request', function() {
|
|
300
|
+
describe('RATE LIMITER TEST - send rateLimitCounter', function() {
|
|
301
|
+
this.timeout(5000)
|
|
249
302
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
expect(err).eql(null)
|
|
253
|
-
return done()
|
|
303
|
+
it('Reset Limiter', async() => {
|
|
304
|
+
await ratelimiter.resetLimiter()
|
|
254
305
|
})
|
|
306
|
+
|
|
307
|
+
it('Update settings', async() => {
|
|
308
|
+
await ratelimiter.updateLimiter({
|
|
309
|
+
routes: [
|
|
310
|
+
{ route: 'user/find', limit: 2, expires: 3, delay: 250 }
|
|
311
|
+
]
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('should not trigger', async() => {
|
|
316
|
+
let result = await ratelimiter.limiter(req, { rateLimitCounter: 0 })
|
|
317
|
+
expect(result).eql(undefined)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should trigger immediately', async() => {
|
|
321
|
+
try {
|
|
322
|
+
await ratelimiter.limiter(req, { rateLimitCounter: 100 })
|
|
323
|
+
}
|
|
324
|
+
catch(e) {
|
|
325
|
+
expect(e).to.have.property('message', 'tooManyRequestsFromThisIP')
|
|
326
|
+
expect(e).to.have.property('status', 429)
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
|
|
255
331
|
})
|
|
256
332
|
})
|
|
333
|
+
|
|
257
334
|
})
|
|
258
335
|
|
|
336
|
+
describe('Use Redis', () => {
|
|
337
|
+
|
|
338
|
+
after(() => {
|
|
339
|
+
redis.quit()
|
|
340
|
+
})
|
|
259
341
|
|
|
260
|
-
describe('
|
|
261
|
-
describe('RATE LIMITER TEST - LOCAL IPS', function() {
|
|
342
|
+
describe('Use Redis for ratelimiter', function() {
|
|
262
343
|
this.timeout(5000)
|
|
263
344
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
]
|
|
270
|
-
ratelimiter.init(init)
|
|
271
|
-
return done()
|
|
345
|
+
it('Update settings', async() => {
|
|
346
|
+
await ratelimiterRedis.updateLimiter({
|
|
347
|
+
routes: [
|
|
348
|
+
{ route: 'user/find', limit: 0, expires: 2, delay: 250 },
|
|
349
|
+
],
|
|
272
350
|
})
|
|
273
|
-
}
|
|
351
|
+
})
|
|
274
352
|
|
|
275
|
-
it('should trigger immediately',
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
353
|
+
it('should trigger immediately', async() => {
|
|
354
|
+
req.determinedIP = '4.1.4.1'
|
|
355
|
+
try {
|
|
356
|
+
let x = await ratelimiterRedis.limiter(req, options)
|
|
357
|
+
console.log(304, x)
|
|
358
|
+
}
|
|
359
|
+
catch(e) {
|
|
360
|
+
expect(e).to.be.an('error')
|
|
361
|
+
expect(e).to.have.property('message', 'tooManyRequestsFromThisIP')
|
|
362
|
+
expect(e).to.have.property('status', 429)
|
|
363
|
+
}
|
|
280
364
|
})
|
|
281
365
|
|
|
282
|
-
it('
|
|
283
|
-
|
|
284
|
-
ratelimiter.limiter(req, options, (err) => {
|
|
285
|
-
expect(err).eql(null)
|
|
286
|
-
return done()
|
|
287
|
-
})
|
|
366
|
+
it('wait for rate limiter to reset', async() => {
|
|
367
|
+
await setTimeout(3000)
|
|
288
368
|
})
|
|
289
369
|
|
|
290
|
-
it('
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
370
|
+
it('Update settings', async() => {
|
|
371
|
+
await ratelimiterRedis.updateLimiter({
|
|
372
|
+
routes: [
|
|
373
|
+
{ route: 'user/find', throttleLimit: 1, limit: 2, expires: 3, delay: 250 },
|
|
374
|
+
],
|
|
295
375
|
})
|
|
296
376
|
})
|
|
377
|
+
|
|
378
|
+
it('req #1 should not trigger', async() => {
|
|
379
|
+
let result = await ratelimiterRedis.limiter(req, options)
|
|
380
|
+
expect(result).eql(undefined)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('req #2 should not trigger throttle warning', async() => {
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await ratelimiterRedis.limiter(req, options)
|
|
387
|
+
}
|
|
388
|
+
catch(e) {
|
|
389
|
+
expect(e).to.be.an('error')
|
|
390
|
+
expect(e).to.have.property('message', 'finalThrottlingActive_requestsIsDelayed')
|
|
391
|
+
expect(e).to.have.property('status', 900)
|
|
392
|
+
expect(e.additionalInfo).to.have.property('counter', 2)
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('req #3 should not trigger the limiter - the throttling has reset the limiter', async() => {
|
|
397
|
+
let result = await ratelimiterRedis.limiter(req, options)
|
|
398
|
+
expect(result).eql(undefined)
|
|
399
|
+
})
|
|
400
|
+
|
|
297
401
|
})
|
|
402
|
+
|
|
298
403
|
})
|