@reldens/server-utils 0.27.0 → 0.28.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/lib/app-server-factory/cors-configurer.js +25 -10
- package/lib/app-server-factory/development-mode-detector.js +31 -5
- package/lib/app-server-factory/protocol-enforcer.js +4 -6
- package/lib/app-server-factory/rate-limit-configurer.js +5 -5
- package/lib/app-server-factory/security-configurer.js +23 -5
- package/lib/app-server-factory.js +40 -45
- package/package.json +1 -1
- package/app-server-factory-v2.patch +0 -29
|
@@ -17,7 +17,7 @@ class CorsConfigurer
|
|
|
17
17
|
this.corsMethods = ['GET','POST'];
|
|
18
18
|
this.corsHeaders = ['Content-Type','Authorization'];
|
|
19
19
|
this.developmentCorsOrigins = [];
|
|
20
|
-
this.developmentPorts = [
|
|
20
|
+
this.developmentPorts = [];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
setup(app, config)
|
|
@@ -40,12 +40,15 @@ class CorsConfigurer
|
|
|
40
40
|
allowedHeaders: this.corsHeaders,
|
|
41
41
|
credentials: true
|
|
42
42
|
};
|
|
43
|
+
if('*' === this.corsOrigin && true === corsOptions.credentials){
|
|
44
|
+
corsOptions.origin = true;
|
|
45
|
+
}
|
|
43
46
|
if(this.isDevelopmentMode && 0 < this.developmentCorsOrigins.length){
|
|
44
47
|
corsOptions.origin = (origin, callback) => {
|
|
45
48
|
if(!origin){
|
|
46
49
|
return callback(null, true);
|
|
47
50
|
}
|
|
48
|
-
if(-1 !== this.developmentCorsOrigins.indexOf(origin)){
|
|
51
|
+
if(-1 !== this.developmentCorsOrigins.indexOf(this.normalizeHost(origin))){
|
|
49
52
|
return callback(null, true);
|
|
50
53
|
}
|
|
51
54
|
if('*' === this.corsOrigin){
|
|
@@ -59,20 +62,32 @@ class CorsConfigurer
|
|
|
59
62
|
|
|
60
63
|
extractDevelopmentOrigins(domainMapping)
|
|
61
64
|
{
|
|
62
|
-
let
|
|
65
|
+
let originsSet = new Set();
|
|
63
66
|
let mappingKeys = Object.keys(domainMapping);
|
|
64
67
|
for(let domain of mappingKeys){
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
let normalizedDomain = this.normalizeHost(domain);
|
|
69
|
+
originsSet.add('http://'+normalizedDomain);
|
|
70
|
+
originsSet.add('https://'+normalizedDomain);
|
|
71
|
+
if(-1 !== normalizedDomain.indexOf(':')){
|
|
68
72
|
continue;
|
|
69
73
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
if(0 < this.developmentPorts.length){
|
|
75
|
+
for(let port of this.developmentPorts){
|
|
76
|
+
originsSet.add('http://'+normalizedDomain+':'+port);
|
|
77
|
+
originsSet.add('https://'+normalizedDomain+':'+port);
|
|
78
|
+
}
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
|
-
return
|
|
81
|
+
return Array.from(originsSet);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
normalizeHost(originOrHost)
|
|
85
|
+
{
|
|
86
|
+
let normalizedHost = (''+originOrHost).toLowerCase();
|
|
87
|
+
if('/' === normalizedHost.charAt(normalizedHost.length - 1)){
|
|
88
|
+
normalizedHost = normalizedHost.substring(0, normalizedHost.length - 1);
|
|
89
|
+
}
|
|
90
|
+
return normalizedHost;
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
}
|
|
@@ -26,6 +26,7 @@ class DevelopmentModeDetector
|
|
|
26
26
|
'staging.'
|
|
27
27
|
];
|
|
28
28
|
this.developmentEnvironments = ['development', 'dev', 'test'];
|
|
29
|
+
this.env = process?.env?.NODE_ENV || 'production';
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
detect(config = {})
|
|
@@ -36,8 +37,7 @@ class DevelopmentModeDetector
|
|
|
36
37
|
if(config.developmentEnvironments){
|
|
37
38
|
this.developmentEnvironments = config.developmentEnvironments;
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
if(this.developmentEnvironments.includes(env)){
|
|
40
|
+
if(this.developmentEnvironments.includes(this.env)){
|
|
41
41
|
return true;
|
|
42
42
|
}
|
|
43
43
|
if(config.developmentDomains && 0 < config.developmentDomains.length){
|
|
@@ -49,7 +49,7 @@ class DevelopmentModeDetector
|
|
|
49
49
|
}
|
|
50
50
|
if(config.domains && 0 < config.domains.length){
|
|
51
51
|
for(let domainConfig of config.domains){
|
|
52
|
-
if(!domainConfig
|
|
52
|
+
if(!domainConfig?.hostname){
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
55
|
if(this.matchesPattern(domainConfig.hostname)){
|
|
@@ -62,8 +62,34 @@ class DevelopmentModeDetector
|
|
|
62
62
|
|
|
63
63
|
matchesPattern(domain)
|
|
64
64
|
{
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
if(!domain){
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
let d = (''+domain).toLowerCase();
|
|
69
|
+
if('localhost' === d){
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if('127.0.0.1' === d){
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if('::1' === d){
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
for(let pattern of this.developmentPatterns || []){
|
|
79
|
+
if(!pattern){
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let p = (''+pattern).toLowerCase();
|
|
83
|
+
if('.' === p.charAt(0) && d.endsWith(p)){
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if('.' === p.charAt(p.length - 1) && 0 === d.indexOf(p)){
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if(d === p){
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if(0 <= d.indexOf(p)){
|
|
67
93
|
return true;
|
|
68
94
|
}
|
|
69
95
|
}
|
|
@@ -20,8 +20,8 @@ class ProtocolEnforcer
|
|
|
20
20
|
this.useHttps = config.useHttps || false;
|
|
21
21
|
this.enforceProtocol = config.enforceProtocol !== false;
|
|
22
22
|
app.use((req, res, next) => {
|
|
23
|
-
let protocol = req.get('X-Forwarded-Proto') || req.protocol;
|
|
24
|
-
let host = req.get('host');
|
|
23
|
+
let protocol = (req.get('X-Forwarded-Proto') || req.protocol || '').toLowerCase();
|
|
24
|
+
let host = (req.get('host') || '').toLowerCase().trim();
|
|
25
25
|
if(this.isDevelopmentMode){
|
|
26
26
|
res.removeHeader('Origin-Agent-Cluster');
|
|
27
27
|
res.removeHeader('Strict-Transport-Security');
|
|
@@ -29,12 +29,10 @@ class ProtocolEnforcer
|
|
|
29
29
|
res.set('Origin-Agent-Cluster', '?0');
|
|
30
30
|
if(this.enforceProtocol && host){
|
|
31
31
|
if(!this.useHttps && 'https' === protocol){
|
|
32
|
-
|
|
33
|
-
return res.redirect(301, redirectUrl);
|
|
32
|
+
return res.redirect(301, 'http://'+host+req.url);
|
|
34
33
|
}
|
|
35
34
|
if(this.useHttps && 'http' === protocol){
|
|
36
|
-
|
|
37
|
-
return res.redirect(301, redirectUrl);
|
|
35
|
+
return res.redirect(301, 'https://'+host+req.url);
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -25,9 +25,9 @@ class RateLimitConfigurer
|
|
|
25
25
|
{
|
|
26
26
|
this.isDevelopmentMode = config.isDevelopmentMode || false;
|
|
27
27
|
this.globalRateLimit = config.globalRateLimit || 0;
|
|
28
|
-
this.windowMs = config.windowMs || this.windowMs;
|
|
29
|
-
this.maxRequests = config.maxRequests || this.maxRequests;
|
|
30
|
-
this.developmentMultiplier = config.developmentMultiplier || this.developmentMultiplier;
|
|
28
|
+
this.windowMs = Number(config.windowMs || this.windowMs);
|
|
29
|
+
this.maxRequests = Number(config.maxRequests || this.maxRequests);
|
|
30
|
+
this.developmentMultiplier = Number(config.developmentMultiplier || this.developmentMultiplier);
|
|
31
31
|
this.applyKeyGenerator = config.applyKeyGenerator || false;
|
|
32
32
|
this.tooManyRequestsMessage = config.tooManyRequestsMessage || this.tooManyRequestsMessage;
|
|
33
33
|
if(!this.globalRateLimit){
|
|
@@ -36,7 +36,7 @@ class RateLimitConfigurer
|
|
|
36
36
|
let limiterParams = {
|
|
37
37
|
windowMs: this.windowMs,
|
|
38
38
|
limit: this.maxRequests,
|
|
39
|
-
standardHeaders:
|
|
39
|
+
standardHeaders: 'draft-8',
|
|
40
40
|
legacyHeaders: false,
|
|
41
41
|
message: this.tooManyRequestsMessage
|
|
42
42
|
};
|
|
@@ -56,7 +56,7 @@ class RateLimitConfigurer
|
|
|
56
56
|
let limiterParams = {
|
|
57
57
|
windowMs: this.windowMs,
|
|
58
58
|
limit: this.maxRequests,
|
|
59
|
-
standardHeaders:
|
|
59
|
+
standardHeaders: 'draft-8',
|
|
60
60
|
legacyHeaders: false
|
|
61
61
|
};
|
|
62
62
|
if(this.isDevelopmentMode){
|
|
@@ -63,7 +63,10 @@ class SecurityConfigurer
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
if(config.developmentExternalDomains){
|
|
66
|
-
this.addExternalDomainsToCsp(
|
|
66
|
+
this.addExternalDomainsToCsp(
|
|
67
|
+
helmetOptions.contentSecurityPolicy.directives,
|
|
68
|
+
config.developmentExternalDomains
|
|
69
|
+
);
|
|
67
70
|
}
|
|
68
71
|
return helmetOptions;
|
|
69
72
|
}
|
|
@@ -99,7 +102,7 @@ class SecurityConfigurer
|
|
|
99
102
|
if(!req.body){
|
|
100
103
|
return next();
|
|
101
104
|
}
|
|
102
|
-
if('object' === typeof req.body){
|
|
105
|
+
if('object' === typeof req.body && null !== req.body){
|
|
103
106
|
this.sanitizeRequestBody(req.body);
|
|
104
107
|
}
|
|
105
108
|
next();
|
|
@@ -108,9 +111,24 @@ class SecurityConfigurer
|
|
|
108
111
|
|
|
109
112
|
sanitizeRequestBody(body)
|
|
110
113
|
{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
if(Array.isArray(body)){
|
|
115
|
+
for(let i = 0; i < body.length; i++){
|
|
116
|
+
if('string' === typeof body[i]){
|
|
117
|
+
body[i] = sanitizeHtml(body[i], this.sanitizeOptions);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if('object' === typeof body[i] && null !== body[i]){
|
|
121
|
+
this.sanitizeRequestBody(body[i]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if('object' !== typeof body || null === body){
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let keys = Object.keys(body);
|
|
130
|
+
for(let i = 0; i < keys.length; i++){
|
|
131
|
+
let key = keys[i];
|
|
114
132
|
if('string' === typeof body[key]){
|
|
115
133
|
body[key] = sanitizeHtml(body[key], this.sanitizeOptions);
|
|
116
134
|
continue;
|
|
@@ -48,8 +48,8 @@ class AppServerFactory
|
|
|
48
48
|
this.useXssProtection = true;
|
|
49
49
|
this.globalRateLimit = 0;
|
|
50
50
|
this.corsOrigin = '*';
|
|
51
|
-
this.corsMethods = [
|
|
52
|
-
this.corsHeaders = [
|
|
51
|
+
this.corsMethods = [];
|
|
52
|
+
this.corsHeaders = [];
|
|
53
53
|
this.tooManyRequestsMessage = 'Too many requests, please try again later.';
|
|
54
54
|
this.error = {};
|
|
55
55
|
this.processErrorResponse = false;
|
|
@@ -74,16 +74,9 @@ class AppServerFactory
|
|
|
74
74
|
this.developmentDomains = [];
|
|
75
75
|
this.domainMapping = {};
|
|
76
76
|
this.enforceProtocol = true;
|
|
77
|
-
this.developmentPatterns = [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'.local',
|
|
81
|
-
'.test',
|
|
82
|
-
'.dev',
|
|
83
|
-
'.staging'
|
|
84
|
-
];
|
|
85
|
-
this.developmentEnvironments = ['development', 'dev', 'test'];
|
|
86
|
-
this.developmentPorts = [3000, 8080, 8081];
|
|
77
|
+
this.developmentPatterns = [];
|
|
78
|
+
this.developmentEnvironments = [];
|
|
79
|
+
this.developmentPorts = [];
|
|
87
80
|
this.developmentMultiplier = 10;
|
|
88
81
|
this.developmentExternalDomains = {};
|
|
89
82
|
this.developmentModeDetector = new DevelopmentModeDetector();
|
|
@@ -133,10 +126,10 @@ class AppServerFactory
|
|
|
133
126
|
|
|
134
127
|
extractDomainFromHttpUrl(url)
|
|
135
128
|
{
|
|
136
|
-
if(!url ||
|
|
129
|
+
if(!url || !url.startsWith('http://')){
|
|
137
130
|
return false;
|
|
138
131
|
}
|
|
139
|
-
return url.replace(/^
|
|
132
|
+
return url.replace(/^http:\/\//, '').split(':')[0];
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
addHttpDomainsAsDevelopment()
|
|
@@ -153,12 +146,17 @@ class AppServerFactory
|
|
|
153
146
|
|
|
154
147
|
detectDevelopmentMode()
|
|
155
148
|
{
|
|
156
|
-
|
|
157
|
-
developmentPatterns: this.developmentPatterns,
|
|
158
|
-
developmentEnvironments: this.developmentEnvironments,
|
|
149
|
+
let detectConfig = {
|
|
159
150
|
developmentDomains: this.developmentDomains,
|
|
160
151
|
domains: this.domains
|
|
161
|
-
}
|
|
152
|
+
};
|
|
153
|
+
if(0 < this.developmentPatterns.length){
|
|
154
|
+
detectConfig['developmentPatterns'] = this.developmentPatterns;
|
|
155
|
+
}
|
|
156
|
+
if(0 < this.developmentEnvironments.length){
|
|
157
|
+
detectConfig['developmentEnvironments'] = this.developmentEnvironments;
|
|
158
|
+
}
|
|
159
|
+
this.isDevelopmentMode = this.developmentModeDetector.detect(detectConfig);
|
|
162
160
|
}
|
|
163
161
|
|
|
164
162
|
setupDevelopmentConfiguration()
|
|
@@ -208,15 +206,28 @@ class AppServerFactory
|
|
|
208
206
|
|
|
209
207
|
setupCors()
|
|
210
208
|
{
|
|
211
|
-
|
|
209
|
+
let corsConfig = {
|
|
212
210
|
isDevelopmentMode: this.isDevelopmentMode,
|
|
213
211
|
useCors: this.useCors,
|
|
214
|
-
corsOrigin: this.corsOrigin
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
212
|
+
corsOrigin: this.corsOrigin
|
|
213
|
+
};
|
|
214
|
+
if(0 < this.corsMethods.length){
|
|
215
|
+
corsConfig['corsMethods'] = this.corsMethods;
|
|
216
|
+
}
|
|
217
|
+
if(0 < this.corsHeaders.length){
|
|
218
|
+
corsConfig['corsHeaders'] = this.corsHeaders;
|
|
219
|
+
}
|
|
220
|
+
if(0 < this.developmentPorts.length){
|
|
221
|
+
corsConfig['developmentPorts'] = this.developmentPorts;
|
|
222
|
+
}
|
|
223
|
+
if(
|
|
224
|
+
'object' === typeof this.domainMapping
|
|
225
|
+
&& null !== this.domainMapping
|
|
226
|
+
&& 0 < Object.keys(this.domainMapping)
|
|
227
|
+
){
|
|
228
|
+
corsConfig['domainMapping'] = this.domainMapping;
|
|
229
|
+
}
|
|
230
|
+
this.corsConfigurer.setup(this.app, corsConfig);
|
|
220
231
|
}
|
|
221
232
|
|
|
222
233
|
setupRateLimiting()
|
|
@@ -255,7 +266,7 @@ class AppServerFactory
|
|
|
255
266
|
setupTrustedProxy()
|
|
256
267
|
{
|
|
257
268
|
if('' !== this.trustedProxy){
|
|
258
|
-
this.app.
|
|
269
|
+
this.app.set('trust proxy', this.trustedProxy);
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
|
|
@@ -308,12 +319,13 @@ class AppServerFactory
|
|
|
308
319
|
return false;
|
|
309
320
|
}
|
|
310
321
|
let cleanHostname = hostname.toLowerCase().trim();
|
|
322
|
+
let hostWithoutPort = cleanHostname.split(':')[0];
|
|
311
323
|
for(let i = 0; i < this.domains.length; i++){
|
|
312
324
|
let domain = this.domains[i];
|
|
313
|
-
if(domain.hostname ===
|
|
325
|
+
if(domain.hostname === hostWithoutPort){
|
|
314
326
|
return domain;
|
|
315
327
|
}
|
|
316
|
-
if(domain.aliases && domain.aliases.includes(
|
|
328
|
+
if(domain.aliases && domain.aliases.includes(hostWithoutPort)){
|
|
317
329
|
return domain;
|
|
318
330
|
}
|
|
319
331
|
}
|
|
@@ -499,23 +511,6 @@ class AppServerFactory
|
|
|
499
511
|
return this.securityConfigurer.enableCSP(this.app, cspOptions);
|
|
500
512
|
}
|
|
501
513
|
|
|
502
|
-
validateInput(input, type)
|
|
503
|
-
{
|
|
504
|
-
if('string' !== typeof input){
|
|
505
|
-
return false;
|
|
506
|
-
}
|
|
507
|
-
let patterns = {
|
|
508
|
-
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
509
|
-
username: /^[a-zA-Z0-9_-]{3,30}$/,
|
|
510
|
-
strongPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
|
511
|
-
alphanumeric: /^[a-zA-Z0-9]+$/,
|
|
512
|
-
numeric: /^\d+$/,
|
|
513
|
-
hexColor: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
|
514
|
-
ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
515
|
-
};
|
|
516
|
-
return patterns[type] ? patterns[type].test(input) : false;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
514
|
}
|
|
520
515
|
|
|
521
516
|
module.exports.AppServerFactory = AppServerFactory;
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
--- a/lib/app-server-factory.js
|
|
2
|
-
+++ b/lib/app-server-factory.js
|
|
3
|
-
@@ -141,10 +141,14 @@ class AppServerFactory
|
|
4
|
-
|
|
5
|
-
addHttpDomainsAsDevelopment()
|
|
6
|
-
{
|
|
7
|
-
+ console.log('ENV VARS:', process.env.RELDENS_APP_HOST, process.env.RELDENS_PUBLIC_URL);
|
|
8
|
-
let hostDomain = this.extractDomainFromHttpUrl(process.env.RELDENS_APP_HOST);
|
|
9
|
-
let publicDomain = this.extractDomainFromHttpUrl(process.env.RELDENS_PUBLIC_URL);
|
|
10
|
-
+ console.log('EXTRACTED DOMAINS:', hostDomain, publicDomain);
|
|
11
|
-
if(hostDomain && !this.developmentDomains.includes(hostDomain)){
|
|
12
|
-
this.developmentDomains.push(hostDomain);
|
|
13
|
-
+ console.log('ADDED HOST DOMAIN:', hostDomain);
|
|
14
|
-
}
|
|
15
|
-
if(publicDomain && !this.developmentDomains.includes(publicDomain)){
|
|
16
|
-
this.developmentDomains.push(publicDomain);
|
|
17
|
-
+ console.log('ADDED PUBLIC DOMAIN:', publicDomain);
|
|
18
|
-
}
|
|
19
|
-
+ console.log('FINAL DEVELOPMENT DOMAINS:', this.developmentDomains);
|
|
20
|
-
}
|
|
21
|
-
@@ -154,6 +158,7 @@ class AppServerFactory
|
|
22
|
-
this.isDevelopmentMode = this.developmentModeDetector.detect({
|
|
23
|
-
developmentPatterns: this.developmentPatterns,
|
|
24
|
-
developmentEnvironments: this.developmentEnvironments,
|
|
25
|
-
developmentDomains: this.developmentDomains,
|
|
26
|
-
domains: this.domains
|
|
27
|
-
});
|
|
28
|
-
+ console.log('DEVELOPMENT MODE DETECTED:', this.isDevelopmentMode);
|
|
29
|
-
}
|