@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.
@@ -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){
@@ -63,7 +63,10 @@ class SecurityConfigurer
63
63
  }
64
64
  };
65
65
  if(config.developmentExternalDomains){
66
- this.addExternalDomainsToCsp(helmetOptions.contentSecurityPolicy.directives, config.developmentExternalDomains);
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
- let bodyKeys = Object.keys(body);
112
- for(let i = 0; i < bodyKeys.length; i++){
113
- 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];
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 = ['GET','POST'];
52
- this.corsHeaders = ['Content-Type','Authorization'];
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
- 'localhost',
79
- '127.0.0.1',
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 || (!url.startsWith('http://') && !url.startsWith('https://'))){
129
+ if(!url || !url.startsWith('http://')){
137
130
  return false;
138
131
  }
139
- return url.replace(/^https?:\/\//, '').split(':')[0];
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
- this.isDevelopmentMode = this.developmentModeDetector.detect({
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
- this.corsConfigurer.setup(this.app, {
209
+ let corsConfig = {
212
210
  isDevelopmentMode: this.isDevelopmentMode,
213
211
  useCors: this.useCors,
214
- corsOrigin: this.corsOrigin,
215
- corsMethods: this.corsMethods,
216
- corsHeaders: this.corsHeaders,
217
- domainMapping: this.domainMapping,
218
- developmentPorts: this.developmentPorts
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.enable('trust proxy', this.trustedProxy);
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 === cleanHostname){
325
+ if(domain.hostname === hostWithoutPort){
314
326
  return domain;
315
327
  }
316
- if(domain.aliases && domain.aliases.includes(cleanHostname)){
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/server-utils",
3
3
  "scope": "@reldens",
4
- "version": "0.27.0",
4
+ "version": "0.28.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -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
- }