@hocuspocus/extension-throttle 1.0.0-beta.7 → 1.0.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.
@@ -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
- const bannedAt = this.bannedIps.get(ip) || 0;
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 minute
38
- const previousConnectionsInTheLastMinute = previousConnections
39
- .filter(timestamp => timestamp + (60 * 1000) > Date.now());
40
- this.connectionsByIp.set(ip, previousConnectionsInTheLastMinute);
41
- if (previousConnectionsInTheLastMinute.length > this.configuration.throttle) {
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,\n banTime: number,\n}\n\nexport class Throttle implements Extension {\n\n configuration: ThrottleConfiguration = {\n throttle: 15,\n banTime: 5,\n }\n\n connectionsByIp: Map<string, Array<number>> = new Map()\n\n bannedIps: Map<string, number> = new Map()\n\n /**\n * Constructor\n */\n constructor(configuration?: Partial<ThrottleConfiguration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\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 const bannedAt = this.bannedIps.get(ip) || 0\n\n if (Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))) {\n return true\n }\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 minute\n const previousConnectionsInTheLastMinute = previousConnections\n .filter(timestamp => timestamp + (60 * 1000) > Date.now())\n\n this.connectionsByIp.set(ip, previousConnectionsInTheLastMinute)\n\n if (previousConnectionsInTheLastMinute.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":";;;;MAUa,QAAQ,CAAA;AAWnB;;AAEG;AACH,IAAA,WAAA,CAAY,aAA8C,EAAA;AAZ1D,QAAA,IAAA,CAAA,aAAa,GAA0B;AACrC,YAAA,QAAQ,EAAE,EAAE;AACZ,YAAA,OAAO,EAAE,CAAC;SACX,CAAA;AAED,QAAA,IAAA,CAAA,eAAe,GAA+B,IAAI,GAAG,EAAE,CAAA;AAEvD,QAAA,IAAA,CAAA,SAAS,GAAwB,IAAI,GAAG,EAAE,CAAA;QAMxC,IAAI,CAAC,aAAa,GAAG;YACnB,GAAG,IAAI,CAAC,aAAa;AACrB,YAAA,GAAG,aAAa;SACjB,CAAA;KACF;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,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;QAE5C,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,IAAI,IAAI,CAAC,aAAa,CAAC,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,EAAE;AACtE,YAAA,OAAO,IAAI,CAAA;AACZ,SAAA;AAED,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,kCAAkC,GAAG,mBAAmB;AAC3D,aAAA,MAAM,CAAC,SAAS,IAAI,SAAS,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QAE5D,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,kCAAkC,CAAC,CAAA;QAEhE,IAAI,kCAAkC,CAAC,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE;AAC3E,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
+ {"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
- const bannedAt = this.bannedIps.get(ip) || 0;
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 minute
34
- const previousConnectionsInTheLastMinute = previousConnections
35
- .filter(timestamp => timestamp + (60 * 1000) > Date.now());
36
- this.connectionsByIp.set(ip, previousConnectionsInTheLastMinute);
37
- if (previousConnectionsInTheLastMinute.length > this.configuration.throttle) {
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,\n banTime: number,\n}\n\nexport class Throttle implements Extension {\n\n configuration: ThrottleConfiguration = {\n throttle: 15,\n banTime: 5,\n }\n\n connectionsByIp: Map<string, Array<number>> = new Map()\n\n bannedIps: Map<string, number> = new Map()\n\n /**\n * Constructor\n */\n constructor(configuration?: Partial<ThrottleConfiguration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\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 const bannedAt = this.bannedIps.get(ip) || 0\n\n if (Date.now() < (bannedAt + (this.configuration.banTime * 60 * 1000))) {\n return true\n }\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 minute\n const previousConnectionsInTheLastMinute = previousConnections\n .filter(timestamp => timestamp + (60 * 1000) > Date.now())\n\n this.connectionsByIp.set(ip, previousConnectionsInTheLastMinute)\n\n if (previousConnectionsInTheLastMinute.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":"MAUa,QAAQ,CAAA;AAWnB;;AAEG;AACH,IAAA,WAAA,CAAY,aAA8C,EAAA;AAZ1D,QAAA,IAAA,CAAA,aAAa,GAA0B;AACrC,YAAA,QAAQ,EAAE,EAAE;AACZ,YAAA,OAAO,EAAE,CAAC;SACX,CAAA;AAED,QAAA,IAAA,CAAA,eAAe,GAA+B,IAAI,GAAG,EAAE,CAAA;AAEvD,QAAA,IAAA,CAAA,SAAS,GAAwB,IAAI,GAAG,EAAE,CAAA;QAMxC,IAAI,CAAC,aAAa,GAAG;YACnB,GAAG,IAAI,CAAC,aAAa;AACrB,YAAA,GAAG,aAAa;SACjB,CAAA;KACF;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,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;QAE5C,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,IAAI,IAAI,CAAC,aAAa,CAAC,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,EAAE;AACtE,YAAA,OAAO,IAAI,CAAA;AACZ,SAAA;AAED,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,kCAAkC,GAAG,mBAAmB;AAC3D,aAAA,MAAM,CAAC,SAAS,IAAI,SAAS,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QAE5D,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,kCAAkC,CAAC,CAAA;QAEhE,IAAI,kCAAkC,CAAC,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE;AAC3E,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
+ {"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":"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"}
@@ -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
@@ -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.0-beta.7",
3
+ "version": "1.0.1",
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.0-beta.7"
30
+ "@hocuspocus/server": "^1.0.1"
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
- banTime: number,
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
- const bannedAt = this.bannedIps.get(ip) || 0
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 minute
54
- const previousConnectionsInTheLastMinute = previousConnections
55
- .filter(timestamp => timestamp + (60 * 1000) > Date.now())
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, previousConnectionsInTheLastMinute)
93
+ this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval)
58
94
 
59
- if (previousConnectionsInTheLastMinute.length > this.configuration.throttle) {
95
+ if (previousConnectionsInTheConsideredInterval.length > this.configuration.throttle) {
60
96
  this.bannedIps.set(ip, Date.now())
61
97
  return true
62
98
  }