@universis/janitor 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/dist/index.d.ts +46 -5
- package/dist/index.esm.js +446 -166
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +454 -172
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/public/schemas/v1/rate-limit-service.json +4 -4
- package/public/schemas/v1/speed-limit-service.json +4 -4
- package/src/AppRateLimitService.d.ts +5 -0
- package/src/AppRateLimitService.js +21 -0
- package/src/AppSpeedLimitService.d.ts +5 -0
- package/src/AppSpeedLimitService.js +21 -0
- package/src/RateLimitService.d.ts +20 -1
- package/src/RateLimitService.js +201 -70
- package/src/RedisClientStore.js +1 -1
- package/src/SpeedLimitService.d.ts +20 -2
- package/src/SpeedLimitService.js +207 -83
- package/src/index.d.ts +2 -0
- package/src/index.js +3 -1
- package/src/polyfills.js +0 -10
package/dist/index.js
CHANGED
|
@@ -1,115 +1,90 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
require('core-js/stable/string/replace-all');
|
|
3
4
|
var common = require('@themost/common');
|
|
4
5
|
var expressRateLimit = require('express-rate-limit');
|
|
5
6
|
var express = require('express');
|
|
6
7
|
var path = require('path');
|
|
8
|
+
var rxjs = require('rxjs');
|
|
7
9
|
var slowDown = require('express-slow-down');
|
|
8
|
-
var
|
|
10
|
+
var rateLimitRedis = require('rate-limit-redis');
|
|
9
11
|
var ioredis = require('ioredis');
|
|
10
12
|
require('@themost/promise-sequence');
|
|
11
13
|
var url = require('url');
|
|
12
14
|
var superagent = require('superagent');
|
|
13
15
|
var jwt = require('jsonwebtoken');
|
|
14
16
|
|
|
15
|
-
if (!String.prototype.replaceAll) {
|
|
16
|
-
String.prototype.replaceAll = function (str, newStr) {
|
|
17
|
-
// If a regex pattern
|
|
18
|
-
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
|
|
19
|
-
return this.replace(str, newStr);
|
|
20
|
-
}
|
|
21
|
-
// If a string
|
|
22
|
-
return this.replace(new RegExp(str, 'g'), newStr);
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
17
|
class RateLimitService extends common.ApplicationService {
|
|
27
18
|
/**
|
|
28
19
|
* @param {import('@themost/express').ExpressDataApplication} app
|
|
29
20
|
*/
|
|
30
21
|
constructor(app) {
|
|
31
22
|
super(app);
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
|
|
24
|
+
// get proxy address forwarding option
|
|
25
|
+
const proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
|
|
26
|
+
this.proxyAddressForwarding = typeof proxyAddressForwarding !== 'boolean' ? false : proxyAddressForwarding;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @type {BehaviorSubject<{ target: RateLimitService }>}
|
|
30
|
+
*/
|
|
31
|
+
this.loaded = new rxjs.BehaviorSubject(null);
|
|
32
|
+
|
|
33
|
+
const serviceContainer = this.getServiceContainer();
|
|
34
|
+
if (serviceContainer == null) {
|
|
35
|
+
common.TraceUtils.warn(`${this.getServiceName()} is being started but the parent router seems to be unavailable.`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
serviceContainer.subscribe((router) => {
|
|
39
|
+
if (router == null) {
|
|
34
40
|
return;
|
|
35
41
|
}
|
|
36
42
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
|
|
46
|
-
common.TraceUtils.log(`@universis/janitor#RateLimitService will try to extend service configuration from ${extendsPath}`);
|
|
47
|
-
serviceConfiguration = require(extendsPath);
|
|
48
|
-
}
|
|
49
|
-
const pathsArray = serviceConfiguration.paths || [];
|
|
50
|
-
const profilesArray = serviceConfiguration.profiles || [];
|
|
43
|
+
// set router for further processing
|
|
44
|
+
Object.defineProperty(this, 'router', {
|
|
45
|
+
value: express.Router(),
|
|
46
|
+
writable: false,
|
|
47
|
+
enumerable: false,
|
|
48
|
+
configurable: true
|
|
49
|
+
});
|
|
50
|
+
const serviceConfiguration = this.getServiceConfiguration();
|
|
51
51
|
// create maps
|
|
52
|
-
const paths =
|
|
53
|
-
const profiles = new Map(profilesArray);
|
|
52
|
+
const paths = serviceConfiguration.paths;
|
|
54
53
|
if (paths.size === 0) {
|
|
55
|
-
common.TraceUtils.warn(
|
|
56
|
-
}
|
|
57
|
-
// get proxy address forwarding option
|
|
58
|
-
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
|
|
59
|
-
if (typeof proxyAddressForwarding !== 'boolean') {
|
|
60
|
-
proxyAddressForwarding = false;
|
|
54
|
+
common.TraceUtils.warn(`${this.getServiceName()} is being started but the collection of paths is empty.`);
|
|
61
55
|
}
|
|
62
56
|
paths.forEach((value, path) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}, profile, {
|
|
77
|
-
keyGenerator: (req) => {
|
|
78
|
-
let remoteAddress;
|
|
79
|
-
if (proxyAddressForwarding) {
|
|
80
|
-
// get proxy headers or remote address
|
|
81
|
-
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
|
|
82
|
-
} else {
|
|
83
|
-
// get remote address
|
|
84
|
-
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
|
|
85
|
-
}
|
|
86
|
-
return `${path}:${remoteAddress}`;
|
|
87
|
-
}
|
|
57
|
+
this.set(path, value);
|
|
58
|
+
});
|
|
59
|
+
if (router.stack) {
|
|
60
|
+
router.stack.unshift.apply(router.stack, this.router.stack);
|
|
61
|
+
} else {
|
|
62
|
+
// use router
|
|
63
|
+
router.use(this.router);
|
|
64
|
+
// get router stack (use a workaround for express 4.x)
|
|
65
|
+
const stack = router._router && router._router.stack;
|
|
66
|
+
if (Array.isArray(stack)) {
|
|
67
|
+
// stage #1 find logger middleware (for supporting request logging)
|
|
68
|
+
let index = stack.findIndex((item) => {
|
|
69
|
+
return item.name === 'logger';
|
|
88
70
|
});
|
|
89
|
-
if (
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const storeModule = require(store[0]);
|
|
95
|
-
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
|
|
96
|
-
StoreClass = storeModule[store[1]];
|
|
97
|
-
rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
|
|
98
|
-
} else {
|
|
99
|
-
throw new Error(`${store} cannot be found or is inaccessible`);
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
StoreClass = require(store[0]);
|
|
103
|
-
// create store
|
|
104
|
-
rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
|
|
105
|
-
}
|
|
71
|
+
if (index === -1) {
|
|
72
|
+
// stage #2 find expressInit middleware
|
|
73
|
+
index = stack.findIndex((item) => {
|
|
74
|
+
return item.name === 'expressInit';
|
|
75
|
+
});
|
|
106
76
|
}
|
|
107
|
-
|
|
77
|
+
// if found, move the last middleware to be after expressInit
|
|
78
|
+
if (index > -1) {
|
|
79
|
+
// move the last middleware to be after expressInit
|
|
80
|
+
stack.splice(index + 1, 0, stack.pop());
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
common.TraceUtils.warn(`${this.getServiceName()} is being started but the container stack is not available.`);
|
|
108
84
|
}
|
|
109
|
-
});
|
|
110
|
-
if (addRouter.stack.length) {
|
|
111
|
-
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
|
|
112
85
|
}
|
|
86
|
+
// notify that the service is loaded
|
|
87
|
+
this.loaded.next({ target: this });
|
|
113
88
|
} catch (err) {
|
|
114
89
|
common.TraceUtils.error('An error occurred while validating rate limit configuration.');
|
|
115
90
|
common.TraceUtils.error(err);
|
|
@@ -118,106 +93,218 @@ class RateLimitService extends common.ApplicationService {
|
|
|
118
93
|
});
|
|
119
94
|
}
|
|
120
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Returns the service router that is used to register rate limit middleware.
|
|
98
|
+
* @returns {import('rxjs').BehaviorSubject<import('express').Router | import('express').Application>} The service router.
|
|
99
|
+
*/
|
|
100
|
+
getServiceContainer() {
|
|
101
|
+
return this.getApplication() && this.getApplication().serviceRouter;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns the service name.
|
|
106
|
+
* @returns {string} The service name.
|
|
107
|
+
*/
|
|
108
|
+
getServiceName() {
|
|
109
|
+
return '@universis/janitor#RateLimitService';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Returns the service configuration.
|
|
113
|
+
* @returns {{extends?: string, profiles?: Array, paths?: Array}} The service configuration.
|
|
114
|
+
*/
|
|
115
|
+
getServiceConfiguration() {
|
|
116
|
+
if (this.serviceConfiguration) {
|
|
117
|
+
return this.serviceConfiguration;
|
|
118
|
+
}
|
|
119
|
+
let serviceConfiguration = {
|
|
120
|
+
profiles: [],
|
|
121
|
+
paths: []
|
|
122
|
+
};
|
|
123
|
+
// get service configuration
|
|
124
|
+
const serviceConfigurationSource = this.getApplication().getConfiguration().getSourceAt('settings/universis/janitor/rateLimit');
|
|
125
|
+
if (serviceConfigurationSource) {
|
|
126
|
+
if (typeof serviceConfigurationSource.extends === 'string') {
|
|
127
|
+
// get additional configuration
|
|
128
|
+
const configurationPath = this.getApplication().getConfiguration().getConfigurationPath();
|
|
129
|
+
const extendsPath = path.resolve(configurationPath, serviceConfigurationSource.extends);
|
|
130
|
+
common.TraceUtils.log(`${this.getServiceName()} will try to extend service configuration using ${extendsPath}`);
|
|
131
|
+
serviceConfiguration = Object.assign({}, {
|
|
132
|
+
profiles: [],
|
|
133
|
+
paths: []
|
|
134
|
+
}, require(extendsPath));
|
|
135
|
+
} else {
|
|
136
|
+
common.TraceUtils.log(`${this.getServiceName()} will use service configuration from settings/universis/janitor/rateLimit`);
|
|
137
|
+
serviceConfiguration = Object.assign({}, {
|
|
138
|
+
profiles: [],
|
|
139
|
+
paths: []
|
|
140
|
+
}, serviceConfigurationSource);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const pathsArray = serviceConfiguration.paths || [];
|
|
144
|
+
const profilesArray = serviceConfiguration.profiles || [];
|
|
145
|
+
// create maps
|
|
146
|
+
serviceConfiguration.paths = new Map(pathsArray);
|
|
147
|
+
serviceConfiguration.profiles = new Map(profilesArray);
|
|
148
|
+
// set service configuration
|
|
149
|
+
Object.defineProperty(this, 'serviceConfiguration', {
|
|
150
|
+
value: serviceConfiguration,
|
|
151
|
+
writable: false,
|
|
152
|
+
enumerable: false,
|
|
153
|
+
configurable: true
|
|
154
|
+
});
|
|
155
|
+
return this.serviceConfiguration;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Sets the rate limit configuration for a specific path.
|
|
160
|
+
* @param {string} path
|
|
161
|
+
* @param {{ profile: string } | import('express-rate-limit').Options} options
|
|
162
|
+
* @returns {RateLimitService} The service instance for chaining.
|
|
163
|
+
*/
|
|
164
|
+
set(path, options) {
|
|
165
|
+
let opts;
|
|
166
|
+
// get profile
|
|
167
|
+
if (options.profile) {
|
|
168
|
+
opts = this.serviceConfiguration.profiles.get(options.profile);
|
|
169
|
+
} else {
|
|
170
|
+
// or options defined inline
|
|
171
|
+
opts = options;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* @type { import('express-rate-limit').Options }
|
|
175
|
+
*/
|
|
176
|
+
const rateLimitOptions = Object.assign({
|
|
177
|
+
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
178
|
+
limit: 50, // 50 requests
|
|
179
|
+
legacyHeaders: true // send headers
|
|
180
|
+
}, opts, {
|
|
181
|
+
keyGenerator: (req) => {
|
|
182
|
+
let remoteAddress;
|
|
183
|
+
if (this.proxyAddressForwarding) {
|
|
184
|
+
// get proxy headers or remote address
|
|
185
|
+
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
|
|
186
|
+
} else {
|
|
187
|
+
// get remote address
|
|
188
|
+
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
|
|
189
|
+
}
|
|
190
|
+
return `${path}:${remoteAddress}`;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (typeof rateLimitOptions.store === 'undefined') {
|
|
194
|
+
const StoreClass = this.getStoreType();
|
|
195
|
+
if (typeof StoreClass === 'function') {
|
|
196
|
+
rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.router.use(path, expressRateLimit.rateLimit(rateLimitOptions));
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @returns {function} The type of store used for rate limiting.
|
|
205
|
+
*/
|
|
206
|
+
getStoreType() {
|
|
207
|
+
const serviceConfiguration = this.getServiceConfiguration();
|
|
208
|
+
if (typeof serviceConfiguration.storeType !== 'string') {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let StoreClass;
|
|
212
|
+
const store = serviceConfiguration.storeType.split('#');
|
|
213
|
+
if (store.length === 2) {
|
|
214
|
+
const storeModule = require(store[0]);
|
|
215
|
+
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
|
|
216
|
+
StoreClass = storeModule[store[1]];
|
|
217
|
+
return StoreClass;
|
|
218
|
+
} else {
|
|
219
|
+
throw new Error(`${store} cannot be found or is inaccessible`);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
StoreClass = require(store[0]);
|
|
223
|
+
return StoreClass;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Unsets the rate limit configuration for a specific path.
|
|
229
|
+
* @param {string} path
|
|
230
|
+
* @returns {RateLimitService} The service instance for chaining.
|
|
231
|
+
*/
|
|
232
|
+
unset(path) {
|
|
233
|
+
const index = this.router.stack.findIndex((layer) => {
|
|
234
|
+
return layer.route && layer.route.path === path;
|
|
235
|
+
});
|
|
236
|
+
if (index !== -1) {
|
|
237
|
+
this.router.stack.splice(index, 1);
|
|
238
|
+
}
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
121
242
|
}
|
|
122
243
|
|
|
123
244
|
class SpeedLimitService extends common.ApplicationService {
|
|
124
245
|
constructor(app) {
|
|
125
246
|
super(app);
|
|
126
247
|
|
|
127
|
-
|
|
128
|
-
|
|
248
|
+
// get proxy address forwarding option
|
|
249
|
+
const proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
|
|
250
|
+
this.proxyAddressForwarding = typeof proxyAddressForwarding !== 'boolean' ? false : proxyAddressForwarding;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @type {BehaviorSubject<{ target: SpeedLimitService }>}
|
|
254
|
+
*/
|
|
255
|
+
this.loaded = new rxjs.BehaviorSubject(null);
|
|
256
|
+
|
|
257
|
+
const serviceContainer = this.getServiceContainer();
|
|
258
|
+
if (serviceContainer == null) {
|
|
259
|
+
common.TraceUtils.warn(`${this.getServiceName()} is being started but the parent router seems to be unavailable.`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
serviceContainer.subscribe((router) => {
|
|
264
|
+
if (router == null) {
|
|
129
265
|
return;
|
|
130
266
|
}
|
|
131
267
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
|
|
141
|
-
common.TraceUtils.log(`@universis/janitor#SpeedLimitService will try to extend service configuration from ${extendsPath}`);
|
|
142
|
-
serviceConfiguration = require(extendsPath);
|
|
143
|
-
}
|
|
144
|
-
const pathsArray = serviceConfiguration.paths || [];
|
|
145
|
-
const profilesArray = serviceConfiguration.profiles || [];
|
|
268
|
+
// set router for further processing
|
|
269
|
+
Object.defineProperty(this, 'router', {
|
|
270
|
+
value: express.Router(),
|
|
271
|
+
writable: false,
|
|
272
|
+
enumerable: false,
|
|
273
|
+
configurable: true
|
|
274
|
+
});
|
|
275
|
+
const serviceConfiguration = this.getServiceConfiguration();
|
|
146
276
|
// create maps
|
|
147
|
-
const paths =
|
|
148
|
-
const profiles = new Map(profilesArray);
|
|
277
|
+
const paths = serviceConfiguration.paths;
|
|
149
278
|
if (paths.size === 0) {
|
|
150
|
-
common.TraceUtils.warn(
|
|
151
|
-
}
|
|
152
|
-
// get proxy address forwarding option
|
|
153
|
-
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
|
|
154
|
-
if (typeof proxyAddressForwarding !== 'boolean') {
|
|
155
|
-
proxyAddressForwarding = false;
|
|
279
|
+
common.TraceUtils.warn(`${this.getServiceName()} is being started but the collection of paths is empty.`);
|
|
156
280
|
}
|
|
157
281
|
paths.forEach((value, path) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
maxDelayMs: 10000 // 10 seconds
|
|
172
|
-
}, profile, {
|
|
173
|
-
keyGenerator: (req) => {
|
|
174
|
-
let remoteAddress;
|
|
175
|
-
if (proxyAddressForwarding) {
|
|
176
|
-
// get proxy headers or remote address
|
|
177
|
-
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
|
|
178
|
-
} else {
|
|
179
|
-
// get remote address
|
|
180
|
-
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
|
|
181
|
-
}
|
|
182
|
-
return `${path}:${remoteAddress}`;
|
|
183
|
-
}
|
|
282
|
+
this.set(path, value);
|
|
283
|
+
});
|
|
284
|
+
if (router.stack) {
|
|
285
|
+
router.stack.unshift.apply(router.stack, this.router.stack);
|
|
286
|
+
} else {
|
|
287
|
+
// use router
|
|
288
|
+
router.use(this.router);
|
|
289
|
+
// get router stack (use a workaround for express 4.x)
|
|
290
|
+
const stack = router._router && router._router.stack;
|
|
291
|
+
if (Array.isArray(stack)) {
|
|
292
|
+
// stage #1 find logger middleware (for supporting request logging)
|
|
293
|
+
let index = stack.findIndex((item) => {
|
|
294
|
+
return item.name === 'logger';
|
|
184
295
|
});
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
if (Array.isArray(slowDownOptions.randomMaxDelayMs)) {
|
|
192
|
-
slowDownOptions.maxDelayMs = () => {
|
|
193
|
-
const maxDelayMs = Math.floor(Math.random() * (slowDownOptions.randomMaxDelayMs[1] - slowDownOptions.randomMaxDelayMs[0] + 1) + slowDownOptions.randomMaxDelayMs[0]);
|
|
194
|
-
return maxDelayMs;
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
if (typeof slowDownOptions.store === 'string') {
|
|
198
|
-
// load store
|
|
199
|
-
const store = slowDownOptions.store.split('#');
|
|
200
|
-
let StoreClass;
|
|
201
|
-
if (store.length === 2) {
|
|
202
|
-
const storeModule = require(store[0]);
|
|
203
|
-
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
|
|
204
|
-
StoreClass = storeModule[store[1]];
|
|
205
|
-
slowDownOptions.store = new StoreClass(this, slowDownOptions);
|
|
206
|
-
} else {
|
|
207
|
-
throw new Error(`${store} cannot be found or is inaccessible`);
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
StoreClass = require(store[0]);
|
|
211
|
-
// create store
|
|
212
|
-
slowDownOptions.store = new StoreClass(this, slowDownOptions);
|
|
213
|
-
}
|
|
296
|
+
if (index === -1) {
|
|
297
|
+
// stage #2 find expressInit middleware
|
|
298
|
+
index = stack.findIndex((item) => {
|
|
299
|
+
return item.name === 'expressInit';
|
|
300
|
+
});
|
|
214
301
|
}
|
|
215
|
-
|
|
302
|
+
} else {
|
|
303
|
+
common.TraceUtils.warn(`${this.getServiceName()} is being started but the container stack is not available.`);
|
|
216
304
|
}
|
|
217
|
-
});
|
|
218
|
-
if (addRouter.stack.length) {
|
|
219
|
-
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
|
|
220
305
|
}
|
|
306
|
+
// notify that the service is loaded
|
|
307
|
+
this.loaded.next({ target: this });
|
|
221
308
|
} catch (err) {
|
|
222
309
|
common.TraceUtils.error('An error occurred while validating speed limit configuration.');
|
|
223
310
|
common.TraceUtils.error(err);
|
|
@@ -226,6 +313,163 @@ class SpeedLimitService extends common.ApplicationService {
|
|
|
226
313
|
});
|
|
227
314
|
}
|
|
228
315
|
|
|
316
|
+
/**
|
|
317
|
+
* @returns {function} The type of store used for rate limiting.
|
|
318
|
+
*/
|
|
319
|
+
getStoreType() {
|
|
320
|
+
const serviceConfiguration = this.getServiceConfiguration();
|
|
321
|
+
if (typeof serviceConfiguration.storeType !== 'string') {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
let StoreClass;
|
|
325
|
+
const store = serviceConfiguration.storeType.split('#');
|
|
326
|
+
if (store.length === 2) {
|
|
327
|
+
const storeModule = require(store[0]);
|
|
328
|
+
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
|
|
329
|
+
StoreClass = storeModule[store[1]];
|
|
330
|
+
return StoreClass;
|
|
331
|
+
} else {
|
|
332
|
+
throw new Error(`${store} cannot be found or is inaccessible`);
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
StoreClass = require(store[0]);
|
|
336
|
+
return StoreClass;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Returns the service name.
|
|
342
|
+
* @returns {string} The service name.
|
|
343
|
+
*/
|
|
344
|
+
getServiceName() {
|
|
345
|
+
return '@universis/janitor#SpeedLimitService';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Returns the service router that is used to register speed limit middleware.
|
|
350
|
+
* @returns {import('express').Router | import('express').Application} The service router.
|
|
351
|
+
*/
|
|
352
|
+
getServiceContainer() {
|
|
353
|
+
return this.getApplication() && this.getApplication().serviceRouter;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Sets the speed limit configuration for a specific path.
|
|
358
|
+
* @param {string} path
|
|
359
|
+
* @param {{ profile: string } | import('express-slow-down').Options} options
|
|
360
|
+
* @returns {SpeedLimitService} The service instance for chaining.
|
|
361
|
+
*/
|
|
362
|
+
set(path, options) {
|
|
363
|
+
let opts;
|
|
364
|
+
// get profile
|
|
365
|
+
if (options.profile) {
|
|
366
|
+
opts = this.serviceConfiguration.profiles.get(options.profile);
|
|
367
|
+
} else {
|
|
368
|
+
// or options defined inline
|
|
369
|
+
opts = options;
|
|
370
|
+
}
|
|
371
|
+
const slowDownOptions = Object.assign({
|
|
372
|
+
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
373
|
+
delayAfter: 20, // 20 requests
|
|
374
|
+
delayMs: 500, // 500 ms
|
|
375
|
+
maxDelayMs: 10000 // 10 seconds
|
|
376
|
+
}, opts, {
|
|
377
|
+
keyGenerator: (req) => {
|
|
378
|
+
let remoteAddress;
|
|
379
|
+
if (this.proxyAddressForwarding) {
|
|
380
|
+
// get proxy headers or remote address
|
|
381
|
+
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
|
|
382
|
+
} else {
|
|
383
|
+
// get remote address
|
|
384
|
+
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
|
|
385
|
+
}
|
|
386
|
+
return `${path}:${remoteAddress}`;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
if (Array.isArray(slowDownOptions.randomDelayMs)) {
|
|
390
|
+
slowDownOptions.delayMs = () => {
|
|
391
|
+
const delayMs = Math.floor(Math.random() * (slowDownOptions.randomDelayMs[1] - slowDownOptions.randomDelayMs[0] + 1) + slowDownOptions.randomDelayMs[0]);
|
|
392
|
+
return delayMs;
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (Array.isArray(slowDownOptions.randomMaxDelayMs)) {
|
|
396
|
+
slowDownOptions.maxDelayMs = () => {
|
|
397
|
+
const maxDelayMs = Math.floor(Math.random() * (slowDownOptions.randomMaxDelayMs[1] - slowDownOptions.randomMaxDelayMs[0] + 1) + slowDownOptions.randomMaxDelayMs[0]);
|
|
398
|
+
return maxDelayMs;
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (typeof slowDownOptions.store === 'undefined') {
|
|
402
|
+
const StoreClass = this.getStoreType();
|
|
403
|
+
if (typeof StoreClass === 'function') {
|
|
404
|
+
slowDownOptions.store = new StoreClass(this, slowDownOptions);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
this.router.use(path, slowDown(slowDownOptions));
|
|
408
|
+
return this;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Unsets the speed limit configuration for a specific path.
|
|
414
|
+
* @param {string} path
|
|
415
|
+
* @return {SpeedLimitService} The service instance for chaining.
|
|
416
|
+
*/
|
|
417
|
+
unset(path) {
|
|
418
|
+
const index = this.router.stack.findIndex((layer) => {
|
|
419
|
+
return layer.route && layer.route.path === path;
|
|
420
|
+
});
|
|
421
|
+
if (index !== -1) {
|
|
422
|
+
this.router.stack.splice(index, 1);
|
|
423
|
+
}
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
*
|
|
429
|
+
* @returns {{extends?: string, profiles?: Array, paths?: Array}} The service configuration.
|
|
430
|
+
*/
|
|
431
|
+
getServiceConfiguration() {
|
|
432
|
+
if (this.serviceConfiguration) {
|
|
433
|
+
return this.serviceConfiguration;
|
|
434
|
+
}
|
|
435
|
+
let serviceConfiguration = {
|
|
436
|
+
profiles: [],
|
|
437
|
+
paths: []
|
|
438
|
+
};
|
|
439
|
+
// get service configuration
|
|
440
|
+
const serviceConfigurationSource = this.getApplication().getConfiguration().getSourceAt('settings/universis/janitor/speedLimit');
|
|
441
|
+
if (serviceConfigurationSource) {
|
|
442
|
+
if (typeof serviceConfigurationSource.extends === 'string') {
|
|
443
|
+
// get additional configuration
|
|
444
|
+
const configurationPath = this.getApplication().getConfiguration().getConfigurationPath();
|
|
445
|
+
const extendsPath = path.resolve(configurationPath, serviceConfigurationSource.extends);
|
|
446
|
+
common.TraceUtils.log(`${this.getServiceName()} will try to extend service configuration using ${extendsPath}`);
|
|
447
|
+
serviceConfiguration = Object.assign({}, {
|
|
448
|
+
profiles: [],
|
|
449
|
+
paths: []
|
|
450
|
+
}, require(extendsPath));
|
|
451
|
+
} else {
|
|
452
|
+
common.TraceUtils.log(`${this.getServiceName()} will use service configuration from settings/universis/janitor/speedLimit`);
|
|
453
|
+
serviceConfiguration = Object.assign({}, {
|
|
454
|
+
profiles: [],
|
|
455
|
+
paths: []
|
|
456
|
+
}, serviceConfigurationSource);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const profilesArray = serviceConfiguration.profiles || [];
|
|
460
|
+
serviceConfiguration.profiles = new Map(profilesArray);
|
|
461
|
+
const pathsArray = serviceConfiguration.paths || [];
|
|
462
|
+
serviceConfiguration.paths = new Map(pathsArray);
|
|
463
|
+
|
|
464
|
+
Object.defineProperty(this, 'serviceConfiguration', {
|
|
465
|
+
value: serviceConfiguration,
|
|
466
|
+
writable: false,
|
|
467
|
+
enumerable: false,
|
|
468
|
+
configurable: true
|
|
469
|
+
});
|
|
470
|
+
return this.serviceConfiguration;
|
|
471
|
+
}
|
|
472
|
+
|
|
229
473
|
}
|
|
230
474
|
|
|
231
475
|
function _defineProperty(e, r, t) {
|
|
@@ -262,19 +506,19 @@ function noLoadIncrementScript() {
|
|
|
262
506
|
|
|
263
507
|
//
|
|
264
508
|
}
|
|
265
|
-
if (RedisStore.prototype.loadIncrementScript.name === 'loadIncrementScript') {
|
|
509
|
+
if (rateLimitRedis.RedisStore.prototype.loadIncrementScript.name === 'loadIncrementScript') {
|
|
266
510
|
// get super method for future use
|
|
267
|
-
superLoadIncrementScript = RedisStore.prototype.loadIncrementScript;
|
|
268
|
-
RedisStore.prototype.loadIncrementScript = noLoadIncrementScript;
|
|
511
|
+
superLoadIncrementScript = rateLimitRedis.RedisStore.prototype.loadIncrementScript;
|
|
512
|
+
rateLimitRedis.RedisStore.prototype.loadIncrementScript = noLoadIncrementScript;
|
|
269
513
|
}
|
|
270
514
|
|
|
271
|
-
if (RedisStore.prototype.loadGetScript.name === 'loadGetScript') {
|
|
515
|
+
if (rateLimitRedis.RedisStore.prototype.loadGetScript.name === 'loadGetScript') {
|
|
272
516
|
// get super method
|
|
273
|
-
superLoadGetScript = RedisStore.prototype.loadGetScript;
|
|
274
|
-
RedisStore.prototype.loadGetScript = noLoadGetScript;
|
|
517
|
+
superLoadGetScript = rateLimitRedis.RedisStore.prototype.loadGetScript;
|
|
518
|
+
rateLimitRedis.RedisStore.prototype.loadGetScript = noLoadGetScript;
|
|
275
519
|
}
|
|
276
520
|
|
|
277
|
-
class RedisClientStore extends RedisStore {
|
|
521
|
+
class RedisClientStore extends rateLimitRedis.RedisStore {
|
|
278
522
|
|
|
279
523
|
|
|
280
524
|
|
|
@@ -904,6 +1148,44 @@ class RemoteAddressValidator extends common.ApplicationService {
|
|
|
904
1148
|
|
|
905
1149
|
}
|
|
906
1150
|
|
|
1151
|
+
class AppRateLimitService extends RateLimitService {
|
|
1152
|
+
/**
|
|
1153
|
+
*
|
|
1154
|
+
* @param {import('@themost/common').ApplicationBase} app
|
|
1155
|
+
*/
|
|
1156
|
+
constructor(app) {
|
|
1157
|
+
super(app);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
getServiceName() {
|
|
1161
|
+
return '@universis/janitor#AppRateLimitService';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
getServiceContainer() {
|
|
1165
|
+
return this.getApplication() && this.getApplication().container;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
class AppSpeedLimitService extends SpeedLimitService {
|
|
1170
|
+
/**
|
|
1171
|
+
*
|
|
1172
|
+
* @param {import('@themost/common').ApplicationBase} app
|
|
1173
|
+
*/
|
|
1174
|
+
constructor(app) {
|
|
1175
|
+
super(app);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
getServiceName() {
|
|
1179
|
+
return '@universis/janitor#AppSpeedLimitService';
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
getServiceContainer() {
|
|
1183
|
+
return this.getApplication() && this.getApplication().container;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
exports.AppRateLimitService = AppRateLimitService;
|
|
1188
|
+
exports.AppSpeedLimitService = AppSpeedLimitService;
|
|
907
1189
|
exports.DefaultScopeAccessConfiguration = DefaultScopeAccessConfiguration;
|
|
908
1190
|
exports.EnableScopeAccessConfiguration = EnableScopeAccessConfiguration;
|
|
909
1191
|
exports.ExtendScopeAccessConfiguration = ExtendScopeAccessConfiguration;
|