@reldens/server-utils 0.42.0 → 0.44.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.
@@ -6,6 +6,7 @@
6
6
 
7
7
  const helmet = require('helmet');
8
8
  const sanitizeHtml = require('sanitize-html');
9
+ const { EventDispatcher } = require('../event-dispatcher');
9
10
 
10
11
  class SecurityConfigurer
11
12
  {
@@ -18,15 +19,24 @@ class SecurityConfigurer
18
19
  this.helmetConfig = false;
19
20
  this.helmetOptions = {};
20
21
  this.sanitizeOptions = {allowedTags: [], allowedAttributes: {}};
22
+ this.onEvent = null;
21
23
  }
22
24
 
23
25
  setupHelmet(app, config)
24
26
  {
25
27
  this.useHelmet = config.useHelmet !== false;
28
+ this.onEvent = config.onEvent || null;
26
29
  if(!this.useHelmet){
27
30
  return;
28
31
  }
29
32
  app.use(helmet(this.mapHelmetOptions(config)));
33
+ EventDispatcher.dispatch(
34
+ this.onEvent,
35
+ 'helmet-configured',
36
+ 'securityConfigurer',
37
+ this,
38
+ {useHelmet: this.useHelmet, isDevelopmentMode: this.isDevelopmentMode}
39
+ );
30
40
  }
31
41
 
32
42
  mapHelmetOptions(config)
@@ -131,6 +141,7 @@ class SecurityConfigurer
131
141
  {
132
142
  this.useXssProtection = config.useXssProtection !== false;
133
143
  this.sanitizeOptions = config.sanitizeOptions || this.sanitizeOptions;
144
+ this.onEvent = config.onEvent || null;
134
145
  if(!this.useXssProtection){
135
146
  return;
136
147
  }
@@ -143,6 +154,13 @@ class SecurityConfigurer
143
154
  }
144
155
  next();
145
156
  });
157
+ EventDispatcher.dispatch(
158
+ this.onEvent,
159
+ 'xss-protection-enabled',
160
+ 'securityConfigurer',
161
+ this,
162
+ {useXssProtection: this.useXssProtection}
163
+ );
146
164
  }
147
165
 
148
166
  sanitizeRequestBody(body)
@@ -9,6 +9,9 @@ const { Http2CdnServer } = require('./http2-cdn-server');
9
9
  const { ServerDefaultConfigurations } = require('./server-default-configurations');
10
10
  const { ServerFactoryUtils } = require('./server-factory-utils');
11
11
  const { ServerHeaders } = require('./server-headers');
12
+ const { ServerErrorHandler } = require('./server-error-handler');
13
+ const { RequestLogger } = require('./request-logger');
14
+ const { EventDispatcher } = require('./event-dispatcher');
12
15
  const { DevelopmentModeDetector } = require('./app-server-factory/development-mode-detector');
13
16
  const { ProtocolEnforcer } = require('./app-server-factory/protocol-enforcer');
14
17
  const { SecurityConfigurer } = require('./app-server-factory/security-configurer');
@@ -115,6 +118,10 @@ class AppServerFactory
115
118
  this.http2CdnServer = false;
116
119
  this.reverseProxyEnabled = false;
117
120
  this.reverseProxyRules = [];
121
+ this.onError = null;
122
+ this.onRequestSuccess = null;
123
+ this.onRequestError = null;
124
+ this.onEvent = null;
118
125
  }
119
126
 
120
127
  buildStaticHeaders(res, path)
@@ -146,6 +153,7 @@ class AppServerFactory
146
153
  this.setupCors();
147
154
  this.setupRateLimiting();
148
155
  this.setupRequestParsing();
156
+ this.setupRequestLogging();
149
157
  this.setupTrustedProxy();
150
158
  try {
151
159
  this.appServer = this.createServer();
@@ -159,6 +167,13 @@ class AppServerFactory
159
167
  }
160
168
  return false;
161
169
  }
170
+ EventDispatcher.dispatch(
171
+ this.onEvent,
172
+ 'app-server-created',
173
+ 'appServerFactory',
174
+ this,
175
+ {port: this.port, useHttps: this.useHttps, isDevelopmentMode: this.isDevelopmentMode}
176
+ );
162
177
  if(this.http2CdnEnabled){
163
178
  if(!this.createHttp2CdnServer()){
164
179
  this.error = {message: 'The createHttp2CdnServer() returned false.'};
@@ -188,6 +203,10 @@ class AppServerFactory
188
203
  }
189
204
  this.http2CdnServer.corsOrigins = this.http2CdnCorsOrigins;
190
205
  this.http2CdnServer.corsAllowAll = this.http2CdnCorsAllowAll;
206
+ this.http2CdnServer.onError = this.onError;
207
+ this.http2CdnServer.onRequestSuccess = this.onRequestSuccess;
208
+ this.http2CdnServer.onRequestError = this.onRequestError;
209
+ this.http2CdnServer.onEvent = this.onEvent;
191
210
  if(!this.http2CdnServer.create()){
192
211
  this.error = this.http2CdnServer.error;
193
212
  return false;
@@ -196,6 +215,13 @@ class AppServerFactory
196
215
  this.error = this.http2CdnServer.error;
197
216
  return false;
198
217
  }
218
+ EventDispatcher.dispatch(
219
+ this.onEvent,
220
+ 'http2-cdn-created',
221
+ 'appServerFactory',
222
+ this,
223
+ {port: this.http2CdnPort}
224
+ );
199
225
  return true;
200
226
  }
201
227
 
@@ -223,7 +249,8 @@ class AppServerFactory
223
249
  {
224
250
  let detectConfig = {
225
251
  developmentDomains: this.developmentDomains,
226
- domains: this.domains
252
+ domains: this.domains,
253
+ onEvent: this.onEvent
227
254
  };
228
255
  if(0 < this.developmentPatterns.length){
229
256
  detectConfig['developmentPatterns'] = this.developmentPatterns;
@@ -257,7 +284,8 @@ class AppServerFactory
257
284
  this.protocolEnforcer.setup(this.app, {
258
285
  isDevelopmentMode: this.isDevelopmentMode,
259
286
  useHttps: this.useHttps,
260
- enforceProtocol: this.enforceProtocol
287
+ enforceProtocol: this.enforceProtocol,
288
+ onEvent: this.onEvent
261
289
  });
262
290
  }
263
291
 
@@ -267,11 +295,13 @@ class AppServerFactory
267
295
  isDevelopmentMode: this.isDevelopmentMode,
268
296
  useHelmet: this.useHelmet,
269
297
  helmetConfig: this.helmetConfig,
270
- developmentExternalDomains: this.developmentExternalDomains
298
+ developmentExternalDomains: this.developmentExternalDomains,
299
+ onEvent: this.onEvent
271
300
  });
272
301
  this.securityConfigurer.setupXssProtection(this.app, {
273
302
  useXssProtection: this.useXssProtection,
274
- sanitizeOptions: this.sanitizeOptions
303
+ sanitizeOptions: this.sanitizeOptions,
304
+ onEvent: this.onEvent
275
305
  });
276
306
  }
277
307
 
@@ -288,7 +318,8 @@ class AppServerFactory
288
318
  let corsConfig = {
289
319
  isDevelopmentMode: this.isDevelopmentMode,
290
320
  useCors: this.useCors,
291
- corsOrigin: this.corsOrigin
321
+ corsOrigin: this.corsOrigin,
322
+ onEvent: this.onEvent
292
323
  };
293
324
  if(0 < this.corsMethods.length){
294
325
  corsConfig['corsMethods'] = this.corsMethods;
@@ -318,7 +349,8 @@ class AppServerFactory
318
349
  maxRequests: this.maxRequests,
319
350
  developmentMultiplier: this.developmentMultiplier,
320
351
  applyKeyGenerator: this.applyKeyGenerator,
321
- tooManyRequestsMessage: this.tooManyRequestsMessage
352
+ tooManyRequestsMessage: this.tooManyRequestsMessage,
353
+ onEvent: this.onEvent
322
354
  });
323
355
  }
324
356
 
@@ -342,6 +374,14 @@ class AppServerFactory
342
374
  }
343
375
  }
344
376
 
377
+ setupRequestLogging()
378
+ {
379
+ if(!this.onRequestSuccess && !this.onRequestError){
380
+ return;
381
+ }
382
+ this.app.use(RequestLogger.createMiddleware(this.onRequestSuccess, this.onRequestError));
383
+ }
384
+
345
385
  setupReverseProxy()
346
386
  {
347
387
  if(!this.reverseProxyEnabled || 0 === this.reverseProxyRules.length){
@@ -350,7 +390,9 @@ class AppServerFactory
350
390
  this.reverseProxyConfigurer.setup(this.app, {
351
391
  reverseProxyRules: this.reverseProxyRules,
352
392
  isDevelopmentMode: this.isDevelopmentMode,
353
- useVirtualHosts: this.useVirtualHosts
393
+ useVirtualHosts: this.useVirtualHosts,
394
+ onError: this.onError,
395
+ onEvent: this.onEvent
354
396
  });
355
397
  }
356
398
 
@@ -388,6 +430,14 @@ class AppServerFactory
388
430
  return next();
389
431
  }
390
432
  this.error = {message: 'No hostname provided and no default domain configured'};
433
+ ServerErrorHandler.handleError(
434
+ this.onError,
435
+ 'appServerFactory',
436
+ this,
437
+ 'virtual-host-no-hostname',
438
+ this.error,
439
+ {request: req, response: res}
440
+ );
391
441
  return res.status(400).send('Bad Request');
392
442
  }
393
443
  let domain = this.findDomainConfig(hostname);
@@ -397,6 +447,14 @@ class AppServerFactory
397
447
  return next();
398
448
  }
399
449
  this.error = {message: 'Unknown domain: '+hostname};
450
+ ServerErrorHandler.handleError(
451
+ this.onError,
452
+ 'appServerFactory',
453
+ this,
454
+ 'virtual-host-unknown-domain',
455
+ this.error,
456
+ {hostname: hostname, request: req, response: res}
457
+ );
400
458
  return res.status(404).send('Domain not found');
401
459
  }
402
460
  req.domain = domain;
@@ -426,7 +484,15 @@ class AppServerFactory
426
484
  createServer()
427
485
  {
428
486
  if(!this.useHttps){
429
- return http.createServer(this.app);
487
+ let httpServer = http.createServer(this.app);
488
+ EventDispatcher.dispatch(
489
+ this.onEvent,
490
+ 'http-server-created',
491
+ 'appServerFactory',
492
+ this,
493
+ {port: this.port}
494
+ );
495
+ return httpServer;
430
496
  }
431
497
  if(this.useVirtualHosts && 0 < this.domains.length){
432
498
  return this.createHttpsServerWithSNI();
@@ -453,7 +519,15 @@ class AppServerFactory
453
519
  credentials.ca = ca;
454
520
  }
455
521
  }
456
- return https.createServer(credentials, this.app);
522
+ let httpsServer = https.createServer(credentials, this.app);
523
+ EventDispatcher.dispatch(
524
+ this.onEvent,
525
+ 'https-server-created',
526
+ 'appServerFactory',
527
+ this,
528
+ {port: this.port}
529
+ );
530
+ return httpsServer;
457
531
  }
458
532
 
459
533
  createHttpsServerWithSNI()
@@ -471,17 +545,41 @@ class AppServerFactory
471
545
  let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
472
546
  if(!key){
473
547
  this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
548
+ ServerErrorHandler.handleError(
549
+ this.onError,
550
+ 'appServerFactory',
551
+ this,
552
+ 'sni-key-read-failure',
553
+ this.error,
554
+ {hostname: hostname, domain: domain, keyPath: domain.keyPath}
555
+ );
474
556
  return callback(null, null);
475
557
  }
476
558
  let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
477
559
  if(!cert){
478
560
  this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
561
+ ServerErrorHandler.handleError(
562
+ this.onError,
563
+ 'appServerFactory',
564
+ this,
565
+ 'sni-cert-read-failure',
566
+ this.error,
567
+ {hostname: hostname, domain: domain, certPath: domain.certPath}
568
+ );
479
569
  return callback(null, null);
480
570
  }
481
571
  let ctx = tls.createSecureContext({key, cert});
482
572
  callback(null, ctx);
483
573
  };
484
- return https.createServer(httpsOptions, this.app);
574
+ let sniServer = https.createServer(httpsOptions, this.app);
575
+ EventDispatcher.dispatch(
576
+ this.onEvent,
577
+ 'sni-server-created',
578
+ 'appServerFactory',
579
+ this,
580
+ {port: this.port, domainsCount: this.domains.length}
581
+ );
582
+ return sniServer;
485
583
  }
486
584
 
487
585
  loadDefaultCredentials()
@@ -507,6 +605,13 @@ class AppServerFactory
507
605
  return false;
508
606
  }
509
607
  this.appServer.listen(listenPort);
608
+ EventDispatcher.dispatch(
609
+ this.onEvent,
610
+ 'app-server-listening',
611
+ 'appServerFactory',
612
+ this,
613
+ {port: listenPort}
614
+ );
510
615
  return true;
511
616
  }
512
617
 
@@ -568,6 +673,13 @@ class AppServerFactory
568
673
  return false;
569
674
  }
570
675
  this.domains.push(domainConfig);
676
+ EventDispatcher.dispatch(
677
+ this.onEvent,
678
+ 'domain-added',
679
+ 'appServerFactory',
680
+ this,
681
+ {hostname: domainConfig.hostname}
682
+ );
571
683
  return true;
572
684
  }
573
685
 
@@ -0,0 +1,130 @@
1
+ /**
2
+ *
3
+ * Reldens - CdnRequestHandler
4
+ *
5
+ */
6
+
7
+ const { FileHandler } = require('./file-handler');
8
+ const { ServerFactoryUtils } = require('./server-factory-utils');
9
+ const { ServerErrorHandler } = require('./server-error-handler');
10
+
11
+ class CdnRequestHandler
12
+ {
13
+
14
+ constructor(cdnServer)
15
+ {
16
+ this.cdnServer = cdnServer;
17
+ }
18
+
19
+ handleRequest(requestContext)
20
+ {
21
+ let startTime = Date.now();
22
+ let requestData = {
23
+ serverType: 'cdn',
24
+ method: requestContext.method,
25
+ path: requestContext.path,
26
+ hostname: requestContext.hostname,
27
+ statusCode: 200,
28
+ responseTime: 0,
29
+ ip: requestContext.ip,
30
+ userAgent: requestContext.userAgent,
31
+ timestamp: new Date().toISOString()
32
+ };
33
+ try {
34
+ if(!requestData.path){
35
+ requestContext.sendResponse(400);
36
+ return;
37
+ }
38
+ let requestOrigin = requestContext.origin || '';
39
+ let allowedOrigin = ServerFactoryUtils.validateOrigin(
40
+ requestOrigin,
41
+ this.cdnServer.corsOrigins,
42
+ this.cdnServer.corsAllowAll
43
+ );
44
+ if('OPTIONS' === requestData.method){
45
+ this.handleOptionsRequest(requestContext, allowedOrigin);
46
+ return;
47
+ }
48
+ let filePath = this.cdnServer.resolveFilePath(requestData.path);
49
+ if(!filePath){
50
+ requestData.statusCode = 404;
51
+ requestData.responseTime = Date.now()-startTime;
52
+ requestContext.sendResponse(404);
53
+ this.cdnServer.invokeRequestError(requestData);
54
+ return;
55
+ }
56
+ let responseHeaders = this.buildResponseHeaders(allowedOrigin, filePath, requestContext.isHttp2);
57
+ requestContext.onSuccess(() => {
58
+ requestData.responseTime = Date.now()-startTime;
59
+ this.cdnServer.invokeRequestSuccess(requestData);
60
+ });
61
+ requestContext.onError((err) => {
62
+ requestData.statusCode = 500;
63
+ requestData.responseTime = Date.now()-startTime;
64
+ requestData.error = err;
65
+ ServerErrorHandler.handleError(
66
+ this.cdnServer.onError,
67
+ 'http2CdnServer',
68
+ this.cdnServer,
69
+ requestContext.isHttp2 ? 'stream-error' : 'http1-stream-error',
70
+ err,
71
+ {port: this.cdnServer.port, path: requestData.path}
72
+ );
73
+ this.cdnServer.invokeRequestError(requestData);
74
+ });
75
+ requestContext.sendFile(filePath, responseHeaders);
76
+ } catch (err) {
77
+ requestData.statusCode = 500;
78
+ requestData.responseTime = Date.now()-startTime;
79
+ requestData.error = err;
80
+ ServerErrorHandler.handleError(
81
+ this.cdnServer.onError,
82
+ 'http2CdnServer',
83
+ this.cdnServer,
84
+ requestContext.isHttp2 ? 'handle-stream-error' : 'handle-http1-request-error',
85
+ err,
86
+ {port: this.cdnServer.port, path: requestData.path}
87
+ );
88
+ this.cdnServer.invokeRequestError(requestData);
89
+ requestContext.sendResponse(500);
90
+ }
91
+ }
92
+
93
+ handleOptionsRequest(requestContext, allowedOrigin)
94
+ {
95
+ let optionsHeaders = requestContext.isHttp2 ? {':status': 200} : {};
96
+ if(allowedOrigin){
97
+ optionsHeaders['access-control-allow-origin'] = allowedOrigin;
98
+ }
99
+ optionsHeaders['access-control-allow-methods'] = this.cdnServer.corsMethods;
100
+ optionsHeaders['access-control-allow-headers'] = this.cdnServer.corsHeaders;
101
+ requestContext.sendResponse(200, optionsHeaders);
102
+ }
103
+
104
+ buildResponseHeaders(allowedOrigin, filePath, isHttp2)
105
+ {
106
+ let ext = FileHandler.extension(filePath);
107
+ let cacheAge = ServerFactoryUtils.getCacheConfigForPath(filePath, this.cdnServer.cacheConfig);
108
+ let responseHeaders = isHttp2 ? {':status': 200} : {};
109
+ let securityKeys = Object.keys(this.cdnServer.securityHeaders);
110
+ for(let headerKey of securityKeys){
111
+ responseHeaders[headerKey] = this.cdnServer.securityHeaders[headerKey];
112
+ }
113
+ if(allowedOrigin){
114
+ responseHeaders['access-control-allow-origin'] = allowedOrigin;
115
+ }
116
+ if(this.cdnServer.varyHeader){
117
+ responseHeaders['vary'] = this.cdnServer.varyHeader;
118
+ }
119
+ if(cacheAge){
120
+ responseHeaders['cache-control'] = 'public, max-age='+cacheAge+', immutable';
121
+ }
122
+ if(this.cdnServer.mimeTypes[ext]){
123
+ responseHeaders['content-type'] = this.cdnServer.mimeTypes[ext];
124
+ }
125
+ return responseHeaders;
126
+ }
127
+
128
+ }
129
+
130
+ module.exports.CdnRequestHandler = CdnRequestHandler;
@@ -0,0 +1,26 @@
1
+ /**
2
+ *
3
+ * Reldens - EventDispatcher
4
+ *
5
+ */
6
+
7
+ class EventDispatcher
8
+ {
9
+
10
+ static dispatch(onEventCallback, eventType, instanceName, instance, data = {})
11
+ {
12
+ if('function' !== typeof onEventCallback){
13
+ return;
14
+ }
15
+ onEventCallback({
16
+ eventType: eventType,
17
+ instanceName: instanceName,
18
+ instance: instance,
19
+ data: data,
20
+ timestamp: new Date().toISOString()
21
+ });
22
+ }
23
+
24
+ }
25
+
26
+ module.exports.EventDispatcher = EventDispatcher;
@@ -765,6 +765,19 @@ class FileHandler
765
765
  return this.writeFile(filePath, content.replace(searchValue, replaceValue));
766
766
  }
767
767
 
768
+ createReadStream(filePath, options)
769
+ {
770
+ if(!this.isValidPath(filePath)){
771
+ this.error = {message: 'Invalid file path.', filePath};
772
+ return false;
773
+ }
774
+ if(!this.isFile(filePath)){
775
+ this.error = {message: 'File does not exist or is not a file.', filePath};
776
+ return false;
777
+ }
778
+ return fs.createReadStream(filePath, options);
779
+ }
780
+
768
781
  }
769
782
 
770
783
  module.exports.FileHandler = new FileHandler();