@reldens/server-utils 0.26.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 +51 -28
- package/lib/app-server-factory.js +516 -499
- package/package.json +10 -10
|
@@ -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){
|
|
@@ -27,40 +27,48 @@ class SecurityConfigurer
|
|
|
27
27
|
if(!this.useHelmet){
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
let helmetOptions = {
|
|
30
|
+
let helmetOptions = this.setModeOptions({
|
|
31
31
|
crossOriginEmbedderPolicy: false,
|
|
32
32
|
crossOriginOpenerPolicy: false,
|
|
33
33
|
crossOriginResourcePolicy: false,
|
|
34
34
|
originAgentCluster: false
|
|
35
|
-
};
|
|
35
|
+
}, config);
|
|
36
|
+
if(this.helmetConfig){
|
|
37
|
+
Object.assign(helmetOptions, this.helmetConfig);
|
|
38
|
+
}
|
|
39
|
+
app.use(helmet(helmetOptions));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setModeOptions(helmetOptions, config)
|
|
43
|
+
{
|
|
36
44
|
if(this.isDevelopmentMode){
|
|
37
45
|
helmetOptions.contentSecurityPolicy = false;
|
|
38
46
|
helmetOptions.hsts = false;
|
|
39
47
|
helmetOptions.noSniff = false;
|
|
40
|
-
|
|
41
|
-
helmetOptions.contentSecurityPolicy = {
|
|
42
|
-
directives: {
|
|
43
|
-
defaultSrc: ["'self'"],
|
|
44
|
-
scriptSrc: ["'self'"],
|
|
45
|
-
scriptSrcElem: ["'self'"],
|
|
46
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
47
|
-
styleSrcElem: ["'self'", "'unsafe-inline'"],
|
|
48
|
-
imgSrc: ["'self'", "data:", "https:"],
|
|
49
|
-
fontSrc: ["'self'"],
|
|
50
|
-
connectSrc: ["'self'"],
|
|
51
|
-
frameAncestors: ["'none'"],
|
|
52
|
-
baseUri: ["'self'"],
|
|
53
|
-
formAction: ["'self'"]
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
if(config.developmentExternalDomains){
|
|
57
|
-
this.addExternalDomainsToCsp(helmetOptions.contentSecurityPolicy.directives, config.developmentExternalDomains);
|
|
58
|
-
}
|
|
48
|
+
return helmetOptions;
|
|
59
49
|
}
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
helmetOptions.contentSecurityPolicy = {
|
|
51
|
+
directives: {
|
|
52
|
+
defaultSrc: ["'self'"],
|
|
53
|
+
scriptSrc: ["'self'"],
|
|
54
|
+
scriptSrcElem: ["'self'"],
|
|
55
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
56
|
+
styleSrcElem: ["'self'", "'unsafe-inline'"],
|
|
57
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
58
|
+
fontSrc: ["'self'"],
|
|
59
|
+
connectSrc: ["'self'"],
|
|
60
|
+
frameAncestors: ["'none'"],
|
|
61
|
+
baseUri: ["'self'"],
|
|
62
|
+
formAction: ["'self'"]
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
if(config.developmentExternalDomains){
|
|
66
|
+
this.addExternalDomainsToCsp(
|
|
67
|
+
helmetOptions.contentSecurityPolicy.directives,
|
|
68
|
+
config.developmentExternalDomains
|
|
69
|
+
);
|
|
62
70
|
}
|
|
63
|
-
|
|
71
|
+
return helmetOptions;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
addExternalDomainsToCsp(directives, externalDomains)
|
|
@@ -94,7 +102,7 @@ class SecurityConfigurer
|
|
|
94
102
|
if(!req.body){
|
|
95
103
|
return next();
|
|
96
104
|
}
|
|
97
|
-
if('object' === typeof req.body){
|
|
105
|
+
if('object' === typeof req.body && null !== req.body){
|
|
98
106
|
this.sanitizeRequestBody(req.body);
|
|
99
107
|
}
|
|
100
108
|
next();
|
|
@@ -103,9 +111,24 @@ class SecurityConfigurer
|
|
|
103
111
|
|
|
104
112
|
sanitizeRequestBody(body)
|
|
105
113
|
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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];
|
|
109
132
|
if('string' === typeof body[key]){
|
|
110
133
|
body[key] = sanitizeHtml(body[key], this.sanitizeOptions);
|
|
111
134
|
continue;
|