rate-limiter-flexible 8.0.1 → 8.2.0

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/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## node-rate-limiter-flexible
12
12
 
13
- **rate-limiter-flexible** counts and limits the number of actions by key and protects from DDoS and brute force attacks at any scale.
13
+ **rate-limiter-flexible** counts and limits the number of actions by key and protects from DoS and brute force attacks at any scale.
14
14
 
15
15
  It works with _Valkey_, _Redis_, _Prisma_, _DynamoDB_, process _Memory_, _Cluster_ or _PM2_, _Memcached_, _MongoDB_, _MySQL_, _SQLite_, and _PostgreSQL_.
16
16
 
@@ -96,7 +96,7 @@ const headers = {
96
96
  * no race conditions
97
97
  * no production dependencies
98
98
  * TypeScript declaration bundled
99
- * Block Strategy against really powerful DDoS attacks (like 100k requests per sec) [Read about it and benchmarking here](https://github.com/animir/node-rate-limiter-flexible/wiki/In-memory-Block-Strategy)
99
+ * Block Strategy against really powerful DoS attacks (like 100k requests per sec) [Read about it and benchmarking here](https://github.com/animir/node-rate-limiter-flexible/wiki/In-memory-Block-Strategy)
100
100
  * Insurance Strategy as emergency solution if database/store is down [Read about Insurance Strategy here](https://github.com/animir/node-rate-limiter-flexible/wiki/Insurance-Strategy)
101
101
  * works in Cluster or PM2 without additional software [See RateLimiterCluster benchmark and detailed description here](https://github.com/animir/node-rate-limiter-flexible/wiki/Cluster)
102
102
  * useful `get`, `set`, `block`, `delete`, `penalty` and `reward` methods
@@ -147,6 +147,7 @@ Some copy/paste examples on Wiki:
147
147
  * [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter) Traffic burst support
148
148
  * [RateLimiterUnion](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterUnion) Combine 2 or more limiters to act as single
149
149
  * [RLWrapperBlackAndWhite](https://github.com/animir/node-rate-limiter-flexible/wiki/Black-and-White-lists) Black and White lists
150
+ * [RLWrapperTimeouts](https://github.com/animir/node-rate-limiter-flexible/wiki/RLWrapperTimeouts) Timeouts
150
151
  * [RateLimiterQueue](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterQueue) Rate limiter with FIFO queue
151
152
  * [AWS SDK v3 Client Rate Limiter](https://github.com/animir/node-rate-limiter-flexible/wiki/AWS-SDK-v3-Client-Rate-Limiter) Prevent punishing rate limit.
152
153
 
@@ -0,0 +1,82 @@
1
+ const RateLimiterAbstract = require('./RateLimiterAbstract');
2
+ const RateLimiterInsuredAbstract = require('./RateLimiterInsuredAbstract');
3
+
4
+ module.exports = class RLWrapperTimeouts extends RateLimiterInsuredAbstract {
5
+ constructor(opts= {}) {
6
+ super(opts);
7
+ this.limiter = opts.limiter;
8
+ this.timeoutMs = opts.timeoutMs || 0;
9
+ }
10
+
11
+ get limiter() {
12
+ return this._limiter;
13
+ }
14
+
15
+ set limiter(limiter) {
16
+ if (!(limiter instanceof RateLimiterAbstract)) {
17
+ throw new TypeError('limiter must be an instance of RateLimiterAbstract');
18
+ }
19
+ this._limiter = limiter;
20
+ if (!this.insuranceLimiter && limiter instanceof RateLimiterInsuredAbstract) {
21
+ this.insuranceLimiter = limiter.insuranceLimiter;
22
+ }
23
+ }
24
+
25
+ get timeoutMs() {
26
+ return this._timeoutMs;
27
+ }
28
+
29
+ set timeoutMs(value) {
30
+ if (typeof value !== 'number' || value < 0) {
31
+ throw new TypeError('timeoutMs must be a non-negative number');
32
+ }
33
+ this._timeoutMs = value;
34
+ }
35
+
36
+ _run(funcName, params) {
37
+ return new Promise(async (resolve, reject) => {
38
+ const timeout = setTimeout(() => {
39
+ return reject(new Error('Operation timed out'));
40
+ }, this.timeoutMs);
41
+
42
+ await this.limiter[funcName](...params)
43
+ .then((result) => {
44
+ clearTimeout(timeout);
45
+ resolve(result);
46
+ })
47
+ .catch((err) => {
48
+ clearTimeout(timeout);
49
+ reject(err);
50
+ });
51
+ });
52
+ }
53
+
54
+ _consume(key, pointsToConsume = 1, options = {}) {
55
+ return this._run('consume', [key, pointsToConsume, options]);
56
+ }
57
+
58
+ _penalty(key, points = 1, options = {}) {
59
+ return this._run('penalty', [key, points, options]);
60
+ }
61
+
62
+ _reward(key, points = 1, options = {}) {
63
+ return this._run('reward', [key, points, options]);
64
+ }
65
+
66
+ _get(key, options = {}) {
67
+ return this._run('get', [key, options]);
68
+ }
69
+
70
+ _set(key, points, secDuration, options = {}) {
71
+ return this._run('set', [key, points, secDuration, options]);
72
+ }
73
+
74
+ _block(key, secDuration, options = {}) {
75
+ return this._run('block', [key, secDuration, options]);
76
+ }
77
+
78
+ _delete(key, options = {}) {
79
+ return this._run('delete', [key, options]);
80
+ }
81
+
82
+ }
@@ -0,0 +1,109 @@
1
+ const RateLimiterAbstract = require('./RateLimiterAbstract');
2
+
3
+ module.exports = class RateLimiterInsuredAbstract extends RateLimiterAbstract {
4
+ constructor(opts = {}) {
5
+ super(opts);
6
+ this.insuranceLimiter = opts.insuranceLimiter;
7
+ }
8
+
9
+ get insuranceLimiter() {
10
+ return this._insuranceLimiter;
11
+ }
12
+
13
+ set insuranceLimiter(value) {
14
+ if (typeof value !== 'undefined' && !(value instanceof RateLimiterAbstract)) {
15
+ throw new Error('insuranceLimiter must be instance of RateLimiterAbstract');
16
+ }
17
+ this._insuranceLimiter = value;
18
+ if (this._insuranceLimiter) {
19
+ this._insuranceLimiter.blockDuration = this.blockDuration;
20
+ this._insuranceLimiter.execEvenly = this.execEvenly;
21
+ }
22
+ }
23
+
24
+ _handleError(err, funcName, resolve, reject, params) {
25
+ if (!(this.insuranceLimiter instanceof RateLimiterAbstract)) {
26
+ reject(err);
27
+ } else {
28
+ this.insuranceLimiter[funcName](...params)
29
+ .then((res) => {
30
+ resolve(res);
31
+ })
32
+ .catch((res) => {
33
+ reject(res);
34
+ });
35
+ }
36
+ }
37
+
38
+ _operation(funcName, params) {
39
+ const promise = this[funcName](...params);
40
+ return new Promise((resolve, reject) => {
41
+ return promise.then((res) => {
42
+ resolve(res);
43
+ })
44
+ .catch((err) => {
45
+ if (funcName.startsWith('_')) {
46
+ funcName = funcName.slice(1);
47
+ }
48
+ this._handleError(err, funcName, resolve, reject, params);
49
+ });
50
+ });
51
+ }
52
+
53
+ consume(key, pointsToConsume = 1, options = {}) {
54
+ return this._operation('_consume', [key, pointsToConsume, options]);
55
+ }
56
+
57
+ penalty(key, points = 1, options = {}) {
58
+ return this._operation('_penalty', [key, points, options]);
59
+ }
60
+
61
+ reward(key, points = 1, options = {}) {
62
+ return this._operation('_reward', [key, points, options]);
63
+ }
64
+
65
+ get(key, options = {}) {
66
+ return this._operation('_get', [key, options]);
67
+ }
68
+
69
+ set(key, points, secDuration, options = {}) {
70
+ return this._operation('_set', [key, points, secDuration, options]);
71
+ }
72
+
73
+ block(key, secDuration, options = {}) {
74
+ return this._operation('_block', [key, secDuration, options]);
75
+ }
76
+
77
+ delete(key, options = {}) {
78
+ return this._operation('_delete', [key, options]);
79
+ }
80
+
81
+ _consume() {
82
+ throw new Error("You have to implement the method '_consume'!");
83
+ }
84
+
85
+ _penalty() {
86
+ throw new Error("You have to implement the method '_penalty'!");
87
+ }
88
+
89
+ _reward() {
90
+ throw new Error("You have to implement the method '_reward'!");
91
+ }
92
+
93
+ _get() {
94
+ throw new Error("You have to implement the method '_get'!");
95
+ }
96
+
97
+ _set() {
98
+ throw new Error("You have to implement the method '_set'!");
99
+ }
100
+
101
+ _block() {
102
+ throw new Error("You have to implement the method '_block'!");
103
+ }
104
+
105
+ _delete() {
106
+ throw new Error("You have to implement the method '_delete'!");
107
+ }
108
+
109
+ }
@@ -43,20 +43,41 @@ class RateLimiterRedis extends RateLimiterStoreAbstract {
43
43
  * Prevent actual redis call if redis connection is not ready
44
44
  * Because of different connection state checks for ioredis and node-redis, only this clients would be actually checked.
45
45
  * For any other clients all the requests would be passed directly to redis client
46
+ * @param {String} rlKey
47
+ * @param {Boolean} isReadonly
46
48
  * @return {boolean}
47
49
  * @private
48
50
  */
49
- _isRedisReady() {
51
+ _isRedisReady(rlKey, isReadonly) {
50
52
  if (!this._rejectIfRedisNotReady) {
51
53
  return true;
52
54
  }
53
55
  // ioredis client
54
- if (this.client.status && this.client.status !== 'ready') {
55
- return false;
56
+ if (this.client.status) {
57
+ return this.client.status === 'ready';
56
58
  }
57
- // node-redis client
58
- if (typeof this.client.isReady === 'function' && !this.client.isReady()) {
59
- return false;
59
+ // node-redis v3 client
60
+ if (typeof this.client.isReady === 'function') {
61
+ return this.client.isReady();
62
+ }
63
+
64
+ // node-redis v4+ (non-cluster) client
65
+ if (typeof this.client.isReady === 'boolean') {
66
+ return this.client.isReady === true;
67
+ }
68
+
69
+ // node-redis v4+ cluster client
70
+ if (this.client._slots && typeof this.client._slots.getClient === 'function') {
71
+ if (typeof this.client.isOpen === 'boolean' && this.client.isOpen !== true) {
72
+ return false;
73
+ }
74
+
75
+ try {
76
+ const slotClient = this.client._slots.getClient(rlKey, isReadonly);
77
+ return slotClient && slotClient.isReady === true;
78
+ } catch (error) {
79
+ return false;
80
+ }
60
81
  }
61
82
  return true;
62
83
  }
@@ -89,7 +110,7 @@ class RateLimiterRedis extends RateLimiterStoreAbstract {
89
110
  throw new Error("Consuming decimal number of points is not supported by this package");
90
111
  }
91
112
 
92
- if (!this._isRedisReady()) {
113
+ if (!this._isRedisReady(rlKey, false)) {
93
114
  throw new Error('Redis connection is not ready');
94
115
  }
95
116
 
@@ -150,7 +171,7 @@ class RateLimiterRedis extends RateLimiterStoreAbstract {
150
171
  }
151
172
 
152
173
  async _get(rlKey) {
153
- if (!this._isRedisReady()) {
174
+ if (!this._isRedisReady(rlKey, true)) {
154
175
  throw new Error('Redis connection is not ready');
155
176
  }
156
177
  if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){
@@ -1,8 +1,9 @@
1
1
  const RateLimiterAbstract = require('./RateLimiterAbstract');
2
2
  const BlockedKeys = require('./component/BlockedKeys');
3
3
  const RateLimiterRes = require('./RateLimiterRes');
4
+ const RateLimiterInsuredAbstract = require('./RateLimiterInsuredAbstract');
4
5
 
5
- module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
6
+ module.exports = class RateLimiterStoreAbstract extends RateLimiterInsuredAbstract {
6
7
  /**
7
8
  *
8
9
  * @param opts Object Defaults {
@@ -18,7 +19,6 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
18
19
 
19
20
  this.inMemoryBlockOnConsumed = opts.inMemoryBlockOnConsumed;
20
21
  this.inMemoryBlockDuration = opts.inMemoryBlockDuration;
21
- this.insuranceLimiter = opts.insuranceLimiter;
22
22
  this._inMemoryBlockedKeys = new BlockedKeys();
23
23
  }
24
24
 
@@ -92,20 +92,6 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
92
92
  }
93
93
  }
94
94
 
95
- _handleError(err, funcName, resolve, reject, key, data = false, options = {}) {
96
- if (!(this.insuranceLimiter instanceof RateLimiterAbstract)) {
97
- reject(err);
98
- } else {
99
- this.insuranceLimiter[funcName](key, data, options)
100
- .then((res) => {
101
- resolve(res);
102
- })
103
- .catch((res) => {
104
- reject(res);
105
- });
106
- }
107
- }
108
-
109
95
  getInMemoryBlockMsBeforeExpire(rlKey) {
110
96
  if (this.inMemoryBlockOnConsumed > 0) {
111
97
  return this._inMemoryBlockedKeys.msBeforeExpire(rlKey);
@@ -140,21 +126,6 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
140
126
  return this._inMemoryBlockDuration * 1000;
141
127
  }
142
128
 
143
- get insuranceLimiter() {
144
- return this._insuranceLimiter;
145
- }
146
-
147
- set insuranceLimiter(value) {
148
- if (typeof value !== 'undefined' && !(value instanceof RateLimiterAbstract)) {
149
- throw new Error('insuranceLimiter must be instance of RateLimiterAbstract');
150
- }
151
- this._insuranceLimiter = value;
152
- if (this._insuranceLimiter) {
153
- this._insuranceLimiter.blockDuration = this.blockDuration;
154
- this._insuranceLimiter.execEvenly = this.execEvenly;
155
- }
156
- }
157
-
158
129
  /**
159
130
  * Block any key for secDuration seconds
160
131
  *
@@ -191,7 +162,7 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
191
162
  * @param {Object} options
192
163
  * @returns Promise<RateLimiterRes>
193
164
  */
194
- consume(key, pointsToConsume = 1, options = {}) {
165
+ _consume(key, pointsToConsume = 1, options = {}) {
195
166
  return new Promise((resolve, reject) => {
196
167
  const rlKey = this.getKey(key);
197
168
 
@@ -204,9 +175,7 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
204
175
  .then((res) => {
205
176
  this._afterConsume(resolve, reject, rlKey, pointsToConsume, res);
206
177
  })
207
- .catch((err) => {
208
- this._handleError(err, 'consume', resolve, reject, key, pointsToConsume, options);
209
- });
178
+ .catch((err) => reject(err));
210
179
  });
211
180
  }
212
181
 
@@ -217,16 +186,14 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
217
186
  * @param {Object} options
218
187
  * @returns Promise<RateLimiterRes>
219
188
  */
220
- penalty(key, points = 1, options = {}) {
189
+ _penalty(key, points = 1, options = {}) {
221
190
  const rlKey = this.getKey(key);
222
191
  return new Promise((resolve, reject) => {
223
192
  this._upsert(rlKey, points, this._getKeySecDuration(options) * 1000, false, options)
224
193
  .then((res) => {
225
194
  resolve(this._getRateLimiterRes(rlKey, points, res));
226
195
  })
227
- .catch((err) => {
228
- this._handleError(err, 'penalty', resolve, reject, key, points, options);
229
- });
196
+ .catch((res) => reject(res));
230
197
  });
231
198
  }
232
199
 
@@ -237,16 +204,14 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
237
204
  * @param {Object} options
238
205
  * @returns Promise<RateLimiterRes>
239
206
  */
240
- reward(key, points = 1, options = {}) {
207
+ _reward(key, points = 1, options = {}) {
241
208
  const rlKey = this.getKey(key);
242
209
  return new Promise((resolve, reject) => {
243
210
  this._upsert(rlKey, -points, this._getKeySecDuration(options) * 1000, false, options)
244
211
  .then((res) => {
245
212
  resolve(this._getRateLimiterRes(rlKey, -points, res));
246
213
  })
247
- .catch((err) => {
248
- this._handleError(err, 'reward', resolve, reject, key, points, options);
249
- });
214
+ .catch((res) => reject(res));
250
215
  });
251
216
  }
252
217
 
@@ -268,7 +233,7 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
268
233
  }
269
234
  })
270
235
  .catch((err) => {
271
- this._handleError(err, 'get', resolve, reject, key, options);
236
+ this._handleError(err, 'get', resolve, reject, [key, options]);
272
237
  });
273
238
  });
274
239
  }
@@ -288,7 +253,7 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
288
253
  resolve(res);
289
254
  })
290
255
  .catch((err) => {
291
- this._handleError(err, 'delete', resolve, reject, key, options);
256
+ this._handleError(err, 'delete', resolve, reject, [key, options]);
292
257
  });
293
258
  });
294
259
  }
@@ -330,7 +295,7 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
330
295
  resolve(new RateLimiterRes(0, msDuration > 0 ? msDuration : -1, initPoints));
331
296
  })
332
297
  .catch((err) => {
333
- this._handleError(err, 'block', resolve, reject, this.parseKey(rlKey), msDuration / 1000, options);
298
+ this._handleError(err, 'block', resolve, reject, [this.parseKey(rlKey), msDuration / 1000, options]);
334
299
  });
335
300
  });
336
301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rate-limiter-flexible",
3
- "version": "8.0.1",
3
+ "version": "8.2.0",
4
4
  "description": "Node.js rate limiter by key and protection from DDoS and Brute-Force attacks in process Memory, Redis, MongoDb, Memcached, MySQL, PostgreSQL, Cluster or PM",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/types.d.ts CHANGED
@@ -204,7 +204,11 @@ export class RateLimiterAbstract {
204
204
  ): Promise<boolean>;
205
205
  }
206
206
 
207
- export class RateLimiterStoreAbstract extends RateLimiterAbstract {
207
+ export class RateLimiterInsuredAbstract extends RateLimiterAbstract {
208
+ constructor(opts: IRateLimiterOptions);
209
+ }
210
+
211
+ export class RateLimiterStoreAbstract extends RateLimiterInsuredAbstract {
208
212
  constructor(opts: IRateLimiterStoreOptions);
209
213
 
210
214
  /**
@@ -220,6 +224,7 @@ interface IRateLimiterOptions {
220
224
  execEvenly?: boolean;
221
225
  execEvenlyMinDelayMs?: number;
222
226
  blockDuration?: number;
227
+ insuranceLimiter?: RateLimiterAbstract;
223
228
  }
224
229
 
225
230
  interface IRateLimiterClusterOptions extends IRateLimiterOptions {
@@ -393,6 +398,15 @@ export class RLWrapperBlackAndWhite extends RateLimiterAbstract {
393
398
  constructor(opts: IRLWrapperBlackAndWhiteOptions);
394
399
  }
395
400
 
401
+ interface IRLWrapperTimeoutsOptions extends IRateLimiterOptions {
402
+ limiter: RateLimiterAbstract;
403
+ timeoutMs?: number;
404
+ }
405
+
406
+ export class RLWrapperTimeouts extends RateLimiterInsuredAbstract {
407
+ constructor(opts: IRLWrapperTimeoutsOptions);
408
+ }
409
+
396
410
  interface IRateLimiterQueueOpts {
397
411
  maxQueueSize?: number;
398
412
  }