@hocuspocus/extension-throttle 1.0.0 → 1.0.2
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/dist/hocuspocus-throttle.cjs +36 -8
- package/dist/hocuspocus-throttle.cjs.map +1 -1
- package/dist/hocuspocus-throttle.esm.js +36 -8
- package/dist/hocuspocus-throttle.esm.js.map +1 -1
- package/dist/packages/extension-throttle/src/index.d.ts +7 -0
- package/dist/packages/transformer/src/Tiptap.d.ts +1 -1
- package/dist/tests/extension-throttle/banning.d.ts +1 -0
- package/package.json +2 -2
- package/src/index.ts +48 -12
|
@@ -10,6 +10,8 @@ class Throttle {
|
|
|
10
10
|
this.configuration = {
|
|
11
11
|
throttle: 15,
|
|
12
12
|
banTime: 5,
|
|
13
|
+
consideredSeconds: 60,
|
|
14
|
+
cleanupInterval: 90,
|
|
13
15
|
};
|
|
14
16
|
this.connectionsByIp = new Map();
|
|
15
17
|
this.bannedIps = new Map();
|
|
@@ -17,6 +19,34 @@ class Throttle {
|
|
|
17
19
|
...this.configuration,
|
|
18
20
|
...configuration,
|
|
19
21
|
};
|
|
22
|
+
this.cleanupInterval = setInterval(this.clearMaps.bind(this), this.configuration.cleanupInterval * 1000);
|
|
23
|
+
}
|
|
24
|
+
onDestroy() {
|
|
25
|
+
if (this.cleanupInterval) {
|
|
26
|
+
clearInterval(this.cleanupInterval);
|
|
27
|
+
}
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
clearMaps() {
|
|
31
|
+
this.connectionsByIp.forEach((value, key) => {
|
|
32
|
+
const filteredValue = value
|
|
33
|
+
.filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now());
|
|
34
|
+
if (filteredValue.length) {
|
|
35
|
+
this.connectionsByIp.set(key, filteredValue);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.connectionsByIp.delete(key);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
this.bannedIps.forEach((value, key) => {
|
|
42
|
+
if (!this.isBanned(key)) {
|
|
43
|
+
this.bannedIps.delete(key);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
isBanned(ip) {
|
|
48
|
+
const bannedAt = this.bannedIps.get(ip) || 0;
|
|
49
|
+
return Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000));
|
|
20
50
|
}
|
|
21
51
|
/**
|
|
22
52
|
* Throttle requests
|
|
@@ -26,19 +56,17 @@ class Throttle {
|
|
|
26
56
|
if (!this.configuration.throttle) {
|
|
27
57
|
return false;
|
|
28
58
|
}
|
|
29
|
-
|
|
30
|
-
if (Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))) {
|
|
59
|
+
if (this.isBanned(ip))
|
|
31
60
|
return true;
|
|
32
|
-
}
|
|
33
61
|
this.bannedIps.delete(ip);
|
|
34
62
|
// add this connection try to the list of previous connections
|
|
35
63
|
const previousConnections = this.connectionsByIp.get(ip) || [];
|
|
36
64
|
previousConnections.push(Date.now());
|
|
37
|
-
// calculate the previous connections in the last
|
|
38
|
-
const
|
|
39
|
-
.filter(timestamp => timestamp + (
|
|
40
|
-
this.connectionsByIp.set(ip,
|
|
41
|
-
if (
|
|
65
|
+
// calculate the previous connections in the last considered time interval
|
|
66
|
+
const previousConnectionsInTheConsideredInterval = previousConnections
|
|
67
|
+
.filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now());
|
|
68
|
+
this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval);
|
|
69
|
+
if (previousConnectionsInTheConsideredInterval.length > this.configuration.throttle) {
|
|
42
70
|
this.bannedIps.set(ip, Date.now());
|
|
43
71
|
return true;
|
|
44
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hocuspocus-throttle.cjs","sources":["../src/index.ts"],"sourcesContent":["import {\n Extension,\n onConnectPayload,\n} from '@hocuspocus/server'\n\nexport interface ThrottleConfiguration {\n throttle: number | null | false
|
|
1
|
+
{"version":3,"file":"hocuspocus-throttle.cjs","sources":["../src/index.ts"],"sourcesContent":["import {\n Extension,\n onConnectPayload,\n} from '@hocuspocus/server'\n\nexport interface ThrottleConfiguration {\n throttle: number | null | false, // how many requests within `consideredSeconds` until we're rejecting requests (setting this to 15 means the 16th request will be rejected)\n consideredSeconds: number, // how many seconds to consider (default is last 60 seconds from the current connection attempt)\n banTime: number, // for how long to ban after receiving too many requests (in minutes!)\n cleanupInterval: number // how often to clean up the records of IPs (this won't delete ips that are still blocked or recent enough by `consideredSeconds`)\n}\n\nexport class Throttle implements Extension {\n\n configuration: ThrottleConfiguration = {\n throttle: 15,\n banTime: 5,\n consideredSeconds: 60,\n cleanupInterval: 90,\n }\n\n connectionsByIp: Map<string, Array<number>> = new Map()\n\n bannedIps: Map<string, number> = new Map()\n\n cleanupInterval?: NodeJS.Timer\n\n /**\n * Constructor\n */\n constructor(configuration?: Partial<ThrottleConfiguration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\n\n this.cleanupInterval = setInterval(this.clearMaps.bind(this), this.configuration.cleanupInterval * 1000)\n }\n\n onDestroy() {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval)\n }\n\n return Promise.resolve()\n }\n\n public clearMaps() {\n this.connectionsByIp.forEach((value, key) => {\n const filteredValue = value\n .filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now())\n\n if (filteredValue.length) {\n this.connectionsByIp.set(key, filteredValue)\n } else {\n this.connectionsByIp.delete(key)\n }\n })\n\n this.bannedIps.forEach((value, key) => {\n if (!this.isBanned(key)) {\n this.bannedIps.delete(key)\n }\n })\n }\n\n isBanned(ip: string) {\n const bannedAt = this.bannedIps.get(ip) || 0\n return Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))\n }\n\n /**\n * Throttle requests\n * @private\n */\n private throttle(ip: string): Boolean {\n if (!this.configuration.throttle) {\n return false\n }\n\n if (this.isBanned(ip)) return true\n\n this.bannedIps.delete(ip)\n\n // add this connection try to the list of previous connections\n const previousConnections = this.connectionsByIp.get(ip) || []\n previousConnections.push(Date.now())\n\n // calculate the previous connections in the last considered time interval\n const previousConnectionsInTheConsideredInterval = previousConnections\n .filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now())\n\n this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval)\n\n if (previousConnectionsInTheConsideredInterval.length > this.configuration.throttle) {\n this.bannedIps.set(ip, Date.now())\n return true\n }\n\n return false\n }\n\n /**\n * onConnect hook\n * @param data\n */\n onConnect(data: onConnectPayload): Promise<any> {\n const { request } = data\n\n // get the remote ip address\n const ip = request.headers['x-real-ip']\n || request.headers['x-forwarded-for']\n || request.socket.remoteAddress\n || ''\n\n // throttle the connection\n return this.throttle(<string> ip) ? Promise.reject() : Promise.resolve()\n }\n\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,8 @@ class Throttle {
|
|
|
6
6
|
this.configuration = {
|
|
7
7
|
throttle: 15,
|
|
8
8
|
banTime: 5,
|
|
9
|
+
consideredSeconds: 60,
|
|
10
|
+
cleanupInterval: 90,
|
|
9
11
|
};
|
|
10
12
|
this.connectionsByIp = new Map();
|
|
11
13
|
this.bannedIps = new Map();
|
|
@@ -13,6 +15,34 @@ class Throttle {
|
|
|
13
15
|
...this.configuration,
|
|
14
16
|
...configuration,
|
|
15
17
|
};
|
|
18
|
+
this.cleanupInterval = setInterval(this.clearMaps.bind(this), this.configuration.cleanupInterval * 1000);
|
|
19
|
+
}
|
|
20
|
+
onDestroy() {
|
|
21
|
+
if (this.cleanupInterval) {
|
|
22
|
+
clearInterval(this.cleanupInterval);
|
|
23
|
+
}
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
clearMaps() {
|
|
27
|
+
this.connectionsByIp.forEach((value, key) => {
|
|
28
|
+
const filteredValue = value
|
|
29
|
+
.filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now());
|
|
30
|
+
if (filteredValue.length) {
|
|
31
|
+
this.connectionsByIp.set(key, filteredValue);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
this.connectionsByIp.delete(key);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
this.bannedIps.forEach((value, key) => {
|
|
38
|
+
if (!this.isBanned(key)) {
|
|
39
|
+
this.bannedIps.delete(key);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
isBanned(ip) {
|
|
44
|
+
const bannedAt = this.bannedIps.get(ip) || 0;
|
|
45
|
+
return Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000));
|
|
16
46
|
}
|
|
17
47
|
/**
|
|
18
48
|
* Throttle requests
|
|
@@ -22,19 +52,17 @@ class Throttle {
|
|
|
22
52
|
if (!this.configuration.throttle) {
|
|
23
53
|
return false;
|
|
24
54
|
}
|
|
25
|
-
|
|
26
|
-
if (Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))) {
|
|
55
|
+
if (this.isBanned(ip))
|
|
27
56
|
return true;
|
|
28
|
-
}
|
|
29
57
|
this.bannedIps.delete(ip);
|
|
30
58
|
// add this connection try to the list of previous connections
|
|
31
59
|
const previousConnections = this.connectionsByIp.get(ip) || [];
|
|
32
60
|
previousConnections.push(Date.now());
|
|
33
|
-
// calculate the previous connections in the last
|
|
34
|
-
const
|
|
35
|
-
.filter(timestamp => timestamp + (
|
|
36
|
-
this.connectionsByIp.set(ip,
|
|
37
|
-
if (
|
|
61
|
+
// calculate the previous connections in the last considered time interval
|
|
62
|
+
const previousConnectionsInTheConsideredInterval = previousConnections
|
|
63
|
+
.filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now());
|
|
64
|
+
this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval);
|
|
65
|
+
if (previousConnectionsInTheConsideredInterval.length > this.configuration.throttle) {
|
|
38
66
|
this.bannedIps.set(ip, Date.now());
|
|
39
67
|
return true;
|
|
40
68
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hocuspocus-throttle.esm.js","sources":["../src/index.ts"],"sourcesContent":["import {\n Extension,\n onConnectPayload,\n} from '@hocuspocus/server'\n\nexport interface ThrottleConfiguration {\n throttle: number | null | false
|
|
1
|
+
{"version":3,"file":"hocuspocus-throttle.esm.js","sources":["../src/index.ts"],"sourcesContent":["import {\n Extension,\n onConnectPayload,\n} from '@hocuspocus/server'\n\nexport interface ThrottleConfiguration {\n throttle: number | null | false, // how many requests within `consideredSeconds` until we're rejecting requests (setting this to 15 means the 16th request will be rejected)\n consideredSeconds: number, // how many seconds to consider (default is last 60 seconds from the current connection attempt)\n banTime: number, // for how long to ban after receiving too many requests (in minutes!)\n cleanupInterval: number // how often to clean up the records of IPs (this won't delete ips that are still blocked or recent enough by `consideredSeconds`)\n}\n\nexport class Throttle implements Extension {\n\n configuration: ThrottleConfiguration = {\n throttle: 15,\n banTime: 5,\n consideredSeconds: 60,\n cleanupInterval: 90,\n }\n\n connectionsByIp: Map<string, Array<number>> = new Map()\n\n bannedIps: Map<string, number> = new Map()\n\n cleanupInterval?: NodeJS.Timer\n\n /**\n * Constructor\n */\n constructor(configuration?: Partial<ThrottleConfiguration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\n\n this.cleanupInterval = setInterval(this.clearMaps.bind(this), this.configuration.cleanupInterval * 1000)\n }\n\n onDestroy() {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval)\n }\n\n return Promise.resolve()\n }\n\n public clearMaps() {\n this.connectionsByIp.forEach((value, key) => {\n const filteredValue = value\n .filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now())\n\n if (filteredValue.length) {\n this.connectionsByIp.set(key, filteredValue)\n } else {\n this.connectionsByIp.delete(key)\n }\n })\n\n this.bannedIps.forEach((value, key) => {\n if (!this.isBanned(key)) {\n this.bannedIps.delete(key)\n }\n })\n }\n\n isBanned(ip: string) {\n const bannedAt = this.bannedIps.get(ip) || 0\n return Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))\n }\n\n /**\n * Throttle requests\n * @private\n */\n private throttle(ip: string): Boolean {\n if (!this.configuration.throttle) {\n return false\n }\n\n if (this.isBanned(ip)) return true\n\n this.bannedIps.delete(ip)\n\n // add this connection try to the list of previous connections\n const previousConnections = this.connectionsByIp.get(ip) || []\n previousConnections.push(Date.now())\n\n // calculate the previous connections in the last considered time interval\n const previousConnectionsInTheConsideredInterval = previousConnections\n .filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now())\n\n this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval)\n\n if (previousConnectionsInTheConsideredInterval.length > this.configuration.throttle) {\n this.bannedIps.set(ip, Date.now())\n return true\n }\n\n return false\n }\n\n /**\n * onConnect hook\n * @param data\n */\n onConnect(data: onConnectPayload): Promise<any> {\n const { request } = data\n\n // get the remote ip address\n const ip = request.headers['x-real-ip']\n || request.headers['x-forwarded-for']\n || request.socket.remoteAddress\n || ''\n\n // throttle the connection\n return this.throttle(<string> ip) ? Promise.reject() : Promise.resolve()\n }\n\n}\n"],"names":[],"mappings":"MAYa,QAAQ,CAAA;AAenB;;AAEG;AACH,IAAA,WAAA,CAAY,aAA8C,EAAA;AAhB1D,QAAA,IAAA,CAAA,aAAa,GAA0B;AACrC,YAAA,QAAQ,EAAE,EAAE;AACZ,YAAA,OAAO,EAAE,CAAC;AACV,YAAA,iBAAiB,EAAE,EAAE;AACrB,YAAA,eAAe,EAAE,EAAE;SACpB,CAAA;AAED,QAAA,IAAA,CAAA,eAAe,GAA+B,IAAI,GAAG,EAAE,CAAA;AAEvD,QAAA,IAAA,CAAA,SAAS,GAAwB,IAAI,GAAG,EAAE,CAAA;QAQxC,IAAI,CAAC,aAAa,GAAG;YACnB,GAAG,IAAI,CAAC,aAAa;AACrB,YAAA,GAAG,aAAa;SACjB,CAAA;QAED,IAAI,CAAC,eAAe,GAAG,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,aAAa,CAAC,eAAe,GAAG,IAAI,CAAC,CAAA;KACzG;IAED,SAAS,GAAA;QACP,IAAI,IAAI,CAAC,eAAe,EAAE;AACxB,YAAA,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;AACpC,SAAA;AAED,QAAA,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;KACzB;IAEM,SAAS,GAAA;QACd,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,KAAI;YAC1C,MAAM,aAAa,GAAG,KAAK;iBACxB,MAAM,CAAC,SAAS,IAAI,SAAS,IAAI,IAAI,CAAC,aAAa,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;YAE9F,IAAI,aAAa,CAAC,MAAM,EAAE;gBACxB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;AAC7C,aAAA;AAAM,iBAAA;AACL,gBAAA,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;AACjC,aAAA;AACH,SAAC,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,KAAI;AACpC,YAAA,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;AACvB,gBAAA,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;AAC3B,aAAA;AACH,SAAC,CAAC,CAAA;KACH;AAED,IAAA,QAAQ,CAAC,EAAU,EAAA;AACjB,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;QAC5C,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,IAAI,IAAI,CAAC,aAAa,CAAC,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;KAC1E;AAED;;;AAGG;AACK,IAAA,QAAQ,CAAC,EAAU,EAAA;AACzB,QAAA,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE;AAChC,YAAA,OAAO,KAAK,CAAA;AACb,SAAA;AAED,QAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;AAAE,YAAA,OAAO,IAAI,CAAA;AAElC,QAAA,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;;AAGzB,QAAA,MAAM,mBAAmB,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAA;QAC9D,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;;QAGpC,MAAM,0CAA0C,GAAG,mBAAmB;aACnE,MAAM,CAAC,SAAS,IAAI,SAAS,IAAI,IAAI,CAAC,aAAa,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QAE9F,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,0CAA0C,CAAC,CAAA;QAExE,IAAI,0CAA0C,CAAC,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE;AACnF,YAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;AAClC,YAAA,OAAO,IAAI,CAAA;AACZ,SAAA;AAED,QAAA,OAAO,KAAK,CAAA;KACb;AAED;;;AAGG;AACH,IAAA,SAAS,CAAC,IAAsB,EAAA;AAC9B,QAAA,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;;AAGxB,QAAA,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC;AAClC,eAAA,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC;eAClC,OAAO,CAAC,MAAM,CAAC,aAAa;AAC5B,eAAA,EAAE,CAAA;;QAGP,OAAO,IAAI,CAAC,QAAQ,CAAU,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAA;KACzE;AAEF;;;;"}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { Extension, onConnectPayload } from '@hocuspocus/server';
|
|
2
3
|
export interface ThrottleConfiguration {
|
|
3
4
|
throttle: number | null | false;
|
|
5
|
+
consideredSeconds: number;
|
|
4
6
|
banTime: number;
|
|
7
|
+
cleanupInterval: number;
|
|
5
8
|
}
|
|
6
9
|
export declare class Throttle implements Extension {
|
|
7
10
|
configuration: ThrottleConfiguration;
|
|
8
11
|
connectionsByIp: Map<string, Array<number>>;
|
|
9
12
|
bannedIps: Map<string, number>;
|
|
13
|
+
cleanupInterval?: NodeJS.Timer;
|
|
10
14
|
/**
|
|
11
15
|
* Constructor
|
|
12
16
|
*/
|
|
13
17
|
constructor(configuration?: Partial<ThrottleConfiguration>);
|
|
18
|
+
onDestroy(): Promise<void>;
|
|
19
|
+
clearMaps(): void;
|
|
20
|
+
isBanned(ip: string): boolean;
|
|
14
21
|
/**
|
|
15
22
|
* Throttle requests
|
|
16
23
|
* @private
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Doc } from 'yjs';
|
|
2
|
-
import { Extensions } from '@tiptap/core
|
|
2
|
+
import { Extensions } from '@tiptap/core';
|
|
3
3
|
import { Transformer } from './types';
|
|
4
4
|
export declare class Tiptap implements Transformer {
|
|
5
5
|
defaultExtensions: Extensions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hocuspocus/extension-throttle",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "hocuspocus throttle extension",
|
|
5
5
|
"homepage": "https://hocuspocus.dev",
|
|
6
6
|
"keywords": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dist"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hocuspocus/server": "^1.0.
|
|
30
|
+
"@hocuspocus/server": "^1.0.2"
|
|
31
31
|
},
|
|
32
32
|
"gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d"
|
|
33
33
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
} from '@hocuspocus/server'
|
|
5
5
|
|
|
6
6
|
export interface ThrottleConfiguration {
|
|
7
|
-
throttle: number | null | false,
|
|
8
|
-
|
|
7
|
+
throttle: number | null | false, // how many requests within `consideredSeconds` until we're rejecting requests (setting this to 15 means the 16th request will be rejected)
|
|
8
|
+
consideredSeconds: number, // how many seconds to consider (default is last 60 seconds from the current connection attempt)
|
|
9
|
+
banTime: number, // for how long to ban after receiving too many requests (in minutes!)
|
|
10
|
+
cleanupInterval: number // how often to clean up the records of IPs (this won't delete ips that are still blocked or recent enough by `consideredSeconds`)
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export class Throttle implements Extension {
|
|
@@ -13,12 +15,16 @@ export class Throttle implements Extension {
|
|
|
13
15
|
configuration: ThrottleConfiguration = {
|
|
14
16
|
throttle: 15,
|
|
15
17
|
banTime: 5,
|
|
18
|
+
consideredSeconds: 60,
|
|
19
|
+
cleanupInterval: 90,
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
connectionsByIp: Map<string, Array<number>> = new Map()
|
|
19
23
|
|
|
20
24
|
bannedIps: Map<string, number> = new Map()
|
|
21
25
|
|
|
26
|
+
cleanupInterval?: NodeJS.Timer
|
|
27
|
+
|
|
22
28
|
/**
|
|
23
29
|
* Constructor
|
|
24
30
|
*/
|
|
@@ -27,6 +33,40 @@ export class Throttle implements Extension {
|
|
|
27
33
|
...this.configuration,
|
|
28
34
|
...configuration,
|
|
29
35
|
}
|
|
36
|
+
|
|
37
|
+
this.cleanupInterval = setInterval(this.clearMaps.bind(this), this.configuration.cleanupInterval * 1000)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onDestroy() {
|
|
41
|
+
if (this.cleanupInterval) {
|
|
42
|
+
clearInterval(this.cleanupInterval)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Promise.resolve()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public clearMaps() {
|
|
49
|
+
this.connectionsByIp.forEach((value, key) => {
|
|
50
|
+
const filteredValue = value
|
|
51
|
+
.filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now())
|
|
52
|
+
|
|
53
|
+
if (filteredValue.length) {
|
|
54
|
+
this.connectionsByIp.set(key, filteredValue)
|
|
55
|
+
} else {
|
|
56
|
+
this.connectionsByIp.delete(key)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
this.bannedIps.forEach((value, key) => {
|
|
61
|
+
if (!this.isBanned(key)) {
|
|
62
|
+
this.bannedIps.delete(key)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isBanned(ip: string) {
|
|
68
|
+
const bannedAt = this.bannedIps.get(ip) || 0
|
|
69
|
+
return Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))
|
|
30
70
|
}
|
|
31
71
|
|
|
32
72
|
/**
|
|
@@ -38,11 +78,7 @@ export class Throttle implements Extension {
|
|
|
38
78
|
return false
|
|
39
79
|
}
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))) {
|
|
44
|
-
return true
|
|
45
|
-
}
|
|
81
|
+
if (this.isBanned(ip)) return true
|
|
46
82
|
|
|
47
83
|
this.bannedIps.delete(ip)
|
|
48
84
|
|
|
@@ -50,13 +86,13 @@ export class Throttle implements Extension {
|
|
|
50
86
|
const previousConnections = this.connectionsByIp.get(ip) || []
|
|
51
87
|
previousConnections.push(Date.now())
|
|
52
88
|
|
|
53
|
-
// calculate the previous connections in the last
|
|
54
|
-
const
|
|
55
|
-
.filter(timestamp => timestamp + (
|
|
89
|
+
// calculate the previous connections in the last considered time interval
|
|
90
|
+
const previousConnectionsInTheConsideredInterval = previousConnections
|
|
91
|
+
.filter(timestamp => timestamp + (this.configuration.consideredSeconds * 1000) > Date.now())
|
|
56
92
|
|
|
57
|
-
this.connectionsByIp.set(ip,
|
|
93
|
+
this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval)
|
|
58
94
|
|
|
59
|
-
if (
|
|
95
|
+
if (previousConnectionsInTheConsideredInterval.length > this.configuration.throttle) {
|
|
60
96
|
this.bannedIps.set(ip, Date.now())
|
|
61
97
|
return true
|
|
62
98
|
}
|