@reldens/server-utils 0.44.0 → 0.46.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.
@@ -77,6 +77,7 @@ Creates Express app servers with modular security components. Supports HTTP, HTT
77
77
  - Lifecycle event dispatching
78
78
 
79
79
  **Configuration Properties:**
80
+ - `http2CdnDomains` - Array of domain configurations for HTTP/2 CDN multi-certificate SNI (optional)
80
81
  - `onError` - Custom error handler callback for server errors
81
82
  - `onRequestSuccess` - Callback for successful requests
82
83
  - `onRequestError` - Callback for failed requests
@@ -84,8 +85,11 @@ Creates Express app servers with modular security components. Supports HTTP, HTT
84
85
 
85
86
  **Methods:**
86
87
  - `createAppServer(config)` - Create and configure server
88
+ - `createHttp2CdnServer()` - Create HTTP/2 CDN server with optional multi-cert SNI
87
89
  - `addDomain(domainConfig)` - Add virtual host domain
88
90
  - `addDevelopmentDomain(domain)` - Add development domain
91
+ - `dispatch(eventName, eventData)` - Dispatch lifecycle event (wrapper for EventDispatcher)
92
+ - `handleError(errorType, error, context)` - Handle and log error (wrapper for ServerErrorHandler)
89
93
  - `enableServeHome(app, callback)` - Enable homepage serving
90
94
  - `serveStatics(app, staticPath)` - Serve static files
91
95
  - `enableCSP(cspOptions)` - Enable Content Security Policy
@@ -157,6 +161,7 @@ File upload handling with Multer.
157
161
  HTTP/2 secure server for CDN-like static file serving.
158
162
 
159
163
  **Features:**
164
+ - Multi-certificate SNI support for multiple domains
160
165
  - Optimized for CSS, JavaScript, images, and fonts
161
166
  - Dynamic CORS origin validation with regex pattern support
162
167
  - Configurable cache headers per file extension
@@ -167,19 +172,37 @@ HTTP/2 secure server for CDN-like static file serving.
167
172
  - Comprehensive error handling (server, TLS, session, stream errors)
168
173
 
169
174
  **Configuration Properties:**
175
+ - `domains` - Array of domain configurations for multi-certificate SNI (optional)
176
+ - `keyPath` - Path to SSL key file (backward compatibility, single cert mode)
177
+ - `certPath` - Path to SSL certificate file (backward compatibility, single cert mode)
170
178
  - `onError` - Custom error handler callback for server errors
171
179
  - `onRequestSuccess` - Callback for successful requests
172
180
  - `onRequestError` - Callback for failed requests
173
181
  - `onEvent` - Callback for lifecycle events
174
182
 
183
+ **Multi-Certificate Configuration Example:**
184
+ ```javascript
185
+ domains: [
186
+ {hostname: 'cdn.domain1.com', keyPath: '/path/to/key1.pem', certPath: '/path/to/cert1.pem'},
187
+ {hostname: 'cdn.domain2.com', keyPath: '/path/to/key2.pem', certPath: '/path/to/cert2.pem'}
188
+ ]
189
+ ```
190
+
175
191
  **Methods:**
176
- - `create()` - Create HTTP/2 secure server
192
+ - `create()` - Create HTTP/2 secure server with SNI support
177
193
  - `listen()` - Start listening on configured port
178
194
  - `close()` - Gracefully close server
195
+ - `dispatch(eventName, eventData)` - Dispatch lifecycle event (wrapper for EventDispatcher)
196
+ - `handleError(errorType, error, context)` - Handle and log error (wrapper for ServerErrorHandler)
179
197
  - `handleStream(stream, headers)` - Handle HTTP/2 stream
180
198
  - `handleHttp1Request(req, res)` - Handle HTTP/1.1 fallback requests
181
199
  - `resolveFilePath(requestPath)` - Resolve file path from request
182
200
  - `setupEventHandlers()` - Configure server error event handlers
201
+ - `setupDomainConfiguration()` - Configure single or multi-certificate mode
202
+ - `validateCertificates()` - Validate all domain certificates exist
203
+ - `buildSniContexts()` - Build TLS contexts for each domain
204
+ - `getSniCallback()` - Get SNI callback for certificate selection
205
+ - `buildServerOptions()` - Build HTTP/2 server options with SNI support
183
206
 
184
207
  ## Utility Classes
185
208
 
package/CLAUDE.md CHANGED
@@ -39,7 +39,7 @@ See `.claude/api-reference.md` for complete API documentation of all classes and
39
39
 
40
40
  **UploaderFactory** - File upload handling with Multer and multi-level security validation.
41
41
 
42
- **Http2CdnServer** - HTTP/2 secure server for CDN-like static file serving with CORS, cache headers, and callback-based logging.
42
+ **Http2CdnServer** - HTTP/2 secure server for CDN-like static file serving with multi-certificate SNI support, CORS, cache headers, and callback-based logging.
43
43
 
44
44
  ## Utility Classes Summary
45
45
 
@@ -115,13 +115,26 @@ class AppServerFactory
115
115
  this.http2CdnCorsAllowAll = false;
116
116
  this.http2CdnMimeTypes = {};
117
117
  this.http2CdnCacheConfig = {};
118
+ this.http2CdnSecurityHeaders = {};
118
119
  this.http2CdnServer = false;
120
+ this.http2CdnDomains = [];
119
121
  this.reverseProxyEnabled = false;
120
122
  this.reverseProxyRules = [];
121
123
  this.onError = null;
122
124
  this.onRequestSuccess = null;
123
125
  this.onRequestError = null;
124
126
  this.onEvent = null;
127
+ this.eventSource = 'appServerFactory';
128
+ }
129
+
130
+ dispatch(eventName, eventData)
131
+ {
132
+ EventDispatcher.dispatch(this.onEvent, eventName, this.eventSource, this, eventData);
133
+ }
134
+
135
+ handleError(errorType, error, context)
136
+ {
137
+ ServerErrorHandler.handleError(this.onError, this.eventSource, this, errorType, error, context);
125
138
  }
126
139
 
127
140
  buildStaticHeaders(res, path)
@@ -167,13 +180,11 @@ class AppServerFactory
167
180
  }
168
181
  return false;
169
182
  }
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
- );
183
+ this.dispatch('app-server-created', {
184
+ port: this.port,
185
+ useHttps: this.useHttps,
186
+ isDevelopmentMode: this.isDevelopmentMode
187
+ });
177
188
  if(this.http2CdnEnabled){
178
189
  if(!this.createHttp2CdnServer()){
179
190
  this.error = {message: 'The createHttp2CdnServer() returned false.'};
@@ -191,9 +202,14 @@ class AppServerFactory
191
202
  this.http2CdnServer = new Http2CdnServer();
192
203
  this.http2CdnServer.enabled = this.http2CdnEnabled;
193
204
  this.http2CdnServer.port = this.http2CdnPort;
194
- this.http2CdnServer.keyPath = this.http2CdnKeyPath || this.keyPath;
195
- this.http2CdnServer.certPath = this.http2CdnCertPath || this.certPath;
196
- this.http2CdnServer.httpsChain = this.http2CdnHttpsChain || this.httpsChain;
205
+ if(this.http2CdnDomains && 0 < this.http2CdnDomains.length){
206
+ this.http2CdnServer.domains = this.http2CdnDomains;
207
+ }
208
+ if(!this.http2CdnDomains || 0 === this.http2CdnDomains.length){
209
+ this.http2CdnServer.keyPath = this.http2CdnKeyPath || this.keyPath;
210
+ this.http2CdnServer.certPath = this.http2CdnCertPath || this.certPath;
211
+ this.http2CdnServer.httpsChain = this.http2CdnHttpsChain || this.httpsChain;
212
+ }
197
213
  this.http2CdnServer.staticPaths = this.http2CdnStaticPaths;
198
214
  this.http2CdnServer.cacheConfig = this.http2CdnCacheConfig && 0 < Object.keys(this.http2CdnCacheConfig).length
199
215
  ? this.http2CdnCacheConfig
@@ -203,6 +219,9 @@ class AppServerFactory
203
219
  }
204
220
  this.http2CdnServer.corsOrigins = this.http2CdnCorsOrigins;
205
221
  this.http2CdnServer.corsAllowAll = this.http2CdnCorsAllowAll;
222
+ if(this.http2CdnSecurityHeaders && 0 < Object.keys(this.http2CdnSecurityHeaders).length){
223
+ this.http2CdnServer.securityHeaders = this.http2CdnSecurityHeaders;
224
+ }
206
225
  this.http2CdnServer.onError = this.onError;
207
226
  this.http2CdnServer.onRequestSuccess = this.onRequestSuccess;
208
227
  this.http2CdnServer.onRequestError = this.onRequestError;
@@ -215,13 +234,7 @@ class AppServerFactory
215
234
  this.error = this.http2CdnServer.error;
216
235
  return false;
217
236
  }
218
- EventDispatcher.dispatch(
219
- this.onEvent,
220
- 'http2-cdn-created',
221
- 'appServerFactory',
222
- this,
223
- {port: this.http2CdnPort}
224
- );
237
+ this.dispatch('http2-cdn-created', {port: this.http2CdnPort});
225
238
  return true;
226
239
  }
227
240
 
@@ -430,14 +443,7 @@ class AppServerFactory
430
443
  return next();
431
444
  }
432
445
  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
- );
446
+ this.handleError('virtual-host-no-hostname', this.error, {request: req, response: res});
441
447
  return res.status(400).send('Bad Request');
442
448
  }
443
449
  let domain = this.findDomainConfig(hostname);
@@ -447,10 +453,7 @@ class AppServerFactory
447
453
  return next();
448
454
  }
449
455
  this.error = {message: 'Unknown domain: '+hostname};
450
- ServerErrorHandler.handleError(
451
- this.onError,
452
- 'appServerFactory',
453
- this,
456
+ this.handleError(
454
457
  'virtual-host-unknown-domain',
455
458
  this.error,
456
459
  {hostname: hostname, request: req, response: res}
@@ -485,13 +488,7 @@ class AppServerFactory
485
488
  {
486
489
  if(!this.useHttps){
487
490
  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
- );
491
+ this.dispatch('http-server-created', {port: this.port});
495
492
  return httpServer;
496
493
  }
497
494
  if(this.useVirtualHosts && 0 < this.domains.length){
@@ -520,13 +517,7 @@ class AppServerFactory
520
517
  }
521
518
  }
522
519
  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
- );
520
+ this.dispatch('https-server-created', {port: this.port});
530
521
  return httpsServer;
531
522
  }
532
523
 
@@ -545,10 +536,7 @@ class AppServerFactory
545
536
  let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
546
537
  if(!key){
547
538
  this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
548
- ServerErrorHandler.handleError(
549
- this.onError,
550
- 'appServerFactory',
551
- this,
539
+ this.handleError(
552
540
  'sni-key-read-failure',
553
541
  this.error,
554
542
  {hostname: hostname, domain: domain, keyPath: domain.keyPath}
@@ -558,10 +546,7 @@ class AppServerFactory
558
546
  let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
559
547
  if(!cert){
560
548
  this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
561
- ServerErrorHandler.handleError(
562
- this.onError,
563
- 'appServerFactory',
564
- this,
549
+ this.handleError(
565
550
  'sni-cert-read-failure',
566
551
  this.error,
567
552
  {hostname: hostname, domain: domain, certPath: domain.certPath}
@@ -572,13 +557,7 @@ class AppServerFactory
572
557
  callback(null, ctx);
573
558
  };
574
559
  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
- );
560
+ this.dispatch('sni-server-created', {port: this.port, domainsCount: this.domains.length});
582
561
  return sniServer;
583
562
  }
584
563
 
@@ -605,13 +584,7 @@ class AppServerFactory
605
584
  return false;
606
585
  }
607
586
  this.appServer.listen(listenPort);
608
- EventDispatcher.dispatch(
609
- this.onEvent,
610
- 'app-server-listening',
611
- 'appServerFactory',
612
- this,
613
- {port: listenPort}
614
- );
587
+ this.dispatch('app-server-listening', {port: listenPort});
615
588
  return true;
616
589
  }
617
590
 
@@ -673,13 +646,7 @@ class AppServerFactory
673
646
  return false;
674
647
  }
675
648
  this.domains.push(domainConfig);
676
- EventDispatcher.dispatch(
677
- this.onEvent,
678
- 'domain-added',
679
- 'appServerFactory',
680
- this,
681
- {hostname: domainConfig.hostname}
682
- );
649
+ this.dispatch('domain-added', {hostname: domainConfig.hostname});
683
650
  return true;
684
651
  }
685
652
 
@@ -6,7 +6,6 @@
6
6
 
7
7
  const { FileHandler } = require('./file-handler');
8
8
  const { ServerFactoryUtils } = require('./server-factory-utils');
9
- const { ServerErrorHandler } = require('./server-error-handler');
10
9
 
11
10
  class CdnRequestHandler
12
11
  {
@@ -62,10 +61,7 @@ class CdnRequestHandler
62
61
  requestData.statusCode = 500;
63
62
  requestData.responseTime = Date.now()-startTime;
64
63
  requestData.error = err;
65
- ServerErrorHandler.handleError(
66
- this.cdnServer.onError,
67
- 'http2CdnServer',
68
- this.cdnServer,
64
+ this.cdnServer.handleError(
69
65
  requestContext.isHttp2 ? 'stream-error' : 'http1-stream-error',
70
66
  err,
71
67
  {port: this.cdnServer.port, path: requestData.path}
@@ -77,10 +73,7 @@ class CdnRequestHandler
77
73
  requestData.statusCode = 500;
78
74
  requestData.responseTime = Date.now()-startTime;
79
75
  requestData.error = err;
80
- ServerErrorHandler.handleError(
81
- this.cdnServer.onError,
82
- 'http2CdnServer',
83
- this.cdnServer,
76
+ this.cdnServer.handleError(
84
77
  requestContext.isHttp2 ? 'handle-stream-error' : 'handle-http1-request-error',
85
78
  err,
86
79
  {port: this.cdnServer.port, path: requestData.path}
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  const http2 = require('http2');
8
+ const tls = require('tls');
8
9
  const { FileHandler } = require('./file-handler');
9
10
  const { ServerDefaultConfigurations } = require('./server-default-configurations');
10
11
  const { ServerFactoryUtils } = require('./server-factory-utils');
@@ -41,22 +42,112 @@ class Http2CdnServer
41
42
  this.onRequestError = null;
42
43
  this.onEvent = null;
43
44
  this.requestHandler = new CdnRequestHandler(this);
45
+ this.domains = [];
46
+ this.useMultiCert = false;
47
+ this.defaultDomain = null;
48
+ this.sniContexts = {};
49
+ this.eventSource = 'http2CdnServer';
50
+ }
51
+
52
+ dispatch(eventName, eventData)
53
+ {
54
+ EventDispatcher.dispatch(this.onEvent, eventName, this.eventSource, this, eventData);
55
+ }
56
+
57
+ handleError(errorType, error, context)
58
+ {
59
+ ServerErrorHandler.handleError(this.onError, this.eventSource, this, errorType, error, context);
44
60
  }
45
61
 
46
62
  create()
47
63
  {
48
- if(!this.keyPath || !this.certPath){
49
- this.error = {message: 'Missing SSL certificates'};
64
+ this.setupDomainConfiguration();
65
+ if(!this.validateCertificates()){
50
66
  return false;
51
67
  }
52
- let key = FileHandler.readFile(this.keyPath);
68
+ if(this.useMultiCert){
69
+ this.buildSniContexts();
70
+ }
71
+ let options = this.buildServerOptions();
72
+ if(!options){
73
+ return false;
74
+ }
75
+ this.http2Server = http2.createSecureServer(options);
76
+ this.setupEventHandlers();
77
+ this.dispatch(
78
+ 'cdn-server-created',
79
+ {port: this.port, allowHTTP1: this.allowHTTP1, multiCert: this.useMultiCert})
80
+ ;
81
+ return true;
82
+ }
83
+
84
+ setupDomainConfiguration()
85
+ {
86
+ if(this.domains && 0 < this.domains.length){
87
+ this.useMultiCert = true;
88
+ this.defaultDomain = this.domains[0];
89
+ this.dispatch('cdn-multi-cert-enabled', {domains: this.domains.map(d => d.hostname)});
90
+ return;
91
+ }
92
+ if(this.keyPath && this.certPath){
93
+ this.useMultiCert = false;
94
+ this.domains = [{hostname: 'default', keyPath: this.keyPath, certPath: this.certPath}];
95
+ this.defaultDomain = this.domains[0];
96
+ this.dispatch('cdn-single-cert-mode', {hostname: this.defaultDomain.hostname});
97
+ return;
98
+ }
99
+ this.error = {message: 'HTTP/2 CDN requires domains array or keyPath/certPath'};
100
+ }
101
+
102
+ validateCertificates()
103
+ {
104
+ for(let domain of this.domains){
105
+ if(!FileHandler.exists(domain.keyPath)){
106
+ this.error = {message: 'Certificate key not found for '+domain.hostname+': '+domain.keyPath};
107
+ this.handleError('certificate-key-not-found', this.error, {hostname: domain.hostname, keyPath: domain.keyPath});
108
+ return false;
109
+ }
110
+ if(!FileHandler.exists(domain.certPath)){
111
+ this.error = {message: 'Certificate file not found for '+domain.hostname+': '+domain.certPath};
112
+ this.handleError('certificate-not-found', this.error, {hostname: domain.hostname, certPath: domain.certPath});
113
+ return false;
114
+ }
115
+ }
116
+ return true;
117
+ }
118
+
119
+ buildSniContexts()
120
+ {
121
+ for(let domain of this.domains){
122
+ let key = FileHandler.readFile(domain.keyPath);
123
+ let cert = FileHandler.readFile(domain.certPath);
124
+ this.sniContexts[domain.hostname] = tls.createSecureContext({key, cert});
125
+ }
126
+ }
127
+
128
+ getSniCallback()
129
+ {
130
+ return (servername, callback) => {
131
+ this.dispatch('cdn-sni-request', {servername});
132
+ let context = this.sniContexts[servername];
133
+ if(!context){
134
+ this.dispatch('cdn-sni-fallback', {servername, defaultHostname: this.defaultDomain.hostname});
135
+ context = this.sniContexts[this.defaultDomain.hostname];
136
+ }
137
+ callback(null, context);
138
+ };
139
+ }
140
+
141
+ buildServerOptions()
142
+ {
143
+ let key = FileHandler.readFile(this.defaultDomain.keyPath);
53
144
  if(!key){
54
- this.error = {message: 'Could not read key from: '+this.keyPath};
145
+ this.error = {message: 'Could not read key from: '+this.defaultDomain.keyPath};
55
146
  return false;
56
147
  }
57
- let cert = FileHandler.readFile(this.certPath);
148
+ let cert = FileHandler.readFile(this.defaultDomain.certPath);
58
149
  if(!cert){
59
- this.error = {message: 'Could not read cert from: '+this.certPath};
150
+ this.error = {message: 'Could not read cert from: '+this.defaultDomain.certPath};
60
151
  return false;
61
152
  }
62
153
  let options = {key, cert, allowHTTP1: this.allowHTTP1};
@@ -66,16 +157,10 @@ class Http2CdnServer
66
157
  options.ca = ca;
67
158
  }
68
159
  }
69
- this.http2Server = http2.createSecureServer(options);
70
- this.setupEventHandlers();
71
- EventDispatcher.dispatch(
72
- this.onEvent,
73
- 'cdn-server-created',
74
- 'http2CdnServer',
75
- this,
76
- {port: this.port, allowHTTP1: this.allowHTTP1}
77
- );
78
- return true;
160
+ if(this.useMultiCert){
161
+ options.SNICallback = this.getSniCallback();
162
+ }
163
+ return options;
79
164
  }
80
165
 
81
166
  setupEventHandlers()
@@ -87,42 +172,15 @@ class Http2CdnServer
87
172
  this.handleHttp1Request(req, res);
88
173
  });
89
174
  this.http2Server.on('error', (err) => {
90
- ServerErrorHandler.handleError(
91
- this.onError,
92
- 'http2CdnServer',
93
- this,
94
- 'server-error',
95
- err,
96
- {port: this.port}
97
- );
175
+ this.handleError('server-error', err, {port: this.port});
98
176
  });
99
177
  this.http2Server.on('tlsClientError', (err, tlsSocket) => {
100
- ServerErrorHandler.handleError(
101
- this.onError,
102
- 'http2CdnServer',
103
- this,
104
- 'tls-client-error',
105
- err,
106
- {port: this.port, remoteAddress: tlsSocket.remoteAddress}
107
- );
178
+ this.handleError('tls-client-error', err, {port: this.port, remoteAddress: tlsSocket.remoteAddress});
108
179
  });
109
180
  this.http2Server.on('sessionError', (err) => {
110
- ServerErrorHandler.handleError(
111
- this.onError,
112
- 'http2CdnServer',
113
- this,
114
- 'session-error',
115
- err,
116
- {port: this.port}
117
- );
181
+ this.handleError('session-error', err, {port: this.port});
118
182
  });
119
- EventDispatcher.dispatch(
120
- this.onEvent,
121
- 'cdn-handlers-setup',
122
- 'http2CdnServer',
123
- this,
124
- {port: this.port}
125
- );
183
+ this.dispatch('cdn-handlers-setup', {port: this.port});
126
184
  }
127
185
 
128
186
  invokeRequestSuccess(requestData)
@@ -246,13 +304,7 @@ class Http2CdnServer
246
304
  return false;
247
305
  }
248
306
  this.http2Server.listen(this.port);
249
- EventDispatcher.dispatch(
250
- this.onEvent,
251
- 'cdn-server-listening',
252
- 'http2CdnServer',
253
- this,
254
- {port: this.port}
255
- );
307
+ this.dispatch('cdn-server-listening', {port: this.port});
256
308
  return true;
257
309
  }
258
310
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/server-utils",
3
3
  "scope": "@reldens",
4
- "version": "0.44.0",
4
+ "version": "0.46.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -38,10 +38,10 @@
38
38
  "dependencies": {
39
39
  "body-parser": "2.2.2",
40
40
  "compression": "1.8.1",
41
- "cors": "2.8.5",
41
+ "cors": "2.8.6",
42
42
  "express": "4.22.1",
43
43
  "express-rate-limit": "8.2.1",
44
- "express-session": "1.18.2",
44
+ "express-session": "1.19.0",
45
45
  "helmet": "8.1.0",
46
46
  "http-proxy-middleware": "3.0.5",
47
47
  "multer": "2.0.2",