rate-limiter-flexible 6.1.0 → 6.2.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/lib/RateLimiterSQLite.js +253 -122
- package/package.json +3 -1
package/lib/RateLimiterSQLite.js
CHANGED
|
@@ -2,6 +2,17 @@ const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract");
|
|
|
2
2
|
const RateLimiterRes = require("./RateLimiterRes");
|
|
3
3
|
|
|
4
4
|
class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
5
|
+
/**
|
|
6
|
+
* Internal store type used to determine the SQLite client in use.
|
|
7
|
+
* It can be one of the following:
|
|
8
|
+
* - `"sqlite3".
|
|
9
|
+
* - `"better-sqlite3".
|
|
10
|
+
*
|
|
11
|
+
* @type {("sqlite3" | "better-sqlite3" | null)}
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
_internalStoreType = null;
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* @callback callback
|
|
7
18
|
* @param {Object} err
|
|
@@ -10,66 +21,151 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
|
10
21
|
* @param {callback} cb
|
|
11
22
|
* Defaults {
|
|
12
23
|
* ... see other in RateLimiterStoreAbstract
|
|
13
|
-
*
|
|
14
|
-
*
|
|
24
|
+
* storeClient: sqliteClient, // SQLite database instance (sqlite3, better-sqlite3, or knex instance)
|
|
25
|
+
* storeType: 'sqlite3' | 'better-sqlite3' | 'knex', // Optional, defaults to 'sqlite3'
|
|
15
26
|
* tableName: 'string',
|
|
27
|
+
* tableCreated: boolean,
|
|
28
|
+
* clearExpiredByTimeout: boolean,
|
|
16
29
|
* }
|
|
17
30
|
*/
|
|
18
31
|
constructor(opts, cb = null) {
|
|
19
32
|
super(opts);
|
|
20
33
|
|
|
21
34
|
this.client = opts.storeClient;
|
|
35
|
+
this.storeType = opts.storeType || "sqlite3";
|
|
22
36
|
this.tableName = opts.tableName;
|
|
23
37
|
this.tableCreated = opts.tableCreated || false;
|
|
24
38
|
this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
throw err;
|
|
32
|
-
}
|
|
40
|
+
this._validateStoreTypes(cb);
|
|
41
|
+
this._validateStoreClient(cb);
|
|
42
|
+
this._setInternalStoreType(cb);
|
|
43
|
+
this._validateTableName(cb);
|
|
33
44
|
|
|
34
45
|
if (!this.tableCreated) {
|
|
35
46
|
this._createDbAndTable()
|
|
36
47
|
.then(() => {
|
|
37
48
|
this.tableCreated = true;
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
this._clearExpiredHourAgo();
|
|
41
|
-
}
|
|
42
|
-
if (typeof cb === "function") {
|
|
43
|
-
cb();
|
|
44
|
-
}
|
|
49
|
+
if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
|
|
50
|
+
if (typeof cb === "function") cb();
|
|
45
51
|
})
|
|
46
52
|
.catch((err) => {
|
|
47
|
-
if (typeof cb === "function")
|
|
48
|
-
|
|
49
|
-
} else {
|
|
50
|
-
throw err;
|
|
51
|
-
}
|
|
53
|
+
if (typeof cb === "function") cb(err);
|
|
54
|
+
else throw err;
|
|
52
55
|
});
|
|
53
56
|
} else {
|
|
54
|
-
if (this.clearExpiredByTimeout)
|
|
55
|
-
|
|
57
|
+
if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
|
|
58
|
+
if (typeof cb === "function") cb();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
_validateStoreTypes(cb) {
|
|
62
|
+
const validStoreTypes = ["sqlite3", "better-sqlite3", "knex"];
|
|
63
|
+
if (!validStoreTypes.includes(this.storeType)) {
|
|
64
|
+
const err = new Error(
|
|
65
|
+
`storeType must be one of: ${validStoreTypes.join(", ")}`
|
|
66
|
+
);
|
|
67
|
+
if (typeof cb === "function") return cb(err);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
_validateStoreClient(cb) {
|
|
72
|
+
if (this.storeType === "sqlite3") {
|
|
73
|
+
if (typeof this.client.run !== "function") {
|
|
74
|
+
const err = new Error(
|
|
75
|
+
"storeClient must be an instance of sqlite3.Database when storeType is 'sqlite3' or no storeType was provided"
|
|
76
|
+
);
|
|
77
|
+
if (typeof cb === "function") return cb(err);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
} else if (this.storeType === "better-sqlite3") {
|
|
81
|
+
if (
|
|
82
|
+
typeof this.client.prepare !== "function" ||
|
|
83
|
+
typeof this.client.run !== "undefined"
|
|
84
|
+
) {
|
|
85
|
+
const err = new Error(
|
|
86
|
+
"storeClient must be an instance of better-sqlite3.Database when storeType is 'better-sqlite3'"
|
|
87
|
+
);
|
|
88
|
+
if (typeof cb === "function") return cb(err);
|
|
89
|
+
throw err;
|
|
56
90
|
}
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
} else if (this.storeType === "knex") {
|
|
92
|
+
if (typeof this.client.raw !== "function") {
|
|
93
|
+
const err = new Error(
|
|
94
|
+
"storeClient must be an instance of Knex when storeType is 'knex'"
|
|
95
|
+
);
|
|
96
|
+
if (typeof cb === "function") return cb(err);
|
|
97
|
+
throw err;
|
|
59
98
|
}
|
|
60
99
|
}
|
|
61
100
|
}
|
|
101
|
+
_setInternalStoreType(cb) {
|
|
102
|
+
if (this.storeType === "knex") {
|
|
103
|
+
const knexClientType = this.client.client.config.client;
|
|
104
|
+
if (knexClientType === "sqlite3") {
|
|
105
|
+
this._internalStoreType = "sqlite3";
|
|
106
|
+
} else if (knexClientType === "better-sqlite3") {
|
|
107
|
+
this._internalStoreType = "better-sqlite3";
|
|
108
|
+
} else {
|
|
109
|
+
const err = new Error(
|
|
110
|
+
"Knex must be configured with 'sqlite3' or 'better-sqlite3' for RateLimiterSQLite"
|
|
111
|
+
);
|
|
112
|
+
if (typeof cb === "function") return cb(err);
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this._internalStoreType = this.storeType;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
_validateTableName(cb) {
|
|
120
|
+
if (!/^[A-Za-z0-9_]*$/.test(this.tableName)) {
|
|
121
|
+
const err = new Error("Table name must contain only letters and numbers");
|
|
122
|
+
if (typeof cb === "function") return cb(err);
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Acquires the database connection based on the storeType.
|
|
129
|
+
* @returns {Promise<Object>} The database client or connection
|
|
130
|
+
*/
|
|
131
|
+
async _getConnection() {
|
|
132
|
+
if (this.storeType === "knex") {
|
|
133
|
+
return this.client.client.acquireConnection(); // Acquire raw connection from knex pool
|
|
134
|
+
}
|
|
135
|
+
return this.client; // For sqlite3 and better-sqlite3, return the client directly
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Releases the database connection if necessary.
|
|
140
|
+
* @param {Object} conn The database client or connection
|
|
141
|
+
*/
|
|
142
|
+
_releaseConnection(conn) {
|
|
143
|
+
if (this.storeType === "knex") {
|
|
144
|
+
this.client.client.releaseConnection(conn);
|
|
145
|
+
}
|
|
146
|
+
// No release needed for direct sqlite3 or better-sqlite3 clients
|
|
147
|
+
}
|
|
148
|
+
|
|
62
149
|
async _createDbAndTable() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
reject
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
150
|
+
const conn = await this._getConnection();
|
|
151
|
+
try {
|
|
152
|
+
switch (this._internalStoreType) {
|
|
153
|
+
case "sqlite3":
|
|
154
|
+
await new Promise((resolve, reject) => {
|
|
155
|
+
conn.run(this._getCreateTableSQL(), (err) =>
|
|
156
|
+
err ? reject(err) : resolve()
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
break;
|
|
160
|
+
case "better-sqlite3":
|
|
161
|
+
conn.prepare(this._getCreateTableSQL()).run();
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
throw new Error("Unsupported internalStoreType");
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
this._releaseConnection(conn);
|
|
168
|
+
}
|
|
73
169
|
}
|
|
74
170
|
|
|
75
171
|
_getCreateTableSQL() {
|
|
@@ -81,26 +177,33 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
|
81
177
|
}
|
|
82
178
|
|
|
83
179
|
_clearExpiredHourAgo() {
|
|
84
|
-
if (this._clearExpiredTimeoutId)
|
|
85
|
-
clearTimeout(this._clearExpiredTimeoutId);
|
|
86
|
-
}
|
|
180
|
+
if (this._clearExpiredTimeoutId) clearTimeout(this._clearExpiredTimeoutId);
|
|
87
181
|
this._clearExpiredTimeoutId = setTimeout(() => {
|
|
88
|
-
this.clearExpired(Date.now() - 3600000) //
|
|
89
|
-
.then(() =>
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
}, 300000);
|
|
182
|
+
this.clearExpired(Date.now() - 3600000) // 1 hour ago
|
|
183
|
+
.then(() => this._clearExpiredHourAgo());
|
|
184
|
+
}, 300000); // Every 5 minutes
|
|
93
185
|
this._clearExpiredTimeoutId.unref();
|
|
94
186
|
}
|
|
95
187
|
|
|
96
|
-
clearExpired(nowMs) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
188
|
+
async clearExpired(nowMs) {
|
|
189
|
+
const sql = `DELETE FROM ${this.tableName} WHERE expire < ?`;
|
|
190
|
+
const conn = await this._getConnection();
|
|
191
|
+
try {
|
|
192
|
+
switch (this._internalStoreType) {
|
|
193
|
+
case "sqlite3":
|
|
194
|
+
await new Promise((resolve, reject) => {
|
|
195
|
+
conn.run(sql, [nowMs], (err) => (err ? reject(err) : resolve()));
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
case "better-sqlite3":
|
|
199
|
+
conn.prepare(sql).run(nowMs);
|
|
200
|
+
break;
|
|
201
|
+
default:
|
|
202
|
+
throw new Error("Unsupported internalStoreType");
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
this._releaseConnection(conn);
|
|
206
|
+
}
|
|
104
207
|
}
|
|
105
208
|
|
|
106
209
|
_getRateLimiterRes(rlKey, changedPoints, result) {
|
|
@@ -111,96 +214,124 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
|
111
214
|
res.msBeforeNext = result.expire
|
|
112
215
|
? Math.max(result.expire - Date.now(), 0)
|
|
113
216
|
: -1;
|
|
114
|
-
|
|
115
217
|
return res;
|
|
116
218
|
}
|
|
117
219
|
|
|
118
|
-
async
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
VALUES (?, ?, ?)
|
|
128
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
129
|
-
points = CASE
|
|
130
|
-
WHEN expire IS NULL OR expire > ? THEN points + excluded.points
|
|
131
|
-
ELSE excluded.points
|
|
132
|
-
END,
|
|
133
|
-
expire = CASE
|
|
134
|
-
WHEN expire IS NULL OR expire > ? THEN expire
|
|
135
|
-
ELSE excluded.expire
|
|
136
|
-
END
|
|
137
|
-
RETURNING points, expire;
|
|
138
|
-
`;
|
|
139
|
-
|
|
140
|
-
const upsertParams = forceExpire
|
|
141
|
-
? [rlKey, points, newExpire]
|
|
142
|
-
: [rlKey, points, newExpire, dateNow, dateNow];
|
|
143
|
-
|
|
144
|
-
this.client.serialize(() => {
|
|
145
|
-
this.client.run("SAVEPOINT rate_limiter_trx;", (savepointErr) => {
|
|
146
|
-
if (savepointErr) return reject(savepointErr);
|
|
147
|
-
|
|
148
|
-
this.client.get(upsertQuery, upsertParams, (queryErr, row) => {
|
|
149
|
-
if (queryErr) {
|
|
150
|
-
return this.client.run(
|
|
151
|
-
"ROLLBACK TO SAVEPOINT rate_limiter_trx;",
|
|
152
|
-
() => reject(queryErr)
|
|
220
|
+
async _upsertTransactionSQLite3(conn, upsertQuery, upsertParams) {
|
|
221
|
+
return await new Promise((resolve, reject) => {
|
|
222
|
+
conn.serialize(() => {
|
|
223
|
+
conn.run("SAVEPOINT rate_limiter_trx;", (err) => {
|
|
224
|
+
if (err) return reject(err);
|
|
225
|
+
conn.get(upsertQuery, upsertParams, (err, row) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
conn.run("ROLLBACK TO SAVEPOINT rate_limiter_trx;", () =>
|
|
228
|
+
reject(err)
|
|
153
229
|
);
|
|
230
|
+
return;
|
|
154
231
|
}
|
|
155
|
-
|
|
156
|
-
this.client.run("RELEASE SAVEPOINT rate_limiter_trx;", () =>
|
|
157
|
-
resolve(row)
|
|
158
|
-
);
|
|
232
|
+
conn.run("RELEASE SAVEPOINT rate_limiter_trx;", () => resolve(row));
|
|
159
233
|
});
|
|
160
234
|
});
|
|
161
235
|
});
|
|
162
236
|
});
|
|
163
237
|
}
|
|
238
|
+
|
|
239
|
+
async _upsertTransactionBetterSQLite3(conn, upsertQuery, upsertParams) {
|
|
240
|
+
return conn.transaction(() =>
|
|
241
|
+
conn.prepare(upsertQuery).get(...upsertParams)
|
|
242
|
+
)();
|
|
243
|
+
}
|
|
244
|
+
async _upsertTransaction(rlKey, points, msDuration, forceExpire) {
|
|
245
|
+
const dateNow = Date.now();
|
|
246
|
+
const newExpire = msDuration > 0 ? dateNow + msDuration : null;
|
|
247
|
+
const upsertQuery = forceExpire
|
|
248
|
+
? `INSERT OR REPLACE INTO ${this.tableName} (key, points, expire) VALUES (?, ?, ?) RETURNING points, expire`
|
|
249
|
+
: `INSERT INTO ${this.tableName} (key, points, expire)
|
|
250
|
+
VALUES (?, ?, ?)
|
|
251
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
252
|
+
points = CASE WHEN expire IS NULL OR expire > ? THEN points + excluded.points ELSE excluded.points END,
|
|
253
|
+
expire = CASE WHEN expire IS NULL OR expire > ? THEN expire ELSE excluded.expire END
|
|
254
|
+
RETURNING points, expire`;
|
|
255
|
+
const upsertParams = forceExpire
|
|
256
|
+
? [rlKey, points, newExpire]
|
|
257
|
+
: [rlKey, points, newExpire, dateNow, dateNow];
|
|
258
|
+
|
|
259
|
+
const conn = await this._getConnection();
|
|
260
|
+
try {
|
|
261
|
+
switch (this._internalStoreType) {
|
|
262
|
+
case "sqlite3":
|
|
263
|
+
return this._upsertTransactionSQLite3(
|
|
264
|
+
conn,
|
|
265
|
+
upsertQuery,
|
|
266
|
+
upsertParams
|
|
267
|
+
);
|
|
268
|
+
case "better-sqlite3":
|
|
269
|
+
return this._upsertTransactionBetterSQLite3(
|
|
270
|
+
conn,
|
|
271
|
+
upsertQuery,
|
|
272
|
+
upsertParams
|
|
273
|
+
);
|
|
274
|
+
default:
|
|
275
|
+
throw new Error("Unsupported internalStoreType");
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
this._releaseConnection(conn);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
164
282
|
_upsert(rlKey, points, msDuration, forceExpire = false) {
|
|
165
283
|
if (!this.tableCreated) {
|
|
166
|
-
return Promise.reject(Error("Table is not created yet"));
|
|
284
|
+
return Promise.reject(new Error("Table is not created yet"));
|
|
167
285
|
}
|
|
168
286
|
return this._upsertTransaction(rlKey, points, msDuration, forceExpire);
|
|
169
287
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
288
|
+
|
|
289
|
+
async _get(rlKey) {
|
|
290
|
+
const sql = `SELECT points, expire FROM ${this.tableName} WHERE key = ? AND (expire > ? OR expire IS NULL)`;
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const conn = await this._getConnection();
|
|
293
|
+
try {
|
|
294
|
+
switch (this._internalStoreType) {
|
|
295
|
+
case "sqlite3":
|
|
296
|
+
return await new Promise((resolve, reject) => {
|
|
297
|
+
conn.get(sql, [rlKey, now], (err, row) =>
|
|
298
|
+
err ? reject(err) : resolve(row || null)
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
case "better-sqlite3":
|
|
302
|
+
return conn.prepare(sql).get(rlKey, now) || null;
|
|
303
|
+
default:
|
|
304
|
+
throw new Error("Unsupported internalStoreType");
|
|
305
|
+
}
|
|
306
|
+
} finally {
|
|
307
|
+
this._releaseConnection(conn);
|
|
308
|
+
}
|
|
184
309
|
}
|
|
185
310
|
|
|
186
|
-
_delete(rlKey) {
|
|
311
|
+
async _delete(rlKey) {
|
|
187
312
|
if (!this.tableCreated) {
|
|
188
|
-
return Promise.reject(Error("Table is not created yet"));
|
|
313
|
+
return Promise.reject(new Error("Table is not created yet"));
|
|
314
|
+
}
|
|
315
|
+
const sql = `DELETE FROM ${this.tableName} WHERE key = ?`;
|
|
316
|
+
const conn = await this._getConnection();
|
|
317
|
+
try {
|
|
318
|
+
switch (this._internalStoreType) {
|
|
319
|
+
case "sqlite3":
|
|
320
|
+
return await new Promise((resolve, reject) => {
|
|
321
|
+
conn.run(sql, [rlKey], function (err) {
|
|
322
|
+
if (err) reject(err);
|
|
323
|
+
else resolve(this.changes > 0);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
case "better-sqlite3":
|
|
327
|
+
const result = conn.prepare(sql).run(rlKey);
|
|
328
|
+
return result.changes > 0;
|
|
329
|
+
default:
|
|
330
|
+
throw new Error("Unsupported internalStoreType");
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
this._releaseConnection(conn);
|
|
189
334
|
}
|
|
190
|
-
|
|
191
|
-
return new Promise((resolve, reject) => {
|
|
192
|
-
this.client.run(
|
|
193
|
-
`DELETE FROM ${this.tableName} WHERE key = ?`,
|
|
194
|
-
[rlKey],
|
|
195
|
-
function (err) {
|
|
196
|
-
if (err) {
|
|
197
|
-
reject(err);
|
|
198
|
-
} else {
|
|
199
|
-
resolve(this.changes > 0);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
);
|
|
203
|
-
});
|
|
204
335
|
}
|
|
205
336
|
}
|
|
206
337
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rate-limiter-flexible",
|
|
3
|
-
"version": "6.1
|
|
3
|
+
"version": "6.2.1",
|
|
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": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@aws-sdk/client-dynamodb": "^3.431.0",
|
|
47
47
|
"@prisma/client": "^5.8.0",
|
|
48
|
+
"better-sqlite3": "^11.9.0",
|
|
48
49
|
"chai": "^4.1.2",
|
|
49
50
|
"coveralls": "^3.0.1",
|
|
50
51
|
"eslint": "^4.19.1",
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"ioredis": "^5.3.2",
|
|
56
57
|
"iovalkey": "^0.3.1",
|
|
57
58
|
"istanbul": "^1.1.0-alpha.1",
|
|
59
|
+
"knex": "^3.1.0",
|
|
58
60
|
"memcached-mock": "^0.1.0",
|
|
59
61
|
"mocha": "^10.2.0",
|
|
60
62
|
"nyc": "^15.1.0",
|