@universis/janitor 1.9.0 → 1.11.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 +176 -5
- package/dist/index.esm.js +591 -154
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +598 -153
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- 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/HttpBearerStategy.js +1 -1
- package/src/PassportService.d.ts +6 -0
- package/src/PassportService.js +27 -0
- package/src/RateLimitService.d.ts +20 -1
- package/src/RateLimitService.js +201 -70
- package/src/SpeedLimitService.d.ts +20 -2
- package/src/SpeedLimitService.js +207 -83
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -0
package/dist/index.esm.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ApplicationService, TraceUtils, ConfigurationStrategy, Args, HttpForbid
|
|
|
3
3
|
import { rateLimit } from 'express-rate-limit';
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import { BehaviorSubject } from 'rxjs';
|
|
6
7
|
import slowDown from 'express-slow-down';
|
|
7
8
|
import { RedisStore } from 'rate-limit-redis';
|
|
8
9
|
import { Redis } from 'ioredis';
|
|
@@ -10,6 +11,8 @@ import '@themost/promise-sequence';
|
|
|
10
11
|
import url, { URL } from 'url';
|
|
11
12
|
import { Request } from 'superagent';
|
|
12
13
|
import jwt from 'jsonwebtoken';
|
|
14
|
+
import BearerStrategy from 'passport-http-bearer';
|
|
15
|
+
import passport from 'passport';
|
|
13
16
|
|
|
14
17
|
class RateLimitService extends ApplicationService {
|
|
15
18
|
/**
|
|
@@ -17,87 +20,71 @@ class RateLimitService extends ApplicationService {
|
|
|
17
20
|
*/
|
|
18
21
|
constructor(app) {
|
|
19
22
|
super(app);
|
|
20
|
-
|
|
21
|
-
|
|
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 BehaviorSubject(null);
|
|
32
|
+
|
|
33
|
+
const serviceContainer = this.getServiceContainer();
|
|
34
|
+
if (serviceContainer == null) {
|
|
35
|
+
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) {
|
|
22
40
|
return;
|
|
23
41
|
}
|
|
24
42
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
|
|
34
|
-
TraceUtils.log(`@universis/janitor#RateLimitService will try to extend service configuration from ${extendsPath}`);
|
|
35
|
-
serviceConfiguration = require(extendsPath);
|
|
36
|
-
}
|
|
37
|
-
const pathsArray = serviceConfiguration.paths || [];
|
|
38
|
-
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();
|
|
39
51
|
// create maps
|
|
40
|
-
const paths =
|
|
41
|
-
const profiles = new Map(profilesArray);
|
|
52
|
+
const paths = serviceConfiguration.paths;
|
|
42
53
|
if (paths.size === 0) {
|
|
43
|
-
TraceUtils.warn(
|
|
44
|
-
}
|
|
45
|
-
// get proxy address forwarding option
|
|
46
|
-
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
|
|
47
|
-
if (typeof proxyAddressForwarding !== 'boolean') {
|
|
48
|
-
proxyAddressForwarding = false;
|
|
54
|
+
TraceUtils.warn(`${this.getServiceName()} is being started but the collection of paths is empty.`);
|
|
49
55
|
}
|
|
50
56
|
paths.forEach((value, path) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}, profile, {
|
|
65
|
-
keyGenerator: (req) => {
|
|
66
|
-
let remoteAddress;
|
|
67
|
-
if (proxyAddressForwarding) {
|
|
68
|
-
// get proxy headers or remote address
|
|
69
|
-
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
|
|
70
|
-
} else {
|
|
71
|
-
// get remote address
|
|
72
|
-
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
|
|
73
|
-
}
|
|
74
|
-
return `${path}:${remoteAddress}`;
|
|
75
|
-
}
|
|
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';
|
|
76
70
|
});
|
|
77
|
-
if (
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const storeModule = require(store[0]);
|
|
83
|
-
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
|
|
84
|
-
StoreClass = storeModule[store[1]];
|
|
85
|
-
rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
|
|
86
|
-
} else {
|
|
87
|
-
throw new Error(`${store} cannot be found or is inaccessible`);
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
StoreClass = require(store[0]);
|
|
91
|
-
// create store
|
|
92
|
-
rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
|
|
93
|
-
}
|
|
71
|
+
if (index === -1) {
|
|
72
|
+
// stage #2 find expressInit middleware
|
|
73
|
+
index = stack.findIndex((item) => {
|
|
74
|
+
return item.name === 'expressInit';
|
|
75
|
+
});
|
|
94
76
|
}
|
|
95
|
-
|
|
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
|
+
TraceUtils.warn(`${this.getServiceName()} is being started but the container stack is not available.`);
|
|
96
84
|
}
|
|
97
|
-
});
|
|
98
|
-
if (addRouter.stack.length) {
|
|
99
|
-
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
|
|
100
85
|
}
|
|
86
|
+
// notify that the service is loaded
|
|
87
|
+
this.loaded.next({ target: this });
|
|
101
88
|
} catch (err) {
|
|
102
89
|
TraceUtils.error('An error occurred while validating rate limit configuration.');
|
|
103
90
|
TraceUtils.error(err);
|
|
@@ -106,106 +93,218 @@ class RateLimitService extends ApplicationService {
|
|
|
106
93
|
});
|
|
107
94
|
}
|
|
108
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
|
+
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
|
+
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, 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
|
+
|
|
109
242
|
}
|
|
110
243
|
|
|
111
244
|
class SpeedLimitService extends ApplicationService {
|
|
112
245
|
constructor(app) {
|
|
113
246
|
super(app);
|
|
114
247
|
|
|
115
|
-
|
|
116
|
-
|
|
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 BehaviorSubject(null);
|
|
256
|
+
|
|
257
|
+
const serviceContainer = this.getServiceContainer();
|
|
258
|
+
if (serviceContainer == null) {
|
|
259
|
+
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) {
|
|
117
265
|
return;
|
|
118
266
|
}
|
|
119
267
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
|
|
129
|
-
TraceUtils.log(`@universis/janitor#SpeedLimitService will try to extend service configuration from ${extendsPath}`);
|
|
130
|
-
serviceConfiguration = require(extendsPath);
|
|
131
|
-
}
|
|
132
|
-
const pathsArray = serviceConfiguration.paths || [];
|
|
133
|
-
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();
|
|
134
276
|
// create maps
|
|
135
|
-
const paths =
|
|
136
|
-
const profiles = new Map(profilesArray);
|
|
277
|
+
const paths = serviceConfiguration.paths;
|
|
137
278
|
if (paths.size === 0) {
|
|
138
|
-
TraceUtils.warn(
|
|
139
|
-
}
|
|
140
|
-
// get proxy address forwarding option
|
|
141
|
-
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
|
|
142
|
-
if (typeof proxyAddressForwarding !== 'boolean') {
|
|
143
|
-
proxyAddressForwarding = false;
|
|
279
|
+
TraceUtils.warn(`${this.getServiceName()} is being started but the collection of paths is empty.`);
|
|
144
280
|
}
|
|
145
281
|
paths.forEach((value, path) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
maxDelayMs: 10000 // 10 seconds
|
|
160
|
-
}, profile, {
|
|
161
|
-
keyGenerator: (req) => {
|
|
162
|
-
let remoteAddress;
|
|
163
|
-
if (proxyAddressForwarding) {
|
|
164
|
-
// get proxy headers or remote address
|
|
165
|
-
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
|
|
166
|
-
} else {
|
|
167
|
-
// get remote address
|
|
168
|
-
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
|
|
169
|
-
}
|
|
170
|
-
return `${path}:${remoteAddress}`;
|
|
171
|
-
}
|
|
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';
|
|
172
295
|
});
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (Array.isArray(slowDownOptions.randomMaxDelayMs)) {
|
|
180
|
-
slowDownOptions.maxDelayMs = () => {
|
|
181
|
-
const maxDelayMs = Math.floor(Math.random() * (slowDownOptions.randomMaxDelayMs[1] - slowDownOptions.randomMaxDelayMs[0] + 1) + slowDownOptions.randomMaxDelayMs[0]);
|
|
182
|
-
return maxDelayMs;
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (typeof slowDownOptions.store === 'string') {
|
|
186
|
-
// load store
|
|
187
|
-
const store = slowDownOptions.store.split('#');
|
|
188
|
-
let StoreClass;
|
|
189
|
-
if (store.length === 2) {
|
|
190
|
-
const storeModule = require(store[0]);
|
|
191
|
-
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
|
|
192
|
-
StoreClass = storeModule[store[1]];
|
|
193
|
-
slowDownOptions.store = new StoreClass(this, slowDownOptions);
|
|
194
|
-
} else {
|
|
195
|
-
throw new Error(`${store} cannot be found or is inaccessible`);
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
StoreClass = require(store[0]);
|
|
199
|
-
// create store
|
|
200
|
-
slowDownOptions.store = new StoreClass(this, slowDownOptions);
|
|
201
|
-
}
|
|
296
|
+
if (index === -1) {
|
|
297
|
+
// stage #2 find expressInit middleware
|
|
298
|
+
index = stack.findIndex((item) => {
|
|
299
|
+
return item.name === 'expressInit';
|
|
300
|
+
});
|
|
202
301
|
}
|
|
203
|
-
|
|
302
|
+
} else {
|
|
303
|
+
TraceUtils.warn(`${this.getServiceName()} is being started but the container stack is not available.`);
|
|
204
304
|
}
|
|
205
|
-
});
|
|
206
|
-
if (addRouter.stack.length) {
|
|
207
|
-
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
|
|
208
305
|
}
|
|
306
|
+
// notify that the service is loaded
|
|
307
|
+
this.loaded.next({ target: this });
|
|
209
308
|
} catch (err) {
|
|
210
309
|
TraceUtils.error('An error occurred while validating speed limit configuration.');
|
|
211
310
|
TraceUtils.error(err);
|
|
@@ -214,6 +313,163 @@ class SpeedLimitService extends ApplicationService {
|
|
|
214
313
|
});
|
|
215
314
|
}
|
|
216
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
|
+
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
|
+
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
|
+
|
|
217
473
|
}
|
|
218
474
|
|
|
219
475
|
function _defineProperty(e, r, t) {
|
|
@@ -892,5 +1148,186 @@ class RemoteAddressValidator extends ApplicationService {
|
|
|
892
1148
|
|
|
893
1149
|
}
|
|
894
1150
|
|
|
895
|
-
|
|
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
|
+
class HttpBearerTokenRequired extends HttpError {
|
|
1188
|
+
constructor() {
|
|
1189
|
+
super(499, 'A token is required to fulfill the request.');
|
|
1190
|
+
this.code = 'E_TOKEN_REQUIRED';
|
|
1191
|
+
this.title = 'Token Required';
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
class HttpBearerTokenNotFound extends HttpError {
|
|
1196
|
+
constructor() {
|
|
1197
|
+
super(498, 'Token was not found.');
|
|
1198
|
+
this.code = 'E_TOKEN_NOT_FOUND';
|
|
1199
|
+
this.title = 'Invalid token';
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
class HttpBearerTokenExpired extends HttpError {
|
|
1204
|
+
constructor() {
|
|
1205
|
+
super(498, 'Token was expired or is in invalid state.');
|
|
1206
|
+
this.code = 'E_TOKEN_EXPIRED';
|
|
1207
|
+
this.title = 'Invalid token';
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
class HttpAccountDisabled extends HttpForbiddenError {
|
|
1212
|
+
constructor() {
|
|
1213
|
+
super('Access is denied. User account is disabled.');
|
|
1214
|
+
this.code = 'E_ACCOUNT_DISABLED';
|
|
1215
|
+
this.statusCode = 403.2;
|
|
1216
|
+
this.title = 'Disabled account';
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
class HttpBearerStrategy extends BearerStrategy {
|
|
1221
|
+
constructor() {
|
|
1222
|
+
super({
|
|
1223
|
+
passReqToCallback: true
|
|
1224
|
+
},
|
|
1225
|
+
/**
|
|
1226
|
+
* @param {Request} req
|
|
1227
|
+
* @param {string} token
|
|
1228
|
+
* @param {Function} done
|
|
1229
|
+
*/
|
|
1230
|
+
function (req, token, done) {
|
|
1231
|
+
/**
|
|
1232
|
+
* Gets OAuth2 client services
|
|
1233
|
+
* @type {import('./OAuth2ClientService').OAuth2ClientService}
|
|
1234
|
+
*/
|
|
1235
|
+
let client = req.context.getApplication().getStrategy(function OAuth2ClientService() {});
|
|
1236
|
+
// if client cannot be found
|
|
1237
|
+
if (client == null) {
|
|
1238
|
+
// throw configuration error
|
|
1239
|
+
return done(new Error('Invalid application configuration. OAuth2 client service cannot be found.'));
|
|
1240
|
+
}
|
|
1241
|
+
if (token == null) {
|
|
1242
|
+
// throw 499 Token Required error
|
|
1243
|
+
return done(new HttpBearerTokenRequired());
|
|
1244
|
+
}
|
|
1245
|
+
// get token info
|
|
1246
|
+
client.getTokenInfo(req.context, token).then((info) => {
|
|
1247
|
+
if (info == null) {
|
|
1248
|
+
// the specified token cannot be found - 498 invalid token with specific code
|
|
1249
|
+
return done(new HttpBearerTokenNotFound());
|
|
1250
|
+
}
|
|
1251
|
+
// if the given token is not active throw token expired - 498 invalid token with specific code
|
|
1252
|
+
if (!info.active) {
|
|
1253
|
+
return done(new HttpBearerTokenExpired());
|
|
1254
|
+
}
|
|
1255
|
+
// find user from token info
|
|
1256
|
+
return function () {
|
|
1257
|
+
/**
|
|
1258
|
+
* @type {import('./services/user-provisioning-mapper-service').UserProvisioningMapperService}
|
|
1259
|
+
*/
|
|
1260
|
+
const mapper = req.context.getApplication().getService(function UserProvisioningMapperService() {});
|
|
1261
|
+
if (mapper == null) {
|
|
1262
|
+
return req.context.model('User').where('name').equal(info.username).silent().getItem();
|
|
1263
|
+
}
|
|
1264
|
+
return mapper.getUser(req.context, info);
|
|
1265
|
+
}().then((user) => {
|
|
1266
|
+
// check if userProvisioning service is installed and try to find related user only if user not found
|
|
1267
|
+
if (user == null) {
|
|
1268
|
+
/**
|
|
1269
|
+
* @type {import('./services/user-provisioning-service').UserProvisioningService}
|
|
1270
|
+
*/
|
|
1271
|
+
const service = req.context.getApplication().getService(function UserProvisioningService() {});
|
|
1272
|
+
if (service == null) {
|
|
1273
|
+
return user;
|
|
1274
|
+
}
|
|
1275
|
+
return service.validateUser(req.context, info);
|
|
1276
|
+
}
|
|
1277
|
+
return user;
|
|
1278
|
+
}).then((user) => {
|
|
1279
|
+
// user cannot be found and of course cannot be authenticated (throw forbidden error)
|
|
1280
|
+
if (user == null) {
|
|
1281
|
+
// write access log for forbidden
|
|
1282
|
+
return done(new HttpForbiddenError());
|
|
1283
|
+
}
|
|
1284
|
+
// check if user has enabled attribute
|
|
1285
|
+
if (Object.prototype.hasOwnProperty.call(user, 'enabled') && !user.enabled) {
|
|
1286
|
+
//if user.enabled is off throw forbidden error
|
|
1287
|
+
return done(new HttpAccountDisabled('Access is denied. User account is disabled.'));
|
|
1288
|
+
}
|
|
1289
|
+
// otherwise return user data
|
|
1290
|
+
return done(null, {
|
|
1291
|
+
'name': user.name,
|
|
1292
|
+
'authenticationProviderKey': user.id,
|
|
1293
|
+
'authenticationType': 'Bearer',
|
|
1294
|
+
'authenticationToken': token,
|
|
1295
|
+
'authenticationScope': info.scope
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
}).catch((err) => {
|
|
1299
|
+
// end log token info request with error
|
|
1300
|
+
if (err && err.statusCode === 404) {
|
|
1301
|
+
// revert 404 not found returned by auth server to 498 invalid token
|
|
1302
|
+
return done(new HttpBearerTokenNotFound());
|
|
1303
|
+
}
|
|
1304
|
+
// otherwise continue with error
|
|
1305
|
+
return done(err);
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
class PassportService extends ApplicationService {
|
|
1312
|
+
constructor(app) {
|
|
1313
|
+
super(app);
|
|
1314
|
+
const authenticator = new passport.Authenticator();
|
|
1315
|
+
Object.defineProperty(this, 'authenticator', {
|
|
1316
|
+
configurable: true,
|
|
1317
|
+
enumerable: false,
|
|
1318
|
+
writable: false,
|
|
1319
|
+
value: authenticator
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* @returns {import('passport').Authenticator}
|
|
1325
|
+
*/
|
|
1326
|
+
getInstance() {
|
|
1327
|
+
return this.authenticator;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
export { AppRateLimitService, AppSpeedLimitService, DefaultScopeAccessConfiguration, EnableScopeAccessConfiguration, ExtendScopeAccessConfiguration, HttpAccountDisabled, HttpBearerStrategy, HttpBearerTokenExpired, HttpBearerTokenNotFound, HttpBearerTokenRequired, HttpRemoteAddrForbiddenError, OAuth2ClientService, PassportService, RateLimitService, RedisClientStore, RemoteAddressValidator, ScopeAccessConfiguration, ScopeString, SpeedLimitService, validateScope };
|
|
896
1333
|
//# sourceMappingURL=index.esm.js.map
|