@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.
@@ -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 = [3000, 8080, 8081];
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 origins = [];
65
+ let originsSet = new Set();
63
66
  let mappingKeys = Object.keys(domainMapping);
64
67
  for(let domain of mappingKeys){
65
- origins.push('http://'+domain);
66
- origins.push('https://'+domain);
67
- if(domain.includes(':')){
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
- for(let port of this.developmentPorts){
71
- origins.push('http://'+domain+':'+port);
72
- origins.push('https://'+domain+':'+port);
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 origins;
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
- let env = process.env.NODE_ENV;
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.hostname){
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
- for(let pattern of this.developmentPatterns){
66
- if(domain.includes(pattern)){
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
- let redirectUrl = 'http://'+host+req.url;
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
- let redirectUrl = 'https://'+host+req.url;
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: true,
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: true,
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
- } else {
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
- if(this.helmetConfig){
61
- Object.assign(helmetOptions, this.helmetConfig);
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
- app.use(helmet(helmetOptions));
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
- let bodyKeys = Object.keys(body);
107
- for(let i = 0; i < bodyKeys.length; i++){
108
- let key = bodyKeys[i];
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;