node_modules ignore

This commit is contained in:
2025-05-08 23:43:47 +02:00
parent e19d52f172
commit 4574544c9f
65041 changed files with 10593536 additions and 0 deletions

22
server/node_modules/koa2-ratelimit/.eslintrc.js generated vendored Normal file
View File

@@ -0,0 +1,22 @@
module.exports = {
"env": {
"mocha": true,
"node": true,
"protractor": true,
"es6": true
},
"extends": "airbnb",
"parser": "babel-eslint",
"rules": {
//"import/no-extraneous-dependencies": ["error", { "devDependencies": ["**/*.spec.js"] }],
"indent": ["error", 4],
"no-param-reassign": 0,
"class-methods-use-this": 0,
"no-underscore-dangle": 0,
"no-restricted-syntax": 0,
"no-await-in-loop": 0,
"arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": true }],
"camelcase": 0,
"max-len": 0,
}
}

3
server/node_modules/koa2-ratelimit/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- "8.9.1"

21
server/node_modules/koa2-ratelimit/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 YSO Corp
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.

334
server/node_modules/koa2-ratelimit/README.md generated vendored Normal file
View File

@@ -0,0 +1,334 @@
# Koajs 2 Rate Limit (Bruteforce)
[![Build Status](https://secure.travis-ci.org/ysocorp/koa2-ratelimit.png?branch=master "Test")](http://travis-ci.org/ysocorp/koa2-ratelimit)
[![NPM version](http://badge.fury.io/js/koa2-ratelimit.png)](https://npmjs.org/package/koa2-ratelimit "View this project on NPM")
Rate-limiting middleware for Koa2 with `async` `await`. Use to limit repeated requests to APIs and/or endpoints such as password reset.
Note: This module is based on [express-rate-limit](https://github.com/nfriedly/express-rate-limit) and adapted to koa2 ES6 with the `async` `await` capabilities.
## Summary
- [Install](#install)
- [Usage](#usage)
- [Use with RedisStore](#use-with-redisStore)
- [Use with SequelizeStore](#use-with-sequelizestore)
- [Use with MongooseStore (Mongodb)](#use-with-mongoosestore)
- [Configuration](#configuration)
- [Time Type](#time-type)
- [Upgrade](#upgrade)
- [0.9.1 to 1.0.0](#0.9.1-to-1.0.0)
## Install
```sh
$ npm install --save koa2-ratelimit
```
## Usage
For an API-only server where the rate-limiter should be applied to all requests:
```js
const RateLimit = require('koa2-ratelimit').RateLimit;
const limiter = RateLimit.middleware({
interval: { min: 15 }, // 15 minutes = 15*60*1000
max: 100, // limit each IP to 100 requests per interval
});
// apply to all requests
app.use(limiter);
```
Create multiple instances to apply different rules to different routes:
```js
const RateLimit = require('koa2-ratelimit').RateLimit;
const KoaRouter = require('koa-router');
const router = new KoaRouter();
const getUserLimiter = RateLimit.middleware({
interval: 15*60*1000, // 15 minutes
max: 100,
prefixKey: 'get/user/:id' // to allow the bdd to Differentiate the endpoint
});
// add route with getUserLimiter middleware
router.get('/user/:id', getUserLimiter, (ctx) => {
// Do your job
});
const createAccountLimiter = RateLimit.middleware({
interval: { hour: 1, min: 30 }, // 1h30 window
delayAfter: 1, // begin slowing down responses after the first request
timeWait: 3*1000, // slow down subsequent responses by 3 seconds per request
max: 5, // start blocking after 5 requests
prefixKey: 'post/user', // to allow the bdd to Differentiate the endpoint
message: "Too many accounts created from this IP, please try again after an hour",
messageKey: "message"
});
// add route with createAccountLimiter middleware
router.post('/user', createAccountLimiter, (ctx) => {
// Do your job
});
// mount routes
app.use(router.middleware())
```
Set default options to all your middleware:
```js
const RateLimit = require('koa2-ratelimit').RateLimit;
RateLimit.defaultOptions({
message: 'Get out.',
// ...
});
const getUserLimiter = RateLimit.middleware({
max: 100,
// message: 'Get out.', will be added
});
const createAccountLimiter = RateLimit.middleware({
max: 5, // start blocking after 5 requests
// message: 'Get out.', will be added
});
```
### Use with RedisStore
```bash
npm install redis@4
```
```js
const RateLimit = require('koa2-ratelimit').RateLimit;
const Stores = require('koa2-ratelimit').Stores;
//Detailed Redis Configuration Reference: https://github.com/redis/node-redis/blob/master/docs/client-configuration.md
RateLimit.defaultOptions({
message: 'Get out.',
store: new Stores.Redis({
socket: {
host: 'redis_host',
port: 'redis_port',
},
password: 'redis_password',
database: 1
})
});
const getUserLimiter = RateLimit.middleware({
prefixKey: 'get/user/:id',
});
router.get('/user/:id', getUserLimiter, (ctx) => {});
const createAccountLimiter = RateLimit.middleware.middleware({
prefixKey: 'post/user',
});
router.post('/user', createAccountLimiter, (ctx) => {});
// mount routes
app.use(router.middleware())
```
### Use with SequelizeStore
```bash
npm install sequelize@5
```
```js
const Sequelize = require('sequelize');
const RateLimit = require('koa2-ratelimit').RateLimit;
const Stores = require('koa2-ratelimit').Stores;
const sequelize = new Sequelize(/*your config to connected to bdd*/);
RateLimit.defaultOptions({
message: 'Get out.',
store: new Stores.Sequelize(sequelize, {
tableName: 'ratelimits', // table to manage the middleware
tableAbuseName: 'ratelimitsabuses', // table to store the history of abuses in.
})
});
const getUserLimiter = RateLimit.middleware({
prefixKey: 'get/user/:id',
});
router.get('/user/:id', getUserLimiter, (ctx) => {});
const createAccountLimiter = RateLimit.middleware.middleware({
prefixKey: 'post/user',
});
router.post('/user', createAccountLimiter, (ctx) => {});
// mount routes
app.use(router.middleware())
```
### Use with MongooseStore (Mongodb)
```bash
npm install mongoose@5
```
```js
const mongoose = require('mongoose');
const RateLimit = require('koa2-ratelimit').RateLimit;
const Stores = require('koa2-ratelimit').Stores;
await mongoose.connect(/*your config to connected to bdd*/);
RateLimit.defaultOptions({
message: 'Get out.',
store: new Stores.Mongodb(mongoose.connection, {
collectionName: 'ratelimits', // table to manage the middleware
collectionAbuseName: 'ratelimitsabuses', // table to store the history of abuses in.
}),
});
```
A `ctx.state.rateLimit` property is added to all requests with the `limit`, `current`, and `remaining` number of requests for usage in your application code.
## Configuration
* **interval**: [Time Type](#time-type) - how long should records of requests be kept in memory. Defaults to `60000` (1 minute).
* **delayAfter**: max number of connections during `interval` before starting to delay responses. Defaults to `1`. Set to `0` to disable delaying.
* **timeWait**: [Time Type](#time-type) - how long to delay the response, multiplied by (number of recent hits - `delayAfter`). Defaults to `1000` (1 second). Set to `0` to disable delaying.
* **max**: max number of connections during `interval` milliseconds before sending a `429` response code. Defaults to `5`. Set to `0` to disable.
* **message**: Error message returned when `max` is exceeded. Defaults to `'Too many requests, please try again later.'`
* **statusCode**: HTTP status code returned when `max` is exceeded. Defaults to `429`.
* **headers**: Enable headers for request limit (`X-RateLimit-Limit`) and current usage (`X-RateLimit-Remaining`) on all responses and time to wait before retrying (`Retry-After`) when `max` is exceeded.
* **skipFailedRequests**: when `true`, failed requests (response status >= 400) won't be counted. Defaults to `false`.
* **whitelist**: Array of whitelisted IPs/UserIds to not be rate limited.
* **getUserIdFromKey**: Function that extracts from given key the userId. Defaults to `(key) => key.split(options.prefixKeySeparator)`.
* **prefixKeySeparator**: Separator string between the prefixKey and the userId. Defaults to `::`. (Set it to `|` if you want whitelist userIds)
* **getUserId**: Function used to get userId (if connected) to be added as key and saved in bdd, should an abuse case surface. Defaults:
```js
async function (ctx) {
const whereFinds = [ctx.state.user, ctx.user, ctx.state.User,
ctx.User, ctx.state, ctx];
const toFinds = ['id', 'userId', 'user_id', 'idUser', 'id_user'];
for (const whereFind of whereFinds) {
if (whereFind) {
for (const toFind of toFinds) {
if (whereFind[toFind]) {
return whereFind[toFind];
}
}
}
}
return null;
},
```
* **keyGenerator**: Function used to generate keys. By default userID (if connected) or the user's IP address. Defaults:
```js
async function (ctx) {
const userId = await this.options.getUserId(ctx);
if (userId) {
return `${this.options.prefixKey}|${userId}`;
}
return `${this.options.prefixKey}|${ctx.request.ip}`;
}
```
* **skip**: Function used to skip requests. Returning true from the function will skip limiting for that request. Defaults:
```js
async function (/*ctx*/) {
return false;
}
```
* **handler**: The function to execute once the max limit has been exceeded. It receives the request and the response objects. The "next" param is available if you need to pass to the next middleware. Defaults:
```js
async function (ctx/*, next*/) {
ctx.status = this.options.statusCode;
ctx.body = { message: this.options.message };
if (this.options.headers) {
ctx.set('Retry-After', Math.ceil(this.options.interval / 1000));
}
}
```
* **onLimitReached**: Function to listen each time the limit is reached. It call the store to save abuse, You can use it to debug/log. Defaults:
```js
async function (ctx) {
this.store.saveAbuse({
key: await this.options.keyGenerator(ctx),
ip: ctx.request.ip,
user_id: await this.options.getUserId(ctx),
});
}
```
* **weight**: Function to set the incrementation of the counter depending on the request. Defaults:
```js
async function (/*ctx*/) {
return 1;
}
```
* **store**: The storage to use when persisting rate limit attempts. By default, the [MemoryStore](src/MemoryStore.js) is used.
Avaliable data stores are:
* [MemoryStore](src/MemoryStore.js): (default)Simple in-memory option. Does not share state when app has multiple processes or servers.
* [SequelizeStore](src/SequelizeStore.js): more suitable for large or demanding deployments.
The `delayAfter` and `timeWait` options were written for human-facing pages such as login and password reset forms.
For public APIs, setting these to `0` (disabled) and relying on only `interval` and `max` for rate-limiting usually makes the most sense.
## Time Type
Time type can be milliseconds or an object
```js
Times = {
ms ?: number,
sec ?: number,
min ?: number,
hour ?: number,
day ?: number,
week ?: number,
month ?: number,
year ?: number,
};
```
Examples
```js
RateLimit.middleware({
interval: { hour: 1, min: 30 }, // 1h30 window
timeWait: { week: 2 }, // 2 weeks window
});
RateLimit.middleware({
interval: { ms: 2000 }, // 2000 ms = 2 sec
timeWait: 2000, // 2000 ms = 2 sec
});
```
## Upgrade
### 0.9.1 to 1.0.0
1.0.0 moves sequelize, mongoose and redis from dependencies to peerDependencies.
Install the one you use (see [Use with RedisStore](#use-with-redisStore), [Use with SequelizeStore](#use-with-sequelizestore) or [Use with MongooseStore (Mongodb)](#use-with-mongoosestore)).
The rest did not change.
## License
MIT © [YSO Corp](http://ysocorp.com/)

11
server/node_modules/koa2-ratelimit/jsconfig.json generated vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES6"
},
"include": [
"./**/*"
],
"exclude": [
"node_modules"
]
}

3
server/node_modules/koa2-ratelimit/mocha.opts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
--recursive
./test/**/*.js
--timeout 4000

87
server/node_modules/koa2-ratelimit/package.json generated vendored Normal file
View File

@@ -0,0 +1,87 @@
{
"name": "koa2-ratelimit",
"author": "YSO CORP",
"version": "1.1.3",
"description": "IP rate-limiting middleware for Koajs 2. Use to limit repeated requests to APIs and/or endpoints such as password reset.",
"main": "src/index.js",
"engines": {
"node": ">=7.10.1"
},
"people": {
"name": "YSO CORP",
"email": "contact@ysocorp.com",
"url": "http://ysocorp.com"
},
"repository": {
"type": "git",
"url": "https://github.com/ysocorp/koa2-ratelimit"
},
"dependencies": {},
"peerDependencies": {
"mongoose": ">= 5",
"redis": ">= 4.0.0",
"sequelize": ">=5.8.7"
},
"peerDependenciesMeta": {
"mongoose": {
"optional": true
},
"redis": {
"optional": true
},
"sequelize": {
"optional": true
}
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.13.0",
"expect": "^24.8.0",
"mocha": "^6.1.4"
},
"scripts": {
"lint": "eslint src test",
"test": "mocha --opts mocha.opts"
},
"keywords": [
"koa2-ratelimit",
"koa2-rate-limit",
"koa-rate-limit",
"koa2-ratelimit",
"koa-ratelimit",
"koa2-brute-force",
"koa-brute-force",
"koa2-bruteforce",
"koa-bruteforce",
"koa2",
"rate",
"limit",
"sequelize",
"mongodb",
"ratelimit",
"ratelimit-sequelize",
"ratelimit-mongodb",
"rate-limit",
"rate-limit-sequelize",
"rate-limit-mongodb",
"middleware",
"ip",
"auth",
"authorization",
"security",
"brute",
"force",
"bruteforce",
"bruteforce-sequelize",
"bruteforce-mongodb",
"brute-force",
"brute-force-sequelize",
"brute-force-mongodb",
"attack"
],
"license": "MIT"
}

53
server/node_modules/koa2-ratelimit/src/MemoryStore.js generated vendored Normal file
View File

@@ -0,0 +1,53 @@
const Store = require('./Store.js');
let Hits = {};
class MemoryStore extends Store {
static cleanAll() {
Hits = {};
}
_getHit(key, options) {
if (!Hits[key]) {
Hits[key] = {
counter: 0,
date_end: Date.now() + options.interval,
};
}
return Hits[key];
}
_resetAll() {
const now = Date.now();
for (const key in Hits) { // eslint-disable-line
this._resetKey(key, now);
}
}
_resetKey(key, now) {
now = now || Date.now();
if (Hits[key] && Hits[key].date_end <= now) {
delete Hits[key];
}
}
async incr(key, options, weight) {
this._resetAll();
const hits = this._getHit(key, options);
hits.counter += weight;
return {
counter: hits.counter,
dateEnd: hits.date_end,
};
}
decrement(key, options, weight) {
const hits = this._getHit(key);
hits.counter -= weight;
}
saveAbuse() {}
}
module.exports = MemoryStore;

156
server/node_modules/koa2-ratelimit/src/MongodbStore.js generated vendored Normal file
View File

@@ -0,0 +1,156 @@
const mongoose = require('mongoose');
const Store = require('./Store.js');
async function findOrCreate({ where, defaults }) {
return this.collection.findAndModify(
where,
[],
{ $setOnInsert: defaults },
{ upsert: true, new: true }, // return new doc if one is upserted
);
}
const abuseSchema = new mongoose.Schema({
key: {
type: String,
required: true,
index: { unique: true },
},
counter: {
type: Number,
required: true,
default: 0,
},
dateEnd: {
type: Date,
required: true,
},
});
abuseSchema.statics.findOrCreate = findOrCreate;
const abuseHistorySchema = new mongoose.Schema({
key: {
type: String,
required: true,
},
prefix: {
type: String,
required: false,
},
interval: {
type: Number,
required: true,
},
nbMax: {
type: Number,
required: true,
},
nbHit: {
type: Number,
required: true,
default: 0,
},
userId: {
type: Number,
required: false,
},
ip: {
type: String,
required: false,
},
dateEnd: {
type: Date,
required: true,
},
createdAt: {
type: Date,
required: true,
default: Date.now,
},
updatedAt: {
type: Date,
required: true,
default: Date.now,
},
});
abuseHistorySchema.index({ key: 1, dateEnd: 1 }, { unique: true });
function beforSave(next) {
this.updatedAt = Date.now();
next();
}
abuseHistorySchema.pre('save', beforSave);
abuseHistorySchema.pre('update', beforSave);
abuseHistorySchema.pre('findOneAndUpdate', beforSave);
abuseHistorySchema.statics.findOrCreate = findOrCreate;
class MongodbStore extends Store {
constructor(mongodb, options = {}) {
super();
this.collectionName = options.collectionName || 'Ratelimits';
this.collectionAbuseName = options.collectionAbuseName || `${this.collectionName}Abuses`;
this.Ratelimits = mongodb.model(this.collectionName, abuseSchema);
this.Abuse = mongodb.model(this.collectionAbuseName, abuseHistorySchema);
}
async _increment(model, where, nb = 1, field) {
return model.findOneAndUpdate(where, { $inc: { [field]: nb } });
}
// remove all if time is passed
async _removeAll() {
await this.Ratelimits.remove({ dateEnd: { $lte: Date.now() } });
}
async incr(key, options, weight) {
await this._removeAll();
const data = await this.Ratelimits.findOrCreate({
where: { key },
defaults: {
key,
dateEnd: Date.now() + options.interval,
counter: 0,
},
});
await this._increment(this.Ratelimits, { key }, weight, 'counter');
return {
counter: data.value.counter + weight,
dateEnd: data.value.dateEnd,
};
}
async decrement(key, options, weight) {
await this._increment(this.Ratelimits, { key }, -weight, 'counter');
}
async saveAbuse(options) {
const ratelimit = await this.Ratelimits.findOne({
key: options.key,
}).exec();
if (ratelimit) {
// eslint-disable-next-line
const dateEnd = ratelimit.dateEnd;
// create if not exist
await this.Abuse.findOrCreate({
where: { key: options.key, dateEnd },
defaults: {
key: options.key,
prefix: options.prefixKey,
interval: options.interval,
nbMax: options.max,
nbHit: options.max,
userId: options.user_id,
ip: options.ip,
dateEnd,
},
}).catch(() => {});
await this._increment(this.Abuse, { key: options.key, dateEnd }, 1, 'nbHit');
}
}
}
module.exports = MongodbStore;

234
server/node_modules/koa2-ratelimit/src/RateLimit.js generated vendored Normal file
View File

@@ -0,0 +1,234 @@
const Store = require('./Store.js');
const MemoryStore = require('./MemoryStore.js');
/* eslint-disable no-var */
var defaultOptions = {
// window, delay, and max apply per-key unless global is set to true
interval: { min: 1 }, // milliseconds - how long to keep records of requests in memory
delayAfter: 0, // how many requests to allow through before starting to delay responses
timeWait: { sec: 1 }, // milliseconds - base delay applied to the response - multiplied by number of recent hits for the same key.
max: 5, // max number of recent connections during `window` milliseconds before sending a 429 response
message: 'Too many requests, please try again later.',
messageKey: 'message',
statusCode: 429, // 429 status = Too Many Requests (RFC 6585)
headers: true, // Send custom rate limit header with limit and remaining
skipFailedRequests: false, // Do not count failed requests (status >= 400)
prefixKey: 'global', // the prefixKey to get to remove all key
prefixKeySeparator: '::', // the seperator between the prefixKey and the userId
store: new MemoryStore(),
// redefin fonction
keyGenerator: undefined,
getUserIdFromKey: undefined,
skip: undefined,
getUserId: undefined,
handler: undefined,
onLimitReached: undefined,
weight: undefined,
whitelist: [],
};
const TimeKeys = ['ms', 'sec', 'min', 'hour', 'day', 'week', 'month', 'year'];
const Times = {
ms: 1,
sec: 1000,
min: 60000,
hour: 3600000,
day: 86400000,
week: 604800000,
month: 2628000000,
year: 12 * 2628000000,
};
const toFinds = ['id', 'userId', 'user_id', 'idUser', 'id_user'];
class RateLimit {
constructor(options) {
this.options = Object.assign({}, defaultOptions, options);
this.options.interval = RateLimit.timeToMs(this.options.interval);
this.options.timeWait = RateLimit.timeToMs(this.options.timeWait);
// store to use for persisting rate limit data
this.store = this.options.store;
// ensure that the store extends Store class
if (!(this.store instanceof Store)) {
throw new Error('The store is not valid.');
}
}
static timeToMs(time) {
if (typeof time === 'object') {
let timeMs = 0;
for (const key in time) {
if (key) {
if (!TimeKeys.includes(key)) {
throw new Error(`Invalide key ${key}, allow keys: ${TimeKeys.toString()}`);
}
if (time[key] > 0) {
timeMs += time[key] * Times[key];
}
}
}
return timeMs;
}
return time;
}
async keyGenerator(ctx) {
if (this.options.keyGenerator) {
return this.options.keyGenerator(ctx);
}
const userId = await this.getUserId(ctx);
if (userId) {
return `${this.options.prefixKey}|${userId}`;
}
return `${this.options.prefixKey}|${ctx.request.ip}`;
}
async weight(ctx) {
if (this.options.weight) {
return this.options.weight(ctx);
}
return 1;
}
async skip(ctx) { // eslint-disable-line
if (this.options.skip) {
return this.options.skip(ctx);
}
return false;
}
async getUserId(ctx) {
if (this.options.getUserId) {
return this.options.getUserId(ctx);
}
const whereFinds = [ctx.state.user, ctx.user, ctx.state.User, ctx.User, ctx.state, ctx];
for (const whereFind of whereFinds) {
if (whereFind) {
for (const toFind of toFinds) {
if (whereFind[toFind]) {
return whereFind[toFind];
}
}
}
}
return null;
}
async handler(ctx/* , next */) {
if (this.options.handler) {
this.options.handler(ctx);
} else {
ctx.status = this.options.statusCode;
ctx.body = { [this.options.messageKey]: this.options.message };
if (this.options.headers) {
ctx.set('Retry-After', Math.ceil(this.options.interval / 1000));
}
}
}
async onLimitReached(ctx) {
if (this.options.onLimitReached) {
this.options.onLimitReached(ctx);
} else {
this.store.saveAbuse(Object.assign({}, this.options, {
key: await this.keyGenerator(ctx),
ip: ctx.request.ip,
user_id: await this.getUserId(ctx),
}));
}
}
get middleware() {
return this._rateLimit.bind(this);
}
async _rateLimit(ctx, next) {
const skip = await this.skip(ctx);
if (skip) {
return next();
}
const key = await this.keyGenerator(ctx);
if (this._isWhitelisted(key)) {
return next();
}
const weight = await this.weight(ctx);
const { counter, dateEnd } = await this.store.incr(key, this.options, weight);
const reset = new Date(dateEnd).getTime();
ctx.state.rateLimit = {
limit: this.options.max,
current: counter,
remaining: Math.max(this.options.max - counter, 0),
reset: Math.ceil(reset / 1000),
};
if (this.options.headers) {
ctx.set('X-RateLimit-Limit', this.options.max);
ctx.set('X-RateLimit-Remaining', ctx.state.rateLimit.remaining);
ctx.set('X-RateLimit-Reset', ctx.state.rateLimit.reset);
}
if (this.options.max && counter > this.options.max) {
await this.onLimitReached(ctx);
return this.handler(ctx, next);
}
if (this.options.skipFailedRequests) {
ctx.res.on('finish', () => {
if (ctx.status >= 400) {
this.store.decrement(key, this.options, weight);
}
});
}
if (this.options.delayAfter && this.options.timeWait && counter > this.options.delayAfter) {
const delay = (counter - this.options.delayAfter) * this.options.timeWait;
await this.wait(delay);
return next();
}
return next();
}
_isWhitelisted(key) {
const { whitelist } = this.options;
if (whitelist == null || whitelist.length === 0) {
return false;
}
const userId = this.getUserIdFromKey(key);
if (userId) {
return whitelist.includes(userId);
}
return false;
}
getUserIdFromKey(key) {
if (this.options.getUserIdFromKey) {
return this.options.getUserIdFromKey(key);
}
const [, userId] = key.split(this.options.prefixKeySeparator);
return userId;
}
async wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = {
RateLimit,
middleware(options = {}) {
return new RateLimit(options).middleware;
},
defaultOptions(options = {}) {
defaultOptions = Object.assign(defaultOptions, options);
},
};

109
server/node_modules/koa2-ratelimit/src/RedisStore.js generated vendored Normal file
View File

@@ -0,0 +1,109 @@
/**
* RedisStore
*
* RedisStore for koa2-ratelimit
*
* @author Ashok Vishwakarma <akvlko@gmail.com>
*/
/**
* Store
*
* Existing Store class
*/
const Store = require('./Store.js');
/**
* redis
*
* node-redis module
*/
const redis = require('redis');
/**
* RedisStore
*
* Class RedisStore
*/
class RedisStore extends Store {
/**
* constructor
* @param {*} config
*
* config is redis config
*/
constructor(config){
super();
this.client = redis.createClient(config);
this.client.on('error', (err) => console.log('Redis Client Error', err));
this.client.connect()
}
/**
* _hit
* @access private
* @param {*} key
* @param {*} options
* @param {*} weight
*/
async _hit(key, options, weight) {
let [counter, dateEnd] = await this.client.multi().get(key).ttl(key).exec();
if(counter === null) {
counter = weight;
dateEnd = Date.now() + options.interval;
const seconds = Math.ceil(options.interval / 1000);
await this.client.setEx(key, seconds.toString(), counter.toString());
} else if (dateEnd === -2 || dateEnd === -1) {
counter = counter + weight;
dateEnd = Date.now() + options.interval;
const seconds = Math.ceil(options.interval / 1000);
await this.client.setEx(key, seconds.toString(), counter.toString());
} else {
counter = await this.client.incrBy(key, weight);
}
return {
counter,
dateEnd
}
}
/**
* incr
*
* Override incr method from Store class
* @param {*} key
* @param {*} options
* @param {*} weight
*/
async incr(key, options, weight) {
return await this._hit(key, options, weight);
}
/**
* decrement
*
* Override decrement method from Store class
* @param {*} key
* @param {*} options
* @param {*} weight
*/
async decrement(key, options, weight) {
await this.client.decrBy(key, weight);
}
/**
* saveAbuse
*
* Override saveAbuse method from Store class
*/
saveAbuse() {}
}
module.exports = RedisStore;

View File

@@ -0,0 +1,185 @@
const Sequelize = require('sequelize');
const Store = require('./Store.js');
const tableOption = [
{
key: {
type: Sequelize.STRING(255),
allowNull: false,
primaryKey: true,
unique: true,
},
counter: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
date_end: {
type: Sequelize.DATE,
allowNull: false,
},
},
{
indexes: [{ unique: true, fields: ['key'] }, { unique: false, fields: ['date_end'] }],
underscored: true,
createdAt: false,
updatedAt: false,
},
];
const tableAbuseOption = [
{
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
key: {
type: Sequelize.STRING(255),
allowNull: false,
},
prefix: {
type: Sequelize.STRING(255),
allowNull: true,
},
interval: {
type: Sequelize.INTEGER,
allowNull: false,
},
nb_max: {
type: Sequelize.INTEGER,
allowNull: false,
},
nb_hit: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
user_id: {
allowNull: true,
type: Sequelize.INTEGER,
},
ip: {
type: Sequelize.STRING(255),
allowNull: true,
},
date_end: {
type: Sequelize.DATE,
allowNull: false,
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('NOW'),
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('NOW'),
},
},
{
indexes: [{ unique: true, fields: ['key', 'date_end'] }],
underscored: true,
},
];
class SequelizeStore extends Store {
constructor(sequelize, options = {}) {
super();
this.sequelize = sequelize;
this.tableName = options.tableName || 'ratelimits';
this.tableAbuseName = options.tableAbuseName || `${this.tableName}abuses`;
this.table = null;
this.tableAbuses = null;
}
async _getTable() {
if (!this.table) {
this.table = this.sequelize.define(this.tableName, tableOption[0], tableOption[1]);
await this.table.sync();
}
return this.table;
}
async _getTableAbuse() {
if (!this.tableAbuses) {
this.tableAbuses = this.sequelize.define(
this.tableAbuseName,
tableAbuseOption[0],
tableAbuseOption[1],
);
await this.tableAbuses.sync();
}
return this.tableAbuses;
}
async _increment(table, where, nb = 1, field) {
return table.update({ [field]: global.sequelize.literal(`${field} + ${nb}`) }, { where });
}
// remove all if time is passed
async _removeAll(table) {
const now = new Date();
await table.destroy({
where: {
date_end: { $lte: now.getTime() },
},
});
}
async incr(key, options, weight) {
const table = await this._getTable();
await this._removeAll(table);
const now = new Date();
const data = await table.findOrCreate({
where: { key },
defaults: {
key,
date_end: now.getTime() + options.interval,
},
});
await this._increment(table, { key }, weight, 'counter');
return {
counter: data[0].counter + weight,
dateEnd: data[0].date_end,
};
}
async decrement(key, options, weight) {
const table = await this._getTable();
await this._increment(table, { key }, -weight, 'counter');
}
async saveAbuse(options) {
const table = await this._getTable();
const ratelimit = await table.findOne({ where: { key: options.key } });
if (ratelimit) {
const tableAbuse = await this._getTableAbuse();
// eslint-disable-next-line
const date_end = ratelimit.date_end;
// create if not exist
await tableAbuse
.findOrCreate({
where: { key: options.key, date_end },
defaults: {
key: options.key,
prefix: options.prefixKey,
interval: options.interval,
nb_max: options.max,
nb_hit: options.max,
user_id: options.user_id,
ip: options.ip,
date_end,
},
})
.catch(() => {});
await this._increment(tableAbuse, { key: options.key, date_end }, 1, 'nb_hit');
}
}
}
module.exports = SequelizeStore;

14
server/node_modules/koa2-ratelimit/src/Store.js generated vendored Normal file
View File

@@ -0,0 +1,14 @@
const methods = ['incr', 'decrement', 'saveAbuse'];
class Store {
constructor() {
for (const elem of methods) {
if (this[elem] === undefined) {
throw new TypeError(`Must override method ${elem}`);
}
}
}
}
module.exports = Store;

23
server/node_modules/koa2-ratelimit/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,23 @@
const RateLimit = require("./RateLimit.js");
const MemoryStore = require("./MemoryStore.js");
const Store = require("./Store.js");
module.exports = {
RateLimit,
Stores: {
Memory: MemoryStore,
get Sequelize() {
// eslint-disable-next-line global-require
return require("./SequelizeStore.js");
},
get Mongodb() {
// eslint-disable-next-line global-require
return require("./MongodbStore.js");
},
get Redis() {
// eslint-disable-next-line global-require
return require("./RedisStore.js");
},
Store,
},
};

318
server/node_modules/koa2-ratelimit/test/RateLimit.js generated vendored Normal file
View File

@@ -0,0 +1,318 @@
/* global describe, it, beforeEach, afterEach */
const assert = require('assert');
const expect = require('expect');
const RateLimit = require('../src/RateLimit.js');
const MemoryStore = require('../src/MemoryStore.js');
const Store = require('../src/Store.js');
class InvalidStore { }
class MockStore extends Store {
constructor() {
super();
this.nb = 0;
this.incr_was_called = false;
this.decrement_was_called = false;
this.saveAbuse_was_called = false;
}
async incr(key, options, weight) {
this.nb += weight;
this.incr_was_called = true;
return {
counter: this.nb,
dateEnd: new Date().setHours(new Date().getHours() + 1),
};
}
async decrement(key, options, weight) {
this.decrement_was_called = true;
this.nb -= weight;
}
async saveAbuse() {
this.saveAbuse_was_called = true;
}
}
function getCtx() {
return {
request: { ip: '192.168.1.0' },
res: { on: () => { } },
state: { user: { id: 1 } },
set: () => { },
};
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
describe('RateLimit node module', () => {
let start;
let nbCall;
let ctx;
let store;
let memoryStore;
beforeEach(() => {
start = Date.now();
store = new MockStore();
MemoryStore.cleanAll();
memoryStore = new MemoryStore();
nbCall = 0;
ctx = getCtx();
});
afterEach(() => {
nbCall = 0;
});
function nextNb() { nbCall += 1; }
it('Times should return the correct time in ms', () => {
expect(RateLimit.RateLimit.timeToMs(123)).toBe(123);
expect(RateLimit.RateLimit.timeToMs({ hour: 2 })).toBe(2 * 3600000);
expect(RateLimit.RateLimit.timeToMs({ hour: 2, min: 3 })).toBe((2 * 3600000) + (3 * 60000));
});
it('Times should throw error if key does not exist', (done) => {
try {
RateLimit.RateLimit.timeToMs({ hours: 3 });
} catch (e) { return done(); }
return done(new Error('Times should throw error if key does not exist'));
});
it('should not allow to use of a store that is not valid', (done) => {
try {
RateLimit.middleware({ store: new InvalidStore() });
} catch (e) {
return done();
}
return done(new Error('It allowed an invalid store'));
});
it('should call incr on the store', async () => {
const middleware = RateLimit.middleware({ store });
await middleware(getCtx(), nextNb);
expect(store.incr_was_called).toBe(true);
});
it('should apply a small delay to the second request', async () => {
const middleware = RateLimit.middleware({ delayAfter: 1, timeWait: 500, store });
await middleware(getCtx(), nextNb);
start = Date.now();
await middleware(getCtx(), nextNb);
expect(Date.now() - start).toBeGreaterThan(500);
});
it('should apply a larger delay to the subsequent request', async () => {
const middleware = RateLimit.middleware({ delayAfter: 1, timeWait: 100, store });
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
expect(Date.now() - start).toBeGreaterThan(400);
});
it('should allow delayAfter requests before delaying responses', async () => {
const middleware = RateLimit.middleware({ delayAfter: 2, timeWait: 100, store });
await middleware(getCtx(), nextNb);
expect(Date.now() - start).toBeLessThan(50);
await middleware(getCtx(), nextNb);
expect(Date.now() - start).toBeLessThan(100);
await middleware(getCtx(), nextNb);
expect(Date.now() - start).toBeGreaterThan(100);
expect(Date.now() - start).toBeLessThan(150);
});
it('should allow delayAfter to be disabled entirely', async () => {
const middleware = RateLimit.middleware({ delayAfter: 0, timeWait: 1000, store });
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
expect(Date.now() - start).toBeLessThan(100);
});
it('should refuse additional connections once IP has reached the max', async () => {
const middleware = RateLimit.middleware({ max: 1, store });
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
expect(nbCall).toBe(1);
});
it('should allow max to be disabled entirely', async () => {
const middleware = RateLimit.middleware({ max: 0, store });
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
expect(nbCall).toBe(3);
});
it('should show the provided message instead of the default message when max connections are reached', async () => {
const message = 'my msg';
const middleware = RateLimit.middleware({ max: 2, message, store });
await middleware(getCtx(), nextNb);
await middleware(getCtx(), nextNb);
const ctxDefault = getCtx();
await middleware(ctxDefault, nextNb);
expect(ctxDefault.body.message).toBe(message);
});
it('should (eventually) accept new connections from a blocked IP', async () => {
const middleware = RateLimit.middleware({
max: 10, interval: 50, prefixKey: start, store: memoryStore,
});
await middleware(ctx, nextNb);
await middleware(ctx, nextNb);
await sleep(60);
await middleware(ctx, nextNb);
expect(nbCall).toBe(3);
});
it('should work repeatedly (issues #2 & #3)', async () => {
const middleware = RateLimit.middleware({
max: 2, interval: 50, prefixKey: start, store: memoryStore,
});
await middleware(ctx, nextNb);
await middleware(ctx, nextNb);
await sleep(60);
await middleware(ctx, nextNb);
expect(nbCall).toBe(3);
});
it('should allow the error statusCode to be customized', async () => {
const middleware = RateLimit.middleware({ max: 1, statusCode: 123, store });
await middleware(ctx, nextNb);
await middleware(ctx, nextNb);
expect(ctx.status).toBe(123);
});
it('should use the custom handler when specified', async () => {
const middleware = RateLimit.middleware({
max: 1,
handler: (c) => { c.status = 231; },
store,
});
await middleware(ctx, nextNb);
await middleware(ctx, nextNb);
expect(ctx.status).toBe(231);
});
it('should allow custom skip function', async () => {
const middleware = RateLimit.middleware({
max: 1,
skip: (c) => {
assert.ok(c);
return true;
},
store,
});
await middleware(ctx, nextNb);
await middleware(ctx, nextNb);
await middleware(ctx, nextNb);
expect(nbCall).toBe(3);
});
it('should allow custom weight function', async () => {
const middleware = RateLimit.middleware({
max: 3,
weight: () => 2,
store,
});
await middleware(ctx, nextNb);
await middleware(ctx, nextNb);
expect(nbCall).toBe(1);
});
it('should allow custom key generators', async () => {
let key = null;
const middleware = RateLimit.middleware({
keyGenerator: (c) => {
assert.ok(c);
key = 'TITI';
return key;
},
store,
});
await middleware(ctx, nextNb);
expect(key).toBe('TITI');
});
it('should set X-RateLimit-Reset with the correct value', async () => {
const middleware = RateLimit.middleware({ store });
const dateEnd = new Date(1528824545000);
const dateEndSec = dateEnd / 1000;
let dateEndReset = null;
store.incr = async () => {
return { counter: 10, dateEnd };
};
ctx.set = (key, value) => {
if (key === 'X-RateLimit-Reset') {
dateEndReset = value;
}
};
await middleware(ctx, nextNb);
expect(dateEndReset).toBe(dateEndSec);
expect(ctx.state.rateLimit.reset).toBe(dateEndSec);
});
describe('Whitelist users', () => {
beforeEach(() => {
store.incr = async () => {
assert.fail('Ratelimit wasn\'t skipped');
};
});
async function runtWhitelistTest(options) {
const middleware = RateLimit.middleware({ store, ...options});
await middleware(ctx, nextNb);
expect(nbCall).toBe(1);
}
it('should skip ratelimit if userId is whitelisted', async () => {
await runtWhitelistTest({
whitelist: ['userId'],
getUserId: () => Promise.resolve('prefix::userId'),
});
});
it('should allow to overwrite the prefix key separator', async () => {
ctx.state.user.id = 'userId';
await runtWhitelistTest({
whitelist: ['userId'],
prefixKeySeparator: '|',
});
});
it('should allow to customize the key parsing logic', async () => {
ctx.state.user.id = 'userId';
await runtWhitelistTest({
store,
whitelist: ['userId'],
getUserIdFromKey: key => key.split('|')[1],
});
});
});
});