@reldens/server-utils 0.37.0 → 0.38.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Reldens - Server Utils
2
2
 
3
- A Node.js server toolkit providing secure application server creation, file handling, encryption, and file upload capabilities for production-ready applications with modular security configurations.
3
+ A Node.js server toolkit providing secure application server creation, HTTP/2 CDN support, file handling, encryption, and file upload capabilities for production-ready applications with modular security configurations.
4
4
 
5
5
  [![Reldens - GitHub - Release](https://www.dwdeveloper.com/media/reldens/reldens-mmorpg-platform.png)](https://github.com/damian-pastorini/reldens)
6
6
 
@@ -9,12 +9,14 @@ A Node.js server toolkit providing secure application server creation, file hand
9
9
  ### AppServerFactory
10
10
  - Complete Express.js server configuration with modular security
11
11
  - HTTPS/HTTP server creation with SSL certificate management
12
+ - HTTP/2 CDN server with optimized static asset delivery
12
13
  - Optimized static asset caching for CSS, JS, fonts, and images
13
14
  - SNI (Server Name Indication) support for multi-domain hosting
14
15
  - Virtual host management with domain mapping
15
16
  - Development mode detection with appropriate configurations
16
17
  - CORS configuration with flexible origin management
17
18
  - Rate limiting with customizable thresholds
19
+ - Reverse proxy support for routing multiple domains to backend servers
18
20
  - Security headers and XSS protection
19
21
  - Helmet integration for enhanced security
20
22
  - Protocol enforcement (HTTP to HTTPS redirection)
@@ -24,6 +26,18 @@ A Node.js server toolkit providing secure application server creation, file hand
24
26
  - Compression middleware with smart filtering
25
27
  - Input validation utilities
26
28
 
29
+ ### Http2CdnServer
30
+ - Dedicated HTTP/2 server for static asset delivery
31
+ - Optimized for serving CSS, JavaScript, images, and fonts
32
+ - Dynamic CORS origin validation with regex pattern support
33
+ - Configurable cache headers per file extension
34
+ - Comprehensive MIME type detection
35
+ - HTTP/1.1 fallback support
36
+ - Security headers (X-Content-Type-Options, X-Frame-Options, Vary)
37
+ - Standalone or integrated with AppServerFactory
38
+ - Multiple static path support
39
+ - Separate SSL certificate support from main server
40
+
27
41
  #### Modular Security Components
28
42
  The AppServerFactory now uses specialized security configurers:
29
43
 
@@ -31,6 +45,7 @@ The AppServerFactory now uses specialized security configurers:
31
45
  - **DevelopmentModeDetector** - Automatic development environment detection
32
46
  - **ProtocolEnforcer** - Protocol redirection with development mode awareness
33
47
  - **RateLimitConfigurer** - Global and endpoint-specific rate limiting
48
+ - **ReverseProxyConfigurer** - Domain-based reverse proxy with WebSocket support
34
49
  - **SecurityConfigurer** - Helmet integration with CSP management and XSS protection
35
50
 
36
51
  ### FileHandler
@@ -100,6 +115,65 @@ if(serverResult){
100
115
  }
101
116
  ```
102
117
 
118
+ ### HTTP/2 CDN Server with Express
119
+
120
+ Serve your main application through Express on port 443, and static assets through HTTP/2 CDN on port 8443:
121
+
122
+ ```javascript
123
+ let appServerFactory = new AppServerFactory();
124
+ let serverResult = appServerFactory.createAppServer({
125
+ port: 443,
126
+ useHttps: true,
127
+ keyPath: '/ssl/main-server.key',
128
+ certPath: '/ssl/main-server.crt',
129
+ http2CdnEnabled: true,
130
+ http2CdnPort: 8443,
131
+ http2CdnKeyPath: '/ssl/cdn-server.key',
132
+ http2CdnCertPath: '/ssl/cdn-server.crt',
133
+ http2CdnStaticPaths: ['/var/www/public'],
134
+ http2CdnCorsOrigins: [
135
+ 'https://example.com',
136
+ /^https:\/\/(www\.)?example\.(com|net)$/
137
+ ],
138
+ autoListen: true
139
+ });
140
+
141
+ if(serverResult){
142
+ let { app, appServer, http2CdnServer } = serverResult;
143
+ console.log('Express server on port 443');
144
+ console.log('HTTP/2 CDN server on port 8443');
145
+ }
146
+ ```
147
+
148
+ **Note:** If `http2CdnKeyPath` and `http2CdnCertPath` are not specified, the CDN server will use the same certificates as the main server (`keyPath` and `certPath`).
149
+
150
+ Browser usage:
151
+ ```html
152
+ <link rel="stylesheet" href="https://cdn.example.com:8443/css/style.css">
153
+ <script src="https://cdn.example.com:8443/js/app.js"></script>
154
+ ```
155
+
156
+ ### Standalone HTTP/2 CDN Server
157
+
158
+ ```javascript
159
+ const { Http2CdnServer } = require('@reldens/server-utils');
160
+
161
+ let cdnServer = new Http2CdnServer();
162
+ cdnServer.port = 8443;
163
+ cdnServer.keyPath = '/ssl/cdn.key';
164
+ cdnServer.certPath = '/ssl/cdn.crt';
165
+ cdnServer.staticPaths = ['/var/www/public', '/var/www/assets'];
166
+ cdnServer.corsOrigins = [
167
+ 'https://main-site.com',
168
+ /^https:\/\/(new\.)?((site1|site2)\.(com|net))$/
169
+ ];
170
+
171
+ if(cdnServer.create()){
172
+ cdnServer.listen();
173
+ console.log('HTTP/2 CDN running on port 8443');
174
+ }
175
+ ```
176
+
103
177
  ### HTTPS Server with Optimized Caching
104
178
 
105
179
  ```javascript
@@ -206,6 +280,56 @@ app.post('/upload', uploader, (req, res) => {
206
280
 
207
281
  ## Advanced Configuration
208
282
 
283
+ ### HTTP/2 CDN with Separate Certificates
284
+
285
+ Configure separate SSL certificates for your CDN server:
286
+
287
+ ```javascript
288
+ let appServerFactory = new AppServerFactory();
289
+ let serverResult = appServerFactory.createAppServer({
290
+ useHttps: true,
291
+ port: 443,
292
+ keyPath: '/ssl/app-server.key',
293
+ certPath: '/ssl/app-server.crt',
294
+ http2CdnEnabled: true,
295
+ http2CdnPort: 8443,
296
+ http2CdnKeyPath: '/ssl/cdn-server.key',
297
+ http2CdnCertPath: '/ssl/cdn-server.crt',
298
+ http2CdnHttpsChain: '/ssl/cdn-chain.pem',
299
+ http2CdnStaticPaths: ['/var/www/public'],
300
+ http2CdnCorsOrigins: [
301
+ 'https://main-site.com',
302
+ 'https://app.main-site.com',
303
+ /^https:\/\/(new\.)?main-site\.(com|net)$/
304
+ ],
305
+ http2CdnCacheConfig: {
306
+ '.css': 31536000,
307
+ '.js': 31536000,
308
+ '.woff2': 31536000,
309
+ '.png': 2592000
310
+ }
311
+ });
312
+ ```
313
+
314
+ ### HTTP/2 CDN with Multiple Origins
315
+
316
+ The HTTP/2 CDN server supports multiple origin validation methods:
317
+
318
+ ```javascript
319
+ let appServerFactory = new AppServerFactory();
320
+ let serverResult = appServerFactory.createAppServer({
321
+ http2CdnEnabled: true,
322
+ http2CdnPort: 8443,
323
+ http2CdnStaticPaths: ['/var/www/public'],
324
+ http2CdnCorsOrigins: [
325
+ 'https://main-site.com',
326
+ 'https://app.main-site.com',
327
+ /^https:\/\/(new\.)?main-site\.(com|net)$/,
328
+ /^https:\/\/(app|admin)\.secondary-site\.com$/
329
+ ]
330
+ });
331
+ ```
332
+
209
333
  ### HTTPS Server with Multiple Domains
210
334
 
211
335
  ```javascript
@@ -350,6 +474,68 @@ let serverResult = appServerFactory.createAppServer({
350
474
  });
351
475
  ```
352
476
 
477
+ ### Reverse Proxy Configuration
478
+
479
+ Route multiple domains to different backend servers through a single SSL-enabled entry point:
480
+
481
+ ```javascript
482
+ let appServerFactory = new AppServerFactory();
483
+
484
+ let serverResult = appServerFactory.createAppServer({
485
+ port: 443,
486
+ useHttps: true,
487
+ useVirtualHosts: true,
488
+ keyPath: '/ssl/server.key',
489
+ certPath: '/ssl/server.crt',
490
+ reverseProxyEnabled: true,
491
+ reverseProxyRules: [
492
+ {
493
+ hostname: 'demo.reldens.com',
494
+ target: 'https://localhost:8444',
495
+ pathPrefix: '/',
496
+ websocket: true,
497
+ secure: false
498
+ },
499
+ {
500
+ hostname: 'api.example.com',
501
+ target: 'https://localhost:8445',
502
+ pathPrefix: '/',
503
+ websocket: false
504
+ }
505
+ ],
506
+ autoListen: true
507
+ });
508
+ ```
509
+
510
+ #### Reverse Proxy Features
511
+
512
+ - Multiple backend routing with independent configuration per domain
513
+ - WebSocket support for real-time applications
514
+ - SSL termination at entry point
515
+ - Header preservation (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host)
516
+ - Virtual host integration
517
+ - Path-based routing
518
+ - Graceful error handling
519
+
520
+ #### Rule Properties
521
+
522
+ - `hostname` (string, required) - Domain to match
523
+ - `target` (string, required) - Backend URL
524
+ - `pathPrefix` (string, optional) - Path prefix, default: '/'
525
+ - `websocket` (boolean, optional) - Enable WebSocket, default: true
526
+ - `changeOrigin` (boolean, optional) - Change origin header, default: true
527
+ - `secure` (boolean, optional) - Verify SSL certificates, default: false
528
+ - `logLevel` (string, optional) - 'debug', 'info', 'warn', 'error', 'silent'
529
+
530
+ #### Example: Multiple Game Servers
531
+
532
+ ```javascript
533
+ reverseProxyRules: [
534
+ { hostname: 'demo.game.com', target: 'https://localhost:8444', websocket: true },
535
+ { hostname: 'staging.game.com', target: 'https://localhost:8445', websocket: true }
536
+ ]
537
+ ```
538
+
353
539
  ## API Reference
354
540
 
355
541
  ### AppServerFactory Methods
@@ -365,6 +551,47 @@ let serverResult = appServerFactory.createAppServer({
365
551
  - `listen(port)` - Starts server listening
366
552
  - `close()` - Gracefully closes server
367
553
 
554
+ ### AppServerFactory HTTP/2 CDN Configuration
555
+
556
+ - `http2CdnEnabled` - Enable HTTP/2 CDN server (default: false)
557
+ - `http2CdnPort` - HTTP/2 CDN port (default: 8443)
558
+ - `http2CdnKeyPath` - CDN SSL private key path (falls back to `keyPath`)
559
+ - `http2CdnCertPath` - CDN SSL certificate path (falls back to `certPath`)
560
+ - `http2CdnHttpsChain` - CDN certificate chain path (falls back to `httpsChain`)
561
+ - `http2CdnStaticPaths` - Paths to serve from CDN (default: [])
562
+ - `http2CdnCorsOrigins` - Allowed CORS origins for CDN (default: [])
563
+ - `http2CdnCorsAllowAll` - Allow all origins (default: false)
564
+ - `http2CdnMimeTypes` - Override default MIME types (default: {})
565
+ - `http2CdnCacheConfig` - Override default cache config (default: {})
566
+
567
+ ### AppServerFactory Reverse Proxy Configuration
568
+
569
+ - `reverseProxyEnabled` - Enable reverse proxy (default: false)
570
+ - `reverseProxyRules` - Array of proxy rules (default: [])
571
+
572
+ ### Http2CdnServer Methods
573
+
574
+ - `create()` - Creates HTTP/2 secure server
575
+ - `listen()` - Starts listening on configured port
576
+ - `close()` - Gracefully closes HTTP/2 server
577
+
578
+ ### Http2CdnServer Configuration
579
+
580
+ - `port` - Server port (default: 8443)
581
+ - `keyPath` - SSL private key path
582
+ - `certPath` - SSL certificate path
583
+ - `httpsChain` - Certificate chain path (optional)
584
+ - `staticPaths` - Array of static file directories
585
+ - `cacheConfig` - Cache max-age per extension
586
+ - `allowHTTP1` - Allow HTTP/1.1 fallback (default: true)
587
+ - `corsOrigins` - Array of allowed origins (strings or RegExp)
588
+ - `corsAllowAll` - Allow all origins (default: false)
589
+ - `corsMethods` - Allowed HTTP methods (default: 'GET, OPTIONS')
590
+ - `corsHeaders` - Allowed request headers (default: 'Content-Type')
591
+ - `securityHeaders` - Custom security headers
592
+ - `varyHeader` - Vary header value (default: 'Accept-Encoding, Origin')
593
+ - `mimeTypes` - MIME type mappings
594
+
368
595
  ### FileHandler Methods
369
596
 
370
597
  - `exists(path)` - Checks if file or folder exists
@@ -386,11 +613,11 @@ let serverResult = appServerFactory.createAppServer({
386
613
  - `generateSecureFilename(originalName)` - Generates cryptographically secure filename
387
614
  - `quarantineFile(path, reason)` - Moves file to quarantine folder
388
615
  - `createTempFile(prefix, extension)` - Creates a temporary file path
389
- - `moveFile(from, to)` - Moves file to new location
616
+ - `moveFile(from, to)` - Moves a file to new location
390
617
  - `getFileSize(path)` - Gets file size in bytes
391
618
  - `compareFiles(file1, file2)` - Compares file contents
392
- - `getRelativePath(from, to)` - Calculates relative path
393
- - `walkDirectory(path, callback)` - Recursively processes directory tree
619
+ - `getRelativePath(from, to)` - Calculates a relative path
620
+ - `walkDirectory(path, callback)` - Recursively processes a directory tree
394
621
  - `getDirectorySize(path)` - Calculates total directory size
395
622
  - `emptyDirectory(path)` - Removes all contents from directory
396
623
 
@@ -430,6 +657,9 @@ Configurable rate limiting with development mode detection for appropriate thres
430
657
  ### HTTPS Support
431
658
  Full SSL/TLS support with SNI for multi-domain hosting and automatic certificate management.
432
659
 
660
+ ### HTTP/2 CDN Security
661
+ The HTTP/2 CDN server includes security headers, CORS validation with pattern matching, and query string stripping to prevent cache poisoning.
662
+
433
663
  ### Input Validation
434
664
  Built-in validators for common input types including email, username, strong passwords, alphanumeric strings, and IP addresses.
435
665
 
package/index.js CHANGED
@@ -8,10 +8,18 @@ const { FileHandler } = require('./lib/file-handler');
8
8
  const { AppServerFactory } = require('./lib/app-server-factory');
9
9
  const { UploaderFactory } = require('./lib/uploader-factory');
10
10
  const { Encryptor } = require('./lib/encryptor');
11
+ const { Http2CdnServer } = require('./lib/http2-cdn-server');
12
+ const { ServerDefaultConfigurations } = require('./lib/server-default-configurations');
13
+ const { ServerFactoryUtils } = require('./lib/server-factory-utils');
14
+ const { ServerHeaders } = require('./lib/server-headers');
11
15
 
12
16
  module.exports = {
13
17
  FileHandler,
14
18
  AppServerFactory,
15
19
  UploaderFactory,
16
- Encryptor
20
+ Encryptor,
21
+ Http2CdnServer,
22
+ ServerDefaultConfigurations,
23
+ ServerFactoryUtils,
24
+ ServerHeaders
17
25
  };
@@ -0,0 +1,132 @@
1
+ /**
2
+ *
3
+ * Reldens - ReverseProxyConfigurer
4
+ *
5
+ */
6
+
7
+ const { createProxyMiddleware } = require('http-proxy-middleware');
8
+ const { ServerHeaders } = require('../server-headers');
9
+
10
+ class ReverseProxyConfigurer
11
+ {
12
+
13
+ constructor()
14
+ {
15
+ this.isDevelopmentMode = false;
16
+ this.useVirtualHosts = false;
17
+ this.reverseProxyRules = [];
18
+ this.reverseProxyOptions = {
19
+ changeOrigin: true,
20
+ ws: true,
21
+ secure: false,
22
+ logLevel: 'warn'
23
+ };
24
+ this.serverHeaders = new ServerHeaders();
25
+ }
26
+
27
+ setup(app, config)
28
+ {
29
+ this.isDevelopmentMode = config.isDevelopmentMode || false;
30
+ this.useVirtualHosts = config.useVirtualHosts || false;
31
+ this.reverseProxyRules = config.reverseProxyRules || [];
32
+ this.reverseProxyOptions = config.reverseProxyOptions || this.reverseProxyOptions;
33
+ if(0 === this.reverseProxyRules.length){
34
+ return;
35
+ }
36
+ this.applyRules(app);
37
+ }
38
+
39
+ applyRules(app)
40
+ {
41
+ for(let rule of this.reverseProxyRules){
42
+ if(!this.validateProxyRule(rule)){
43
+ continue;
44
+ }
45
+ let proxyMiddleware = this.createProxyMiddleware(rule);
46
+ if(!proxyMiddleware){
47
+ continue;
48
+ }
49
+ let pathPrefix = rule.pathPrefix || '/';
50
+ if(this.useVirtualHosts){
51
+ app.use(pathPrefix, (req, res, next) => {
52
+ let hostname = this.extractHostname(req);
53
+ if(hostname !== rule.hostname){
54
+ return next();
55
+ }
56
+ return proxyMiddleware(req, res, next);
57
+ });
58
+ continue;
59
+ }
60
+ app.use(pathPrefix, proxyMiddleware);
61
+ }
62
+ }
63
+
64
+ extractHostname(req)
65
+ {
66
+ if(req.domain && req.domain.hostname){
67
+ return req.domain.hostname;
68
+ }
69
+ let host = req.get('host') || '';
70
+ return host.split(':')[0].toLowerCase();
71
+ }
72
+
73
+ validateProxyRule(rule)
74
+ {
75
+ if(!rule){
76
+ return false;
77
+ }
78
+ if(!rule.hostname){
79
+ return false;
80
+ }
81
+ if(!rule.target){
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+
87
+ createProxyMiddleware(rule)
88
+ {
89
+ let options = Object.assign({}, this.reverseProxyOptions);
90
+ if('boolean' === typeof rule.changeOrigin){
91
+ options.changeOrigin = rule.changeOrigin;
92
+ }
93
+ if('boolean' === typeof rule.websocket){
94
+ options.ws = rule.websocket;
95
+ }
96
+ if('boolean' === typeof rule.secure){
97
+ options.secure = rule.secure;
98
+ }
99
+ if('string' === typeof rule.logLevel){
100
+ options.logLevel = rule.logLevel;
101
+ }
102
+ options.target = rule.target;
103
+ options.onError = this.handleProxyError.bind(this);
104
+ options.onProxyReq = (proxyReq, req) => {
105
+ if(req.headers['x-forwarded-for']){
106
+ return;
107
+ }
108
+ let clientIp = req.ip || req.connection.remoteAddress || '';
109
+ proxyReq.setHeader(this.serverHeaders.proxyForwardedFor, clientIp);
110
+ proxyReq.setHeader(this.serverHeaders.proxyForwardedProto, req.protocol);
111
+ proxyReq.setHeader(this.serverHeaders.proxyForwardedHost, req.get('host') || '');
112
+ };
113
+ return createProxyMiddleware(options);
114
+ }
115
+
116
+ handleProxyError(err, req, res)
117
+ {
118
+ if(res.headersSent){
119
+ return;
120
+ }
121
+ if('ECONNREFUSED' === err.code){
122
+ return res.status(502).send('Bad Gateway - Backend server unavailable');
123
+ }
124
+ if('ETIMEDOUT' === err.code || 'ESOCKETTIMEDOUT' === err.code){
125
+ return res.status(504).send('Gateway Timeout');
126
+ }
127
+ return res.status(500).send('Proxy Error');
128
+ }
129
+
130
+ }
131
+
132
+ module.exports.ReverseProxyConfigurer = ReverseProxyConfigurer;
@@ -5,10 +5,15 @@
5
5
  */
6
6
 
7
7
  const { FileHandler } = require('./file-handler');
8
+ const { Http2CdnServer } = require('./http2-cdn-server');
9
+ const { ServerDefaultConfigurations } = require('./server-default-configurations');
10
+ const { ServerFactoryUtils } = require('./server-factory-utils');
11
+ const { ServerHeaders } = require('./server-headers');
8
12
  const { DevelopmentModeDetector } = require('./app-server-factory/development-mode-detector');
9
13
  const { ProtocolEnforcer } = require('./app-server-factory/protocol-enforcer');
10
14
  const { SecurityConfigurer } = require('./app-server-factory/security-configurer');
11
15
  const { CorsConfigurer } = require('./app-server-factory/cors-configurer');
16
+ const { ReverseProxyConfigurer } = require('./app-server-factory/reverse-proxy-configurer');
12
17
  const { RateLimitConfigurer } = require('./app-server-factory/rate-limit-configurer');
13
18
  const http = require('http');
14
19
  const https = require('https');
@@ -61,21 +66,8 @@ class AppServerFactory
61
66
  this.defaultDomain = '';
62
67
  this.maxRequestSize = '10mb';
63
68
  this.sanitizeOptions = {allowedTags: [], allowedAttributes: {}};
64
- this.cacheConfig = {
65
- '.css': 31536000,
66
- '.js': 31536000,
67
- '.woff': 31536000,
68
- '.woff2': 31536000,
69
- '.ttf': 31536000,
70
- '.eot': 31536000,
71
- '.jpg': 2592000,
72
- '.jpeg': 2592000,
73
- '.png': 2592000,
74
- '.gif': 2592000,
75
- '.webp': 2592000,
76
- '.svg': 2592000,
77
- '.ico': 2592000
78
- };
69
+ this.cacheConfig = ServerDefaultConfigurations.cacheConfig;
70
+ this.serverHeaders = new ServerHeaders();
79
71
  this.staticOptions = {
80
72
  maxAge: '1d',
81
73
  immutable: false,
@@ -98,6 +90,7 @@ class AppServerFactory
98
90
  this.securityConfigurer = new SecurityConfigurer();
99
91
  this.corsConfigurer = new CorsConfigurer();
100
92
  this.rateLimitConfigurer = new RateLimitConfigurer();
93
+ this.reverseProxyConfigurer = new ReverseProxyConfigurer();
101
94
  this.useCompression = true;
102
95
  this.compressionOptions = {
103
96
  level: 6,
@@ -109,28 +102,32 @@ class AppServerFactory
109
102
  return compression.filter(req, res);
110
103
  }
111
104
  };
105
+ this.http2CdnEnabled = false;
106
+ this.http2CdnPort = 8443;
107
+ this.http2CdnKeyPath = '';
108
+ this.http2CdnCertPath = '';
109
+ this.http2CdnHttpsChain = '';
110
+ this.http2CdnStaticPaths = [];
111
+ this.http2CdnCorsOrigins = [];
112
+ this.http2CdnCorsAllowAll = false;
113
+ this.http2CdnMimeTypes = {};
114
+ this.http2CdnCacheConfig = {};
115
+ this.http2CdnServer = false;
116
+ this.reverseProxyEnabled = false;
117
+ this.reverseProxyRules = [];
112
118
  }
113
119
 
114
120
  buildStaticHeaders(res, path)
115
121
  {
116
- res.set('X-Content-Type-Options', 'nosniff');
117
- res.set('X-Frame-Options', 'DENY');
118
- res.set('Vary', 'Accept-Encoding');
119
- let cacheMaxAge = this.getCacheConfigForPath(path);
120
- if(cacheMaxAge){
121
- res.set('Cache-Control', 'public, max-age='+cacheMaxAge+', immutable');
122
+ let securityHeaderKeys = Object.keys(this.serverHeaders.expressSecurityHeaders);
123
+ for(let headerKey of securityHeaderKeys){
124
+ res.set(headerKey, this.serverHeaders.expressSecurityHeaders[headerKey]);
122
125
  }
123
- }
124
-
125
- getCacheConfigForPath(path)
126
- {
127
- let cacheKeys = Object.keys(this.cacheConfig);
128
- for(let ext of cacheKeys){
129
- if(path.endsWith(ext)){
130
- return this.cacheConfig[ext];
131
- }
126
+ res.set('Vary', this.serverHeaders.expressVaryHeader);
127
+ let cacheMaxAge = ServerFactoryUtils.getCacheConfigForPath(path, this.cacheConfig);
128
+ if(cacheMaxAge){
129
+ res.set('Cache-Control', this.serverHeaders.buildCacheControlHeader(cacheMaxAge));
132
130
  }
133
- return false;
134
131
  }
135
132
 
136
133
  createAppServer(appServerConfig)
@@ -148,6 +145,7 @@ class AppServerFactory
148
145
  this.setupCors();
149
146
  this.setupRateLimiting();
150
147
  this.setupRequestParsing();
148
+ this.setupReverseProxy();
151
149
  this.setupTrustedProxy();
152
150
  try {
153
151
  this.appServer = this.createServer();
@@ -157,14 +155,48 @@ class AppServerFactory
157
155
  }
158
156
  if(!this.appServer){
159
157
  if(!this.error.message){
160
- this.error = {message: 'The createServer() returned false - check certificate paths and permissions'};
158
+ this.error = {message: 'The createServer() returned false - check certificate paths and permissions.'};
161
159
  }
162
160
  return false;
163
161
  }
162
+ if(this.http2CdnEnabled){
163
+ if(!this.createHttp2CdnServer()){
164
+ this.error = {message: 'The createHttp2CdnServer() returned false.'};
165
+ return false;
166
+ }
167
+ }
164
168
  if(this.autoListen){
165
169
  this.listen();
166
170
  }
167
- return {app: this.app, appServer: this.appServer};
171
+ return {app: this.app, appServer: this.appServer, http2CdnServer: this.http2CdnServer};
172
+ }
173
+
174
+ createHttp2CdnServer()
175
+ {
176
+ this.http2CdnServer = new Http2CdnServer();
177
+ this.http2CdnServer.enabled = this.http2CdnEnabled;
178
+ this.http2CdnServer.port = this.http2CdnPort;
179
+ this.http2CdnServer.keyPath = this.http2CdnKeyPath || this.keyPath;
180
+ this.http2CdnServer.certPath = this.http2CdnCertPath || this.certPath;
181
+ this.http2CdnServer.httpsChain = this.http2CdnHttpsChain || this.httpsChain;
182
+ this.http2CdnServer.staticPaths = this.http2CdnStaticPaths;
183
+ this.http2CdnServer.cacheConfig = this.http2CdnCacheConfig && 0 < Object.keys(this.http2CdnCacheConfig).length
184
+ ? this.http2CdnCacheConfig
185
+ : this.cacheConfig;
186
+ if(this.http2CdnMimeTypes && 0 < Object.keys(this.http2CdnMimeTypes).length){
187
+ this.http2CdnServer.mimeTypes = this.http2CdnMimeTypes;
188
+ }
189
+ this.http2CdnServer.corsOrigins = this.http2CdnCorsOrigins;
190
+ this.http2CdnServer.corsAllowAll = this.http2CdnCorsAllowAll;
191
+ if(!this.http2CdnServer.create()){
192
+ this.error = this.http2CdnServer.error;
193
+ return false;
194
+ }
195
+ if(!this.http2CdnServer.listen()){
196
+ this.error = this.http2CdnServer.error;
197
+ return false;
198
+ }
199
+ return true;
168
200
  }
169
201
 
170
202
  extractDomainFromHttpUrl(url)
@@ -209,12 +241,14 @@ class AppServerFactory
209
241
  }
210
242
  this.staticOptions.immutable = false;
211
243
  this.staticOptions.setHeaders = (res, path) => {
212
- res.set('X-Content-Type-Options', 'nosniff');
213
- res.set('X-Frame-Options', 'SAMEORIGIN');
214
- res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
215
- res.set('Pragma', 'no-cache');
216
- res.set('Expires', '0');
217
- res.set('Vary', 'Accept-Encoding');
244
+ let securityHeaderKeys = Object.keys(this.serverHeaders.expressSecurityHeaders);
245
+ for(let headerKey of securityHeaderKeys){
246
+ res.set(headerKey, this.serverHeaders.expressSecurityHeaders[headerKey]);
247
+ }
248
+ res.set('Cache-Control', this.serverHeaders.expressCacheControlNoCache);
249
+ res.set('Pragma', this.serverHeaders.expressPragma);
250
+ res.set('Expires', this.serverHeaders.expressExpires);
251
+ res.set('Vary', this.serverHeaders.expressVaryHeader);
218
252
  };
219
253
  }
220
254
 
@@ -308,6 +342,18 @@ class AppServerFactory
308
342
  }
309
343
  }
310
344
 
345
+ setupReverseProxy()
346
+ {
347
+ if(!this.reverseProxyEnabled || 0 === this.reverseProxyRules.length){
348
+ return;
349
+ }
350
+ this.reverseProxyConfigurer.setup(this.app, {
351
+ reverseProxyRules: this.reverseProxyRules,
352
+ isDevelopmentMode: this.isDevelopmentMode,
353
+ useVirtualHosts: this.useVirtualHosts
354
+ });
355
+ }
356
+
311
357
  setupTrustedProxy()
312
358
  {
313
359
  if('' !== this.trustedProxy){
@@ -545,6 +591,9 @@ class AppServerFactory
545
591
 
546
592
  async close()
547
593
  {
594
+ if(this.http2CdnServer){
595
+ await this.http2CdnServer.close();
596
+ }
548
597
  if(!this.appServer){
549
598
  return true;
550
599
  }
@@ -0,0 +1,161 @@
1
+ /**
2
+ *
3
+ * Reldens - Http2CdnServer
4
+ *
5
+ */
6
+
7
+ const http2 = require('http2');
8
+ const path = require('path');
9
+ const { FileHandler } = require('./file-handler');
10
+ const { ServerDefaultConfigurations } = require('./server-default-configurations');
11
+ const { ServerFactoryUtils } = require('./server-factory-utils');
12
+ const { ServerHeaders } = require('./server-headers');
13
+
14
+ class Http2CdnServer
15
+ {
16
+
17
+ constructor()
18
+ {
19
+ this.enabled = false;
20
+ this.port = 8443;
21
+ this.keyPath = '';
22
+ this.certPath = '';
23
+ this.httpsChain = '';
24
+ this.staticPaths = [];
25
+ this.cacheConfig = ServerDefaultConfigurations.cacheConfig;
26
+ this.allowHTTP1 = true;
27
+ this.http2Server = false;
28
+ this.error = {};
29
+ this.mimeTypes = ServerDefaultConfigurations.mimeTypes;
30
+ this.corsOrigins = [];
31
+ this.corsAllowAll = false;
32
+ this.serverHeaders = new ServerHeaders();
33
+ this.corsMethods = this.serverHeaders.http2CorsMethods;
34
+ this.corsHeaders = this.serverHeaders.http2CorsHeaders;
35
+ this.securityHeaders = this.serverHeaders.http2SecurityHeaders;
36
+ this.varyHeader = this.serverHeaders.http2VaryHeader;
37
+ }
38
+
39
+ create()
40
+ {
41
+ if(!this.keyPath || !this.certPath){
42
+ this.error = {message: 'Missing SSL certificates'};
43
+ return false;
44
+ }
45
+ let key = FileHandler.readFile(this.keyPath);
46
+ if(!key){
47
+ this.error = {message: 'Could not read key from: '+this.keyPath};
48
+ return false;
49
+ }
50
+ let cert = FileHandler.readFile(this.certPath);
51
+ if(!cert){
52
+ this.error = {message: 'Could not read cert from: '+this.certPath};
53
+ return false;
54
+ }
55
+ let options = {key, cert, allowHTTP1: this.allowHTTP1};
56
+ if(this.httpsChain){
57
+ let ca = FileHandler.readFile(this.httpsChain);
58
+ if(ca){
59
+ options.ca = ca;
60
+ }
61
+ }
62
+ this.http2Server = http2.createSecureServer(options);
63
+ this.setupStreamHandler();
64
+ return true;
65
+ }
66
+
67
+ setupStreamHandler()
68
+ {
69
+ this.http2Server.on('stream', (stream, headers) => {
70
+ this.handleStream(stream, headers);
71
+ });
72
+ }
73
+
74
+ handleStream(stream, headers)
75
+ {
76
+ let requestPath = headers[':path'];
77
+ if(!requestPath){
78
+ stream.respond({':status': 400});
79
+ stream.end();
80
+ return;
81
+ }
82
+ let requestOrigin = headers['origin'] || '';
83
+ let allowedOrigin = ServerFactoryUtils.validateOrigin(requestOrigin, this.corsOrigins, this.corsAllowAll);
84
+ if('OPTIONS' === headers[':method']){
85
+ let optionsHeaders = {':status': 200};
86
+ if(allowedOrigin){
87
+ optionsHeaders['access-control-allow-origin'] = allowedOrigin;
88
+ }
89
+ optionsHeaders['access-control-allow-methods'] = this.corsMethods;
90
+ optionsHeaders['access-control-allow-headers'] = this.corsHeaders;
91
+ stream.respond(optionsHeaders);
92
+ stream.end();
93
+ return;
94
+ }
95
+ let filePath = this.resolveFilePath(requestPath);
96
+ if(!filePath){
97
+ stream.respond({':status': 404});
98
+ stream.end();
99
+ return;
100
+ }
101
+ let ext = path.extname(filePath);
102
+ let cacheAge = ServerFactoryUtils.getCacheConfigForPath(filePath, this.cacheConfig);
103
+ let responseHeaders = {':status': 200};
104
+ let securityKeys = Object.keys(this.securityHeaders);
105
+ for(let headerKey of securityKeys){
106
+ responseHeaders[headerKey] = this.securityHeaders[headerKey];
107
+ }
108
+ if(allowedOrigin){
109
+ responseHeaders['access-control-allow-origin'] = allowedOrigin;
110
+ }
111
+ if(this.varyHeader){
112
+ responseHeaders['vary'] = this.varyHeader;
113
+ }
114
+ if(cacheAge){
115
+ responseHeaders['cache-control'] = 'public, max-age='+cacheAge+', immutable';
116
+ }
117
+ if(this.mimeTypes[ext]){
118
+ responseHeaders['content-type'] = this.mimeTypes[ext];
119
+ }
120
+ stream.respondWithFile(filePath, responseHeaders);
121
+ }
122
+
123
+ resolveFilePath(requestPath)
124
+ {
125
+ let cleanPath = ServerFactoryUtils.stripQueryString(requestPath);
126
+ for(let staticPath of this.staticPaths){
127
+ let fullPath = path.join(staticPath, cleanPath);
128
+ if(!FileHandler.exists(fullPath)){
129
+ continue;
130
+ }
131
+ if(!FileHandler.isFile(fullPath)){
132
+ continue;
133
+ }
134
+ return fullPath;
135
+ }
136
+ return false;
137
+ }
138
+
139
+ listen()
140
+ {
141
+ if(!this.http2Server){
142
+ this.error = {message: 'HTTP2 server not created'};
143
+ return false;
144
+ }
145
+ this.http2Server.listen(this.port);
146
+ return true;
147
+ }
148
+
149
+ async close()
150
+ {
151
+ if(!this.http2Server){
152
+ return true;
153
+ }
154
+ return new Promise((resolve) => {
155
+ this.http2Server.close(() => resolve(true));
156
+ });
157
+ }
158
+
159
+ }
160
+
161
+ module.exports.Http2CdnServer = Http2CdnServer;
@@ -0,0 +1,57 @@
1
+ /**
2
+ *
3
+ * Reldens - ServerDefaultConfigurations
4
+ *
5
+ */
6
+
7
+ class ServerDefaultConfigurations
8
+ {
9
+
10
+ static get mimeTypes()
11
+ {
12
+ return {
13
+ '.html': 'text/html',
14
+ '.css': 'text/css',
15
+ '.js': 'application/javascript',
16
+ '.json': 'application/json',
17
+ '.xml': 'application/xml',
18
+ '.txt': 'text/plain',
19
+ '.jpg': 'image/jpeg',
20
+ '.jpeg': 'image/jpeg',
21
+ '.png': 'image/png',
22
+ '.gif': 'image/gif',
23
+ '.webp': 'image/webp',
24
+ '.svg': 'image/svg+xml',
25
+ '.ico': 'image/x-icon',
26
+ '.woff': 'font/woff',
27
+ '.woff2': 'font/woff2',
28
+ '.ttf': 'font/ttf',
29
+ '.eot': 'application/vnd.ms-fontobject',
30
+ '.webmanifest': 'application/manifest+json'
31
+ };
32
+ }
33
+
34
+ static get cacheConfig()
35
+ {
36
+ return {
37
+ '.css': 31536000,
38
+ '.js': 31536000,
39
+ '.woff': 31536000,
40
+ '.woff2': 31536000,
41
+ '.ttf': 31536000,
42
+ '.eot': 31536000,
43
+ '.jpg': 2592000,
44
+ '.jpeg': 2592000,
45
+ '.png': 2592000,
46
+ '.gif': 2592000,
47
+ '.webp': 2592000,
48
+ '.svg': 2592000,
49
+ '.ico': 2592000,
50
+ '.xml': 31536000,
51
+ '.webmanifest': 31536000
52
+ };
53
+ }
54
+
55
+ }
56
+
57
+ module.exports.ServerDefaultConfigurations = ServerDefaultConfigurations;
@@ -0,0 +1,56 @@
1
+ /**
2
+ *
3
+ * Reldens - ServerFactoryUtils
4
+ *
5
+ */
6
+
7
+ class ServerFactoryUtils
8
+ {
9
+
10
+ static getCacheConfigForPath(path, cacheConfig)
11
+ {
12
+ if(!cacheConfig){
13
+ return false;
14
+ }
15
+ let cacheKeys = Object.keys(cacheConfig);
16
+ for(let ext of cacheKeys){
17
+ if(path.endsWith(ext)){
18
+ return cacheConfig[ext];
19
+ }
20
+ }
21
+ return false;
22
+ }
23
+
24
+ static validateOrigin(origin, corsOrigins, corsAllowAll)
25
+ {
26
+ if(corsAllowAll){
27
+ return '*';
28
+ }
29
+ if(!origin){
30
+ return false;
31
+ }
32
+ if(0 === corsOrigins.length){
33
+ return false;
34
+ }
35
+ for(let allowedOrigin of corsOrigins){
36
+ if('string' === typeof allowedOrigin && origin === allowedOrigin){
37
+ return origin;
38
+ }
39
+ if(allowedOrigin instanceof RegExp && allowedOrigin.test(origin)){
40
+ return origin;
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+
46
+ static stripQueryString(url)
47
+ {
48
+ if(!url){
49
+ return '';
50
+ }
51
+ return url.split('?')[0];
52
+ }
53
+
54
+ }
55
+
56
+ module.exports.ServerFactoryUtils = ServerFactoryUtils;
@@ -0,0 +1,40 @@
1
+ /**
2
+ *
3
+ * Reldens - ServerHeaders
4
+ *
5
+ */
6
+
7
+ class ServerHeaders
8
+ {
9
+
10
+ constructor()
11
+ {
12
+ this.http2SecurityHeaders = {
13
+ 'x-content-type-options': 'nosniff',
14
+ 'x-frame-options': 'DENY'
15
+ };
16
+ this.http2VaryHeader = 'Accept-Encoding, Origin';
17
+ this.http2CorsMethods = 'GET, OPTIONS';
18
+ this.http2CorsHeaders = 'Content-Type';
19
+ this.expressSecurityHeaders = {
20
+ 'X-Content-Type-Options': 'nosniff',
21
+ 'X-Frame-Options': 'DENY'
22
+ };
23
+ this.expressVaryHeader = 'Accept-Encoding';
24
+ this.expressCacheControlNoCache = 'no-cache, no-store, must-revalidate';
25
+ this.expressCacheControlPublic = 'public, max-age={maxAge}, immutable';
26
+ this.expressPragma = 'no-cache';
27
+ this.expressExpires = '0';
28
+ this.proxyForwardedFor = 'X-Forwarded-For';
29
+ this.proxyForwardedProto = 'X-Forwarded-Proto';
30
+ this.proxyForwardedHost = 'X-Forwarded-Host';
31
+ }
32
+
33
+ buildCacheControlHeader(maxAge)
34
+ {
35
+ return this.expressCacheControlPublic.replace('{maxAge}', maxAge);
36
+ }
37
+
38
+ }
39
+
40
+ module.exports.ServerHeaders = ServerHeaders;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/server-utils",
3
3
  "scope": "@reldens",
4
- "version": "0.37.0",
4
+ "version": "0.38.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -43,6 +43,7 @@
43
43
  "express-rate-limit": "8.1.0",
44
44
  "express-session": "1.18.2",
45
45
  "helmet": "8.1.0",
46
+ "http-proxy-middleware": "^3.0.5",
46
47
  "multer": "2.0.2",
47
48
  "sanitize-html": "2.17.0"
48
49
  }