@reldens/server-utils 0.45.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
 
@@ -117,12 +117,24 @@ class AppServerFactory
117
117
  this.http2CdnCacheConfig = {};
118
118
  this.http2CdnSecurityHeaders = {};
119
119
  this.http2CdnServer = false;
120
+ this.http2CdnDomains = [];
120
121
  this.reverseProxyEnabled = false;
121
122
  this.reverseProxyRules = [];
122
123
  this.onError = null;
123
124
  this.onRequestSuccess = null;
124
125
  this.onRequestError = null;
125
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);
126
138
  }
127
139
 
128
140
  buildStaticHeaders(res, path)
@@ -168,13 +180,11 @@ class AppServerFactory
168
180
  }
169
181
  return false;
170
182
  }
171
- EventDispatcher.dispatch(
172
- this.onEvent,
173
- 'app-server-created',
174
- 'appServerFactory',
175
- this,
176
- {port: this.port, useHttps: this.useHttps, isDevelopmentMode: this.isDevelopmentMode}
177
- );
183
+ this.dispatch('app-server-created', {
184
+ port: this.port,
185
+ useHttps: this.useHttps,
186
+ isDevelopmentMode: this.isDevelopmentMode
187
+ });
178
188
  if(this.http2CdnEnabled){
179
189
  if(!this.createHttp2CdnServer()){
180
190
  this.error = {message: 'The createHttp2CdnServer() returned false.'};
@@ -192,9 +202,14 @@ class AppServerFactory
192
202
  this.http2CdnServer = new Http2CdnServer();
193
203
  this.http2CdnServer.enabled = this.http2CdnEnabled;
194
204
  this.http2CdnServer.port = this.http2CdnPort;
195
- this.http2CdnServer.keyPath = this.http2CdnKeyPath || this.keyPath;
196
- this.http2CdnServer.certPath = this.http2CdnCertPath || this.certPath;
197
- 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
+ }
198
213
  this.http2CdnServer.staticPaths = this.http2CdnStaticPaths;
199
214
  this.http2CdnServer.cacheConfig = this.http2CdnCacheConfig && 0 < Object.keys(this.http2CdnCacheConfig).length
200
215
  ? this.http2CdnCacheConfig
@@ -219,13 +234,7 @@ class AppServerFactory
219
234
  this.error = this.http2CdnServer.error;
220
235
  return false;
221
236
  }
222
- EventDispatcher.dispatch(
223
- this.onEvent,
224
- 'http2-cdn-created',
225
- 'appServerFactory',
226
- this,
227
- {port: this.http2CdnPort}
228
- );
237
+ this.dispatch('http2-cdn-created', {port: this.http2CdnPort});
229
238
  return true;
230
239
  }
231
240
 
@@ -434,14 +443,7 @@ class AppServerFactory
434
443
  return next();
435
444
  }
436
445
  this.error = {message: 'No hostname provided and no default domain configured'};
437
- ServerErrorHandler.handleError(
438
- this.onError,
439
- 'appServerFactory',
440
- this,
441
- 'virtual-host-no-hostname',
442
- this.error,
443
- {request: req, response: res}
444
- );
446
+ this.handleError('virtual-host-no-hostname', this.error, {request: req, response: res});
445
447
  return res.status(400).send('Bad Request');
446
448
  }
447
449
  let domain = this.findDomainConfig(hostname);
@@ -451,10 +453,7 @@ class AppServerFactory
451
453
  return next();
452
454
  }
453
455
  this.error = {message: 'Unknown domain: '+hostname};
454
- ServerErrorHandler.handleError(
455
- this.onError,
456
- 'appServerFactory',
457
- this,
456
+ this.handleError(
458
457
  'virtual-host-unknown-domain',
459
458
  this.error,
460
459
  {hostname: hostname, request: req, response: res}
@@ -489,13 +488,7 @@ class AppServerFactory
489
488
  {
490
489
  if(!this.useHttps){
491
490
  let httpServer = http.createServer(this.app);
492
- EventDispatcher.dispatch(
493
- this.onEvent,
494
- 'http-server-created',
495
- 'appServerFactory',
496
- this,
497
- {port: this.port}
498
- );
491
+ this.dispatch('http-server-created', {port: this.port});
499
492
  return httpServer;
500
493
  }
501
494
  if(this.useVirtualHosts && 0 < this.domains.length){
@@ -524,13 +517,7 @@ class AppServerFactory
524
517
  }
525
518
  }
526
519
  let httpsServer = https.createServer(credentials, this.app);
527
- EventDispatcher.dispatch(
528
- this.onEvent,
529
- 'https-server-created',
530
- 'appServerFactory',
531
- this,
532
- {port: this.port}
533
- );
520
+ this.dispatch('https-server-created', {port: this.port});
534
521
  return httpsServer;
535
522
  }
536
523
 
@@ -549,10 +536,7 @@ class AppServerFactory
549
536
  let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
550
537
  if(!key){
551
538
  this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
552
- ServerErrorHandler.handleError(
553
- this.onError,
554
- 'appServerFactory',
555
- this,
539
+ this.handleError(
556
540
  'sni-key-read-failure',
557
541
  this.error,
558
542
  {hostname: hostname, domain: domain, keyPath: domain.keyPath}
@@ -562,10 +546,7 @@ class AppServerFactory
562
546
  let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
563
547
  if(!cert){
564
548
  this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
565
- ServerErrorHandler.handleError(
566
- this.onError,
567
- 'appServerFactory',
568
- this,
549
+ this.handleError(
569
550
  'sni-cert-read-failure',
570
551
  this.error,
571
552
  {hostname: hostname, domain: domain, certPath: domain.certPath}
@@ -576,13 +557,7 @@ class AppServerFactory
576
557
  callback(null, ctx);
577
558
  };
578
559
  let sniServer = https.createServer(httpsOptions, this.app);
579
- EventDispatcher.dispatch(
580
- this.onEvent,
581
- 'sni-server-created',
582
- 'appServerFactory',
583
- this,
584
- {port: this.port, domainsCount: this.domains.length}
585
- );
560
+ this.dispatch('sni-server-created', {port: this.port, domainsCount: this.domains.length});
586
561
  return sniServer;
587
562
  }
588
563
 
@@ -609,13 +584,7 @@ class AppServerFactory
609
584
  return false;
610
585
  }
611
586
  this.appServer.listen(listenPort);
612
- EventDispatcher.dispatch(
613
- this.onEvent,
614
- 'app-server-listening',
615
- 'appServerFactory',
616
- this,
617
- {port: listenPort}
618
- );
587
+ this.dispatch('app-server-listening', {port: listenPort});
619
588
  return true;
620
589
  }
621
590
 
@@ -677,13 +646,7 @@ class AppServerFactory
677
646
  return false;
678
647
  }
679
648
  this.domains.push(domainConfig);
680
- EventDispatcher.dispatch(
681
- this.onEvent,
682
- 'domain-added',
683
- 'appServerFactory',
684
- this,
685
- {hostname: domainConfig.hostname}
686
- );
649
+ this.dispatch('domain-added', {hostname: domainConfig.hostname});
687
650
  return true;
688
651
  }
689
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.45.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",