@reldens/server-utils 0.41.0 → 0.43.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/CLAUDE.md +31 -4
- package/README.md +81 -2
- package/lib/app-server-factory/reverse-proxy-configurer.js +16 -1
- package/lib/app-server-factory.js +37 -1
- package/lib/file-handler.js +13 -0
- package/lib/http2-cdn-server.js +184 -45
- package/lib/server-error-handler.js +25 -0
- package/package.json +2 -2
package/CLAUDE.md
CHANGED
|
@@ -67,6 +67,8 @@ npm test
|
|
|
67
67
|
- `appendToFile(filePath, content)` - Append to file
|
|
68
68
|
- `prependToFile(filePath, content)` - Prepend to file
|
|
69
69
|
- `replaceInFile(filePath, searchValue, replaceValue)` - Replace in file
|
|
70
|
+
- `createReadStream(filePath, options)` - Create readable stream for file
|
|
71
|
+
- `getFileModificationTime(filePath)` - Get file last modification time
|
|
70
72
|
- All methods include built-in error handling, no need for try/catch
|
|
71
73
|
- DO NOT use FileHandler.exists to validate other FileHandler methods
|
|
72
74
|
- DO NOT enclose FileHandler methods in try/catch blocks
|
|
@@ -79,6 +81,7 @@ npm test
|
|
|
79
81
|
- HTTP/2 CDN server integration
|
|
80
82
|
- Reverse proxy with WebSocket support
|
|
81
83
|
- Development mode detection and configuration
|
|
84
|
+
- Custom error handling via onError callback
|
|
82
85
|
- Security configurers (modular):
|
|
83
86
|
- `DevelopmentModeDetector` - Auto-detect development environment
|
|
84
87
|
- `ProtocolEnforcer` - HTTP/HTTPS protocol enforcement
|
|
@@ -86,6 +89,8 @@ npm test
|
|
|
86
89
|
- `CorsConfigurer` - CORS with dynamic origin validation
|
|
87
90
|
- `RateLimitConfigurer` - Global and endpoint-specific rate limiting
|
|
88
91
|
- `ReverseProxyConfigurer` - Domain-based reverse proxy
|
|
92
|
+
- Configuration properties:
|
|
93
|
+
- `onError` - Custom error handler callback for server errors
|
|
89
94
|
- Methods:
|
|
90
95
|
- `createAppServer(config)` - Create and configure server
|
|
91
96
|
- `addDomain(domainConfig)` - Add virtual host domain
|
|
@@ -146,15 +151,30 @@ npm test
|
|
|
146
151
|
- HTTP/1.1 fallback support
|
|
147
152
|
- Security headers (X-Content-Type-Options, X-Frame-Options, Vary)
|
|
148
153
|
- Query string stripping for cache optimization
|
|
154
|
+
- Comprehensive error handling (server, TLS, session, stream errors)
|
|
155
|
+
- Custom error handler callback support
|
|
156
|
+
- Configuration properties:
|
|
157
|
+
- `onError` - Custom error handler callback for server errors
|
|
149
158
|
- Methods:
|
|
150
159
|
- `create()` - Create HTTP/2 secure server
|
|
151
160
|
- `listen()` - Start listening on configured port
|
|
152
161
|
- `close()` - Gracefully close server
|
|
153
162
|
- `handleStream(stream, headers)` - Handle HTTP/2 stream
|
|
163
|
+
- `handleHttp1Request(req, res)` - Handle HTTP/1.1 fallback requests
|
|
154
164
|
- `resolveFilePath(requestPath)` - Resolve file path from request
|
|
165
|
+
- `setupEventHandlers()` - Configure server error event handlers
|
|
155
166
|
|
|
156
167
|
### Utility Classes
|
|
157
168
|
|
|
169
|
+
**ServerErrorHandler** (`lib/server-error-handler.js`):
|
|
170
|
+
- Centralized error handling for all server components
|
|
171
|
+
- Static class with standardized error handling interface
|
|
172
|
+
- Methods:
|
|
173
|
+
- `handleError(onErrorCallback, instanceName, instance, key, error, context)` - Handle and delegate errors
|
|
174
|
+
- Used by Http2CdnServer, AppServerFactory, and ReverseProxyConfigurer
|
|
175
|
+
- Supports custom error callbacks for application-specific error processing
|
|
176
|
+
- Provides structured error context with instance details and metadata
|
|
177
|
+
|
|
158
178
|
**ServerDefaultConfigurations** (`lib/server-default-configurations.js`):
|
|
159
179
|
- Static class providing default configurations
|
|
160
180
|
- `mimeTypes` - Comprehensive MIME type mappings for common file extensions
|
|
@@ -220,10 +240,17 @@ npm test
|
|
|
220
240
|
- SSL termination
|
|
221
241
|
- Header preservation (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host)
|
|
222
242
|
- Virtual host integration
|
|
223
|
-
-
|
|
224
|
-
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
243
|
+
- Comprehensive error handling with proper HTTP status codes:
|
|
244
|
+
- 502 Bad Gateway for ECONNREFUSED errors
|
|
245
|
+
- 504 Gateway Timeout for ETIMEDOUT/ESOCKETTIMEDOUT errors
|
|
246
|
+
- 500 Internal Server Error for other proxy errors
|
|
247
|
+
- Custom error callback support via ServerErrorHandler
|
|
248
|
+
- Methods:
|
|
249
|
+
- `setup(app, config)` - Setup reverse proxy
|
|
250
|
+
- `createProxyMiddleware(rule)` - Create proxy middleware for rule
|
|
251
|
+
- `handleProxyError(err, req, res)` - Handle proxy errors with status codes
|
|
252
|
+
- `validateProxyRule(rule)` - Validate proxy rule configuration
|
|
253
|
+
- `extractHostname(req)` - Extract hostname from request
|
|
227
254
|
|
|
228
255
|
## Important Notes
|
|
229
256
|
|
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ A Node.js server toolkit providing secure application server creation, HTTP/2 CD
|
|
|
25
25
|
- Static file serving with security headers
|
|
26
26
|
- Compression middleware with smart filtering
|
|
27
27
|
- Input validation utilities
|
|
28
|
+
- Custom error handling with callback support
|
|
28
29
|
|
|
29
30
|
### Http2CdnServer
|
|
30
31
|
- Dedicated HTTP/2 server for static asset delivery
|
|
@@ -37,6 +38,8 @@ A Node.js server toolkit providing secure application server creation, HTTP/2 CD
|
|
|
37
38
|
- Standalone or integrated with AppServerFactory
|
|
38
39
|
- Multiple static path support
|
|
39
40
|
- Separate SSL certificate support from main server
|
|
41
|
+
- Comprehensive error handling (server, TLS, session, stream errors)
|
|
42
|
+
- Custom error handler callback support
|
|
40
43
|
|
|
41
44
|
#### Modular Security Components
|
|
42
45
|
The AppServerFactory now uses specialized security configurers:
|
|
@@ -47,6 +50,7 @@ The AppServerFactory now uses specialized security configurers:
|
|
|
47
50
|
- **RateLimitConfigurer** - Global and endpoint-specific rate limiting
|
|
48
51
|
- **ReverseProxyConfigurer** - Domain-based reverse proxy with WebSocket support
|
|
49
52
|
- **SecurityConfigurer** - Helmet integration with CSP management and XSS protection
|
|
53
|
+
- **ServerErrorHandler** - Centralized error handling with custom callback support
|
|
50
54
|
|
|
51
55
|
### FileHandler
|
|
52
56
|
- Secure file system operations with path validation
|
|
@@ -63,6 +67,8 @@ The AppServerFactory now uses specialized security configurers:
|
|
|
63
67
|
- Directory walking with callback processing
|
|
64
68
|
- File comparison and relative path calculations
|
|
65
69
|
- Comprehensive error handling with detailed context
|
|
70
|
+
- Read stream creation for efficient file streaming
|
|
71
|
+
- File modification time retrieval
|
|
66
72
|
|
|
67
73
|
### Encryptor
|
|
68
74
|
- Password hashing using PBKDF2 with configurable iterations
|
|
@@ -174,6 +180,34 @@ if(cdnServer.create()){
|
|
|
174
180
|
}
|
|
175
181
|
```
|
|
176
182
|
|
|
183
|
+
### Custom Error Handling
|
|
184
|
+
|
|
185
|
+
Configure custom error handlers for server errors:
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
let appServerFactory = new AppServerFactory();
|
|
189
|
+
|
|
190
|
+
appServerFactory.onError = (errorData) => {
|
|
191
|
+
console.error('Server error:', errorData.key);
|
|
192
|
+
console.error('Error details:', errorData.error);
|
|
193
|
+
console.error('Context:', errorData);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
let serverResult = appServerFactory.createAppServer({
|
|
197
|
+
port: 443,
|
|
198
|
+
useHttps: true,
|
|
199
|
+
http2CdnEnabled: true,
|
|
200
|
+
autoListen: true
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `onError` callback receives structured error data:
|
|
205
|
+
- `instanceName` - Name of the component (e.g., 'http2CdnServer', 'appServerFactory')
|
|
206
|
+
- `instance` - Reference to the component instance
|
|
207
|
+
- `key` - Error type identifier (e.g., 'server-error', 'tls-client-error', 'proxy-error')
|
|
208
|
+
- `error` - The actual error object
|
|
209
|
+
- `...context` - Additional context data (port, path, hostname, etc.)
|
|
210
|
+
|
|
177
211
|
### HTTPS Server with Optimized Caching
|
|
178
212
|
|
|
179
213
|
```javascript
|
|
@@ -225,6 +259,18 @@ if(FileHandler.createFolder('/path/to/new/folder')){
|
|
|
225
259
|
// Generate a secure filename
|
|
226
260
|
let secureFilename = FileHandler.generateSecureFilename('user-upload.jpg');
|
|
227
261
|
console.log('Secure filename:', secureFilename);
|
|
262
|
+
|
|
263
|
+
// Create a read stream for efficient file handling
|
|
264
|
+
let stream = FileHandler.createReadStream('/path/to/large-file.txt');
|
|
265
|
+
if(stream){
|
|
266
|
+
stream.pipe(response);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get file modification time
|
|
270
|
+
let modTime = FileHandler.getFileModificationTime('/path/to/file.txt');
|
|
271
|
+
if(modTime){
|
|
272
|
+
console.log('Last modified:', modTime);
|
|
273
|
+
}
|
|
228
274
|
```
|
|
229
275
|
|
|
230
276
|
### Password Encryption
|
|
@@ -515,7 +561,10 @@ let serverResult = appServerFactory.createAppServer({
|
|
|
515
561
|
- Header preservation (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host)
|
|
516
562
|
- Virtual host integration
|
|
517
563
|
- Path-based routing
|
|
518
|
-
-
|
|
564
|
+
- Comprehensive error handling with proper HTTP status codes:
|
|
565
|
+
- 502 Bad Gateway for connection refused errors
|
|
566
|
+
- 504 Gateway Timeout for timeout errors
|
|
567
|
+
- 500 Internal Server Error for other proxy errors
|
|
519
568
|
|
|
520
569
|
#### Rule Properties
|
|
521
570
|
|
|
@@ -569,6 +618,15 @@ reverseProxyRules: [
|
|
|
569
618
|
- `reverseProxyEnabled` - Enable reverse proxy (default: false)
|
|
570
619
|
- `reverseProxyRules` - Array of proxy rules (default: [])
|
|
571
620
|
|
|
621
|
+
### AppServerFactory Error Handling
|
|
622
|
+
|
|
623
|
+
- `onError` - Custom error handler callback function receiving error data:
|
|
624
|
+
- `instanceName` - Component name (e.g., 'appServerFactory', 'http2CdnServer')
|
|
625
|
+
- `instance` - Component instance reference
|
|
626
|
+
- `key` - Error type identifier (e.g., 'server-error', 'virtual-host-error')
|
|
627
|
+
- `error` - The error object
|
|
628
|
+
- Additional context data (port, hostname, path, etc.)
|
|
629
|
+
|
|
572
630
|
### Http2CdnServer Methods
|
|
573
631
|
|
|
574
632
|
- `create()` - Creates HTTP/2 secure server
|
|
@@ -591,6 +649,7 @@ reverseProxyRules: [
|
|
|
591
649
|
- `securityHeaders` - Custom security headers
|
|
592
650
|
- `varyHeader` - Vary header value (default: 'Accept-Encoding, Origin')
|
|
593
651
|
- `mimeTypes` - MIME type mappings
|
|
652
|
+
- `onError` - Custom error handler callback
|
|
594
653
|
|
|
595
654
|
### FileHandler Methods
|
|
596
655
|
|
|
@@ -620,6 +679,8 @@ reverseProxyRules: [
|
|
|
620
679
|
- `walkDirectory(path, callback)` - Recursively processes a directory tree
|
|
621
680
|
- `getDirectorySize(path)` - Calculates total directory size
|
|
622
681
|
- `emptyDirectory(path)` - Removes all contents from directory
|
|
682
|
+
- `createReadStream(path, options)` - Creates readable stream for file
|
|
683
|
+
- `getFileModificationTime(path)` - Gets file last modification time
|
|
623
684
|
|
|
624
685
|
### Encryptor Methods
|
|
625
686
|
|
|
@@ -643,6 +704,16 @@ reverseProxyRules: [
|
|
|
643
704
|
- `validateFileContents(file, allowedType)` - Validates file content after upload
|
|
644
705
|
- `convertToRegex(key)` - Converts MIME type patterns to regex
|
|
645
706
|
|
|
707
|
+
### ServerErrorHandler Methods
|
|
708
|
+
|
|
709
|
+
- `handleError(onErrorCallback, instanceName, instance, key, error, context)` - Static method for centralized error handling
|
|
710
|
+
- `onErrorCallback` - Custom error handler function (optional)
|
|
711
|
+
- `instanceName` - Name of the component ('http2CdnServer', 'appServerFactory', etc.)
|
|
712
|
+
- `instance` - Reference to the component instance
|
|
713
|
+
- `key` - Error type identifier
|
|
714
|
+
- `error` - The error object
|
|
715
|
+
- `context` - Additional context object (port, hostname, path, etc.)
|
|
716
|
+
|
|
646
717
|
## Security Features
|
|
647
718
|
|
|
648
719
|
### Path Traversal Protection
|
|
@@ -668,7 +739,15 @@ Industry-standard encryption using PBKDF2 for passwords, AES-256-GCM for data en
|
|
|
668
739
|
|
|
669
740
|
## Error Handling
|
|
670
741
|
|
|
671
|
-
All
|
|
742
|
+
All server components include comprehensive error handling with centralized error management through the ServerErrorHandler class. Custom error handlers can be configured via the `onError` callback to process errors according to application requirements. Errors are logged appropriately and never expose sensitive system information.
|
|
743
|
+
|
|
744
|
+
Error handling features:
|
|
745
|
+
- Centralized error management with ServerErrorHandler
|
|
746
|
+
- Custom error callback support across all components
|
|
747
|
+
- Structured error context with instance details and metadata
|
|
748
|
+
- Graceful HTTP status codes for proxy errors (502, 504, 500)
|
|
749
|
+
- TLS and session error handling for HTTP/2 servers
|
|
750
|
+
- Virtual host and SNI certificate error handling
|
|
672
751
|
|
|
673
752
|
---
|
|
674
753
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
8
8
|
const { ServerHeaders } = require('../server-headers');
|
|
9
|
+
const { ServerErrorHandler } = require('../server-error-handler');
|
|
9
10
|
|
|
10
11
|
class ReverseProxyConfigurer
|
|
11
12
|
{
|
|
@@ -22,6 +23,7 @@ class ReverseProxyConfigurer
|
|
|
22
23
|
logLevel: 'warn'
|
|
23
24
|
};
|
|
24
25
|
this.serverHeaders = new ServerHeaders();
|
|
26
|
+
this.onError = null;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
setup(app, config)
|
|
@@ -30,6 +32,7 @@ class ReverseProxyConfigurer
|
|
|
30
32
|
this.useVirtualHosts = config.useVirtualHosts || false;
|
|
31
33
|
this.reverseProxyRules = config.reverseProxyRules || [];
|
|
32
34
|
this.reverseProxyOptions = config.reverseProxyOptions || this.reverseProxyOptions;
|
|
35
|
+
this.onError = config.onError || null;
|
|
33
36
|
if(0 === this.reverseProxyRules.length){
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
@@ -100,7 +103,9 @@ class ReverseProxyConfigurer
|
|
|
100
103
|
options.logLevel = rule.logLevel;
|
|
101
104
|
}
|
|
102
105
|
options.target = rule.target;
|
|
103
|
-
options.onError =
|
|
106
|
+
options.onError = (err, req, res) => {
|
|
107
|
+
this.handleProxyError(err, req, res);
|
|
108
|
+
};
|
|
104
109
|
options.onProxyReq = (proxyReq, req) => {
|
|
105
110
|
if(req.headers['x-forwarded-for']){
|
|
106
111
|
return;
|
|
@@ -115,6 +120,16 @@ class ReverseProxyConfigurer
|
|
|
115
120
|
|
|
116
121
|
handleProxyError(err, req, res)
|
|
117
122
|
{
|
|
123
|
+
let hostname = req.get('host') || 'unknown';
|
|
124
|
+
let requestPath = req.path || req.url || 'unknown';
|
|
125
|
+
ServerErrorHandler.handleError(
|
|
126
|
+
this.onError,
|
|
127
|
+
'reverseProxyConfigurer',
|
|
128
|
+
this,
|
|
129
|
+
'proxy-error',
|
|
130
|
+
err,
|
|
131
|
+
{request: req, response: res, hostname: hostname, path: requestPath}
|
|
132
|
+
);
|
|
118
133
|
if(res.headersSent){
|
|
119
134
|
return;
|
|
120
135
|
}
|
|
@@ -9,6 +9,7 @@ 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');
|
|
12
13
|
const { DevelopmentModeDetector } = require('./app-server-factory/development-mode-detector');
|
|
13
14
|
const { ProtocolEnforcer } = require('./app-server-factory/protocol-enforcer');
|
|
14
15
|
const { SecurityConfigurer } = require('./app-server-factory/security-configurer');
|
|
@@ -115,6 +116,7 @@ class AppServerFactory
|
|
|
115
116
|
this.http2CdnServer = false;
|
|
116
117
|
this.reverseProxyEnabled = false;
|
|
117
118
|
this.reverseProxyRules = [];
|
|
119
|
+
this.onError = null;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
buildStaticHeaders(res, path)
|
|
@@ -188,6 +190,7 @@ class AppServerFactory
|
|
|
188
190
|
}
|
|
189
191
|
this.http2CdnServer.corsOrigins = this.http2CdnCorsOrigins;
|
|
190
192
|
this.http2CdnServer.corsAllowAll = this.http2CdnCorsAllowAll;
|
|
193
|
+
this.http2CdnServer.onError = this.onError;
|
|
191
194
|
if(!this.http2CdnServer.create()){
|
|
192
195
|
this.error = this.http2CdnServer.error;
|
|
193
196
|
return false;
|
|
@@ -350,7 +353,8 @@ class AppServerFactory
|
|
|
350
353
|
this.reverseProxyConfigurer.setup(this.app, {
|
|
351
354
|
reverseProxyRules: this.reverseProxyRules,
|
|
352
355
|
isDevelopmentMode: this.isDevelopmentMode,
|
|
353
|
-
useVirtualHosts: this.useVirtualHosts
|
|
356
|
+
useVirtualHosts: this.useVirtualHosts,
|
|
357
|
+
onError: this.onError
|
|
354
358
|
});
|
|
355
359
|
}
|
|
356
360
|
|
|
@@ -388,6 +392,14 @@ class AppServerFactory
|
|
|
388
392
|
return next();
|
|
389
393
|
}
|
|
390
394
|
this.error = {message: 'No hostname provided and no default domain configured'};
|
|
395
|
+
ServerErrorHandler.handleError(
|
|
396
|
+
this.onError,
|
|
397
|
+
'appServerFactory',
|
|
398
|
+
this,
|
|
399
|
+
'virtual-host-no-hostname',
|
|
400
|
+
this.error,
|
|
401
|
+
{request: req, response: res}
|
|
402
|
+
);
|
|
391
403
|
return res.status(400).send('Bad Request');
|
|
392
404
|
}
|
|
393
405
|
let domain = this.findDomainConfig(hostname);
|
|
@@ -397,6 +409,14 @@ class AppServerFactory
|
|
|
397
409
|
return next();
|
|
398
410
|
}
|
|
399
411
|
this.error = {message: 'Unknown domain: '+hostname};
|
|
412
|
+
ServerErrorHandler.handleError(
|
|
413
|
+
this.onError,
|
|
414
|
+
'appServerFactory',
|
|
415
|
+
this,
|
|
416
|
+
'virtual-host-unknown-domain',
|
|
417
|
+
this.error,
|
|
418
|
+
{hostname: hostname, request: req, response: res}
|
|
419
|
+
);
|
|
400
420
|
return res.status(404).send('Domain not found');
|
|
401
421
|
}
|
|
402
422
|
req.domain = domain;
|
|
@@ -471,11 +491,27 @@ class AppServerFactory
|
|
|
471
491
|
let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
|
|
472
492
|
if(!key){
|
|
473
493
|
this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
|
|
494
|
+
ServerErrorHandler.handleError(
|
|
495
|
+
this.onError,
|
|
496
|
+
'appServerFactory',
|
|
497
|
+
this,
|
|
498
|
+
'sni-key-read-failure',
|
|
499
|
+
this.error,
|
|
500
|
+
{hostname: hostname, domain: domain, keyPath: domain.keyPath}
|
|
501
|
+
);
|
|
474
502
|
return callback(null, null);
|
|
475
503
|
}
|
|
476
504
|
let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
|
|
477
505
|
if(!cert){
|
|
478
506
|
this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
|
|
507
|
+
ServerErrorHandler.handleError(
|
|
508
|
+
this.onError,
|
|
509
|
+
'appServerFactory',
|
|
510
|
+
this,
|
|
511
|
+
'sni-cert-read-failure',
|
|
512
|
+
this.error,
|
|
513
|
+
{hostname: hostname, domain: domain, certPath: domain.certPath}
|
|
514
|
+
);
|
|
479
515
|
return callback(null, null);
|
|
480
516
|
}
|
|
481
517
|
let ctx = tls.createSecureContext({key, cert});
|
package/lib/file-handler.js
CHANGED
|
@@ -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();
|
package/lib/http2-cdn-server.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const http2 = require('http2');
|
|
8
|
-
const path = require('path');
|
|
9
8
|
const { FileHandler } = require('./file-handler');
|
|
10
9
|
const { ServerDefaultConfigurations } = require('./server-default-configurations');
|
|
11
10
|
const { ServerFactoryUtils } = require('./server-factory-utils');
|
|
12
11
|
const { ServerHeaders } = require('./server-headers');
|
|
12
|
+
const { ServerErrorHandler } = require('./server-error-handler');
|
|
13
13
|
|
|
14
14
|
class Http2CdnServer
|
|
15
15
|
{
|
|
@@ -34,6 +34,7 @@ class Http2CdnServer
|
|
|
34
34
|
this.corsHeaders = this.serverHeaders.http2CorsHeaders;
|
|
35
35
|
this.securityHeaders = this.serverHeaders.http2SecurityHeaders;
|
|
36
36
|
this.varyHeader = this.serverHeaders.http2VaryHeader;
|
|
37
|
+
this.onError = null;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
create()
|
|
@@ -60,71 +61,209 @@ class Http2CdnServer
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
this.http2Server = http2.createSecureServer(options);
|
|
63
|
-
this.
|
|
64
|
+
this.setupEventHandlers();
|
|
64
65
|
return true;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
setupEventHandlers()
|
|
68
69
|
{
|
|
69
70
|
this.http2Server.on('stream', (stream, headers) => {
|
|
70
71
|
this.handleStream(stream, headers);
|
|
71
72
|
});
|
|
73
|
+
this.http2Server.on('request', (req, res) => {
|
|
74
|
+
this.handleHttp1Request(req, res);
|
|
75
|
+
});
|
|
76
|
+
this.http2Server.on('error', (err) => {
|
|
77
|
+
ServerErrorHandler.handleError(
|
|
78
|
+
this.onError,
|
|
79
|
+
'http2CdnServer',
|
|
80
|
+
this,
|
|
81
|
+
'server-error',
|
|
82
|
+
err,
|
|
83
|
+
{port: this.port}
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
this.http2Server.on('tlsClientError', (err, tlsSocket) => {
|
|
87
|
+
ServerErrorHandler.handleError(
|
|
88
|
+
this.onError,
|
|
89
|
+
'http2CdnServer',
|
|
90
|
+
this,
|
|
91
|
+
'tls-client-error',
|
|
92
|
+
err,
|
|
93
|
+
{port: this.port, remoteAddress: tlsSocket.remoteAddress}
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
this.http2Server.on('sessionError', (err) => {
|
|
97
|
+
ServerErrorHandler.handleError(
|
|
98
|
+
this.onError,
|
|
99
|
+
'http2CdnServer',
|
|
100
|
+
this,
|
|
101
|
+
'session-error',
|
|
102
|
+
err,
|
|
103
|
+
{port: this.port}
|
|
104
|
+
);
|
|
105
|
+
});
|
|
72
106
|
}
|
|
73
107
|
|
|
74
108
|
handleStream(stream, headers)
|
|
75
109
|
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
try {
|
|
111
|
+
let requestPath = headers[':path'];
|
|
112
|
+
if(!requestPath){
|
|
113
|
+
stream.respond({':status': 400});
|
|
114
|
+
stream.end();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let requestOrigin = headers['origin'] || '';
|
|
118
|
+
let allowedOrigin = ServerFactoryUtils.validateOrigin(requestOrigin, this.corsOrigins, this.corsAllowAll);
|
|
119
|
+
if('OPTIONS' === headers[':method']){
|
|
120
|
+
let optionsHeaders = {':status': 200};
|
|
121
|
+
if(allowedOrigin){
|
|
122
|
+
optionsHeaders['access-control-allow-origin'] = allowedOrigin;
|
|
123
|
+
}
|
|
124
|
+
optionsHeaders['access-control-allow-methods'] = this.corsMethods;
|
|
125
|
+
optionsHeaders['access-control-allow-headers'] = this.corsHeaders;
|
|
126
|
+
stream.respond(optionsHeaders);
|
|
127
|
+
stream.end();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let filePath = this.resolveFilePath(requestPath);
|
|
131
|
+
if(!filePath){
|
|
132
|
+
stream.respond({':status': 404});
|
|
133
|
+
stream.end();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let ext = FileHandler.extension(filePath);
|
|
137
|
+
let cacheAge = ServerFactoryUtils.getCacheConfigForPath(filePath, this.cacheConfig);
|
|
138
|
+
let responseHeaders = {':status': 200};
|
|
139
|
+
let securityKeys = Object.keys(this.securityHeaders);
|
|
140
|
+
for(let headerKey of securityKeys){
|
|
141
|
+
responseHeaders[headerKey] = this.securityHeaders[headerKey];
|
|
142
|
+
}
|
|
86
143
|
if(allowedOrigin){
|
|
87
|
-
|
|
144
|
+
responseHeaders['access-control-allow-origin'] = allowedOrigin;
|
|
145
|
+
}
|
|
146
|
+
if(this.varyHeader){
|
|
147
|
+
responseHeaders['vary'] = this.varyHeader;
|
|
148
|
+
}
|
|
149
|
+
if(cacheAge){
|
|
150
|
+
responseHeaders['cache-control'] = 'public, max-age='+cacheAge+', immutable';
|
|
151
|
+
}
|
|
152
|
+
if(this.mimeTypes[ext]){
|
|
153
|
+
responseHeaders['content-type'] = this.mimeTypes[ext];
|
|
154
|
+
}
|
|
155
|
+
stream.on('error', (err) => {
|
|
156
|
+
ServerErrorHandler.handleError(
|
|
157
|
+
this.onError,
|
|
158
|
+
'http2CdnServer',
|
|
159
|
+
this,
|
|
160
|
+
'stream-error',
|
|
161
|
+
err,
|
|
162
|
+
{port: this.port, path: requestPath}
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
stream.respondWithFile(filePath, responseHeaders);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
ServerErrorHandler.handleError(
|
|
168
|
+
this.onError,
|
|
169
|
+
'http2CdnServer',
|
|
170
|
+
this,
|
|
171
|
+
'handle-stream-error',
|
|
172
|
+
err,
|
|
173
|
+
{port: this.port, path: headers[':path']}
|
|
174
|
+
);
|
|
175
|
+
if(!stream.destroyed){
|
|
176
|
+
stream.respond({':status': 500});
|
|
177
|
+
stream.end();
|
|
88
178
|
}
|
|
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
179
|
}
|
|
117
|
-
|
|
118
|
-
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
handleHttp1Request(req, res)
|
|
183
|
+
{
|
|
184
|
+
try {
|
|
185
|
+
let requestPath = req.url;
|
|
186
|
+
if(!requestPath){
|
|
187
|
+
res.writeHead(400);
|
|
188
|
+
res.end();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
let requestOrigin = req.headers['origin'] || '';
|
|
192
|
+
let allowedOrigin = ServerFactoryUtils.validateOrigin(requestOrigin, this.corsOrigins, this.corsAllowAll);
|
|
193
|
+
if('OPTIONS' === req.method){
|
|
194
|
+
let optionsHeaders = {};
|
|
195
|
+
if(allowedOrigin){
|
|
196
|
+
optionsHeaders['access-control-allow-origin'] = allowedOrigin;
|
|
197
|
+
}
|
|
198
|
+
optionsHeaders['access-control-allow-methods'] = this.corsMethods;
|
|
199
|
+
optionsHeaders['access-control-allow-headers'] = this.corsHeaders;
|
|
200
|
+
res.writeHead(200, optionsHeaders);
|
|
201
|
+
res.end();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let filePath = this.resolveFilePath(requestPath);
|
|
205
|
+
if(!filePath){
|
|
206
|
+
res.writeHead(404);
|
|
207
|
+
res.end();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
let ext = FileHandler.extension(filePath);
|
|
211
|
+
let cacheAge = ServerFactoryUtils.getCacheConfigForPath(filePath, this.cacheConfig);
|
|
212
|
+
let responseHeaders = {};
|
|
213
|
+
let securityKeys = Object.keys(this.securityHeaders);
|
|
214
|
+
for(let headerKey of securityKeys){
|
|
215
|
+
responseHeaders[headerKey] = this.securityHeaders[headerKey];
|
|
216
|
+
}
|
|
217
|
+
if(allowedOrigin){
|
|
218
|
+
responseHeaders['access-control-allow-origin'] = allowedOrigin;
|
|
219
|
+
}
|
|
220
|
+
if(this.varyHeader){
|
|
221
|
+
responseHeaders['vary'] = this.varyHeader;
|
|
222
|
+
}
|
|
223
|
+
if(cacheAge){
|
|
224
|
+
responseHeaders['cache-control'] = 'public, max-age='+cacheAge+', immutable';
|
|
225
|
+
}
|
|
226
|
+
if(this.mimeTypes[ext]){
|
|
227
|
+
responseHeaders['content-type'] = this.mimeTypes[ext];
|
|
228
|
+
}
|
|
229
|
+
let fileStream = FileHandler.createReadStream(filePath);
|
|
230
|
+
fileStream.on('error', (err) => {
|
|
231
|
+
ServerErrorHandler.handleError(
|
|
232
|
+
this.onError,
|
|
233
|
+
'http2CdnServer',
|
|
234
|
+
this,
|
|
235
|
+
'http1-stream-error',
|
|
236
|
+
err,
|
|
237
|
+
{port: this.port, path: requestPath}
|
|
238
|
+
);
|
|
239
|
+
if(!res.headersSent){
|
|
240
|
+
res.writeHead(500);
|
|
241
|
+
res.end();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
res.writeHead(200, responseHeaders);
|
|
245
|
+
fileStream.pipe(res);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
ServerErrorHandler.handleError(
|
|
248
|
+
this.onError,
|
|
249
|
+
'http2CdnServer',
|
|
250
|
+
this,
|
|
251
|
+
'handle-http1-request-error',
|
|
252
|
+
err,
|
|
253
|
+
{port: this.port, path: req.url}
|
|
254
|
+
);
|
|
255
|
+
if(!res.headersSent){
|
|
256
|
+
res.writeHead(500);
|
|
257
|
+
res.end();
|
|
258
|
+
}
|
|
119
259
|
}
|
|
120
|
-
stream.respondWithFile(filePath, responseHeaders);
|
|
121
260
|
}
|
|
122
261
|
|
|
123
262
|
resolveFilePath(requestPath)
|
|
124
263
|
{
|
|
125
264
|
let cleanPath = ServerFactoryUtils.stripQueryString(requestPath);
|
|
126
265
|
for(let staticPath of this.staticPaths){
|
|
127
|
-
let fullPath =
|
|
266
|
+
let fullPath = FileHandler.joinPaths(staticPath, cleanPath);
|
|
128
267
|
if(!FileHandler.exists(fullPath)){
|
|
129
268
|
continue;
|
|
130
269
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - ServerErrorHandler
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class ServerErrorHandler
|
|
8
|
+
{
|
|
9
|
+
|
|
10
|
+
static handleError(onErrorCallback, instanceName, instance, key, error, context = {})
|
|
11
|
+
{
|
|
12
|
+
if('function' !== typeof onErrorCallback){
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
onErrorCallback({
|
|
16
|
+
[instanceName]: instance,
|
|
17
|
+
key: key,
|
|
18
|
+
error: error,
|
|
19
|
+
...context
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports.ServerErrorHandler = ServerErrorHandler;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reldens/server-utils",
|
|
3
3
|
"scope": "@reldens",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.43.0",
|
|
5
5
|
"description": "Reldens - Server Utils",
|
|
6
6
|
"author": "Damian A. Pastorini",
|
|
7
7
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"body-parser": "2.2.1",
|
|
40
40
|
"compression": "1.8.1",
|
|
41
41
|
"cors": "2.8.5",
|
|
42
|
-
"express": "4.
|
|
42
|
+
"express": "4.22.1",
|
|
43
43
|
"express-rate-limit": "8.2.1",
|
|
44
44
|
"express-session": "1.18.2",
|
|
45
45
|
"helmet": "8.1.0",
|