@reldens/server-utils 0.36.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 +687 -388
- package/index.js +9 -1
- package/lib/app-server-factory/reverse-proxy-configurer.js +132 -0
- package/lib/app-server-factory/security-configurer.js +58 -22
- package/lib/app-server-factory.js +88 -39
- package/lib/http2-cdn-server.js +161 -0
- package/lib/server-default-configurations.js +57 -0
- package/lib/server-factory-utils.js +56 -0
- package/lib/server-headers.js +40 -0
- package/package.json +2 -1
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;
|
|
@@ -16,38 +16,36 @@ class SecurityConfigurer
|
|
|
16
16
|
this.useHelmet = true;
|
|
17
17
|
this.useXssProtection = true;
|
|
18
18
|
this.helmetConfig = false;
|
|
19
|
+
this.helmetOptions = {};
|
|
19
20
|
this.sanitizeOptions = {allowedTags: [], allowedAttributes: {}};
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
setupHelmet(app, config)
|
|
23
24
|
{
|
|
24
|
-
this.isDevelopmentMode = config.isDevelopmentMode || false;
|
|
25
25
|
this.useHelmet = config.useHelmet !== false;
|
|
26
|
-
this.helmetConfig = config.helmetConfig || false;
|
|
27
26
|
if(!this.useHelmet){
|
|
28
27
|
return;
|
|
29
28
|
}
|
|
30
|
-
|
|
29
|
+
app.use(helmet(this.mapHelmetOptions(config)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mapHelmetOptions(config)
|
|
33
|
+
{
|
|
34
|
+
this.isDevelopmentMode = config.isDevelopmentMode || false;
|
|
35
|
+
this.helmetConfig = config.helmetConfig || {};
|
|
36
|
+
this.helmetOptions = {
|
|
31
37
|
crossOriginEmbedderPolicy: false,
|
|
32
38
|
crossOriginOpenerPolicy: false,
|
|
33
39
|
crossOriginResourcePolicy: false,
|
|
34
40
|
originAgentCluster: false
|
|
35
|
-
}
|
|
36
|
-
if(this.helmetConfig){
|
|
37
|
-
Object.assign(helmetOptions, this.helmetConfig);
|
|
38
|
-
}
|
|
39
|
-
app.use(helmet(helmetOptions));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
setModeOptions(helmetOptions, config)
|
|
43
|
-
{
|
|
41
|
+
};
|
|
44
42
|
if(this.isDevelopmentMode){
|
|
45
|
-
helmetOptions.contentSecurityPolicy = false;
|
|
46
|
-
helmetOptions.hsts = false;
|
|
47
|
-
helmetOptions.noSniff = false;
|
|
48
|
-
return helmetOptions;
|
|
43
|
+
this.helmetOptions.contentSecurityPolicy = false;
|
|
44
|
+
this.helmetOptions.hsts = false;
|
|
45
|
+
this.helmetOptions.noSniff = false;
|
|
46
|
+
return this.helmetOptions;
|
|
49
47
|
}
|
|
50
|
-
helmetOptions.contentSecurityPolicy = {
|
|
48
|
+
this.helmetOptions.contentSecurityPolicy = {
|
|
51
49
|
directives: {
|
|
52
50
|
defaultSrc: ["'self'"],
|
|
53
51
|
scriptSrc: ["'self'"],
|
|
@@ -62,13 +60,50 @@ class SecurityConfigurer
|
|
|
62
60
|
formAction: ["'self'"]
|
|
63
61
|
}
|
|
64
62
|
};
|
|
63
|
+
if(this.helmetConfig.contentSecurityPolicy){
|
|
64
|
+
if(this.helmetConfig.contentSecurityPolicy.overrideDirectives){
|
|
65
|
+
this.helmetOptions.contentSecurityPolicy.directives = this.helmetConfig.contentSecurityPolicy.directives;
|
|
66
|
+
}
|
|
67
|
+
if(
|
|
68
|
+
!this.helmetConfig.contentSecurityPolicy.overrideDirectives
|
|
69
|
+
&& this.helmetConfig.contentSecurityPolicy.directives
|
|
70
|
+
){
|
|
71
|
+
let configDirectivesKeys = Object.keys(this.helmetConfig.contentSecurityPolicy.directives);
|
|
72
|
+
for(let directiveKey of configDirectivesKeys){
|
|
73
|
+
let directiveValues = this.helmetConfig.contentSecurityPolicy.directives[directiveKey];
|
|
74
|
+
if(!Array.isArray(directiveValues)){
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if(!this.helmetOptions.contentSecurityPolicy.directives[directiveKey]){
|
|
78
|
+
this.helmetOptions.contentSecurityPolicy.directives[directiveKey] = [];
|
|
79
|
+
}
|
|
80
|
+
for(let value of directiveValues){
|
|
81
|
+
this.helmetOptions.contentSecurityPolicy.directives[directiveKey].push(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let cspKeys = Object.keys(this.helmetConfig.contentSecurityPolicy);
|
|
86
|
+
for(let cspKey of cspKeys){
|
|
87
|
+
if('directives' === cspKey || 'overrideDirectives' === cspKey){
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
this.helmetOptions.contentSecurityPolicy[cspKey] = this.helmetConfig.contentSecurityPolicy[cspKey];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let helmetConfigKeys = Object.keys(this.helmetConfig);
|
|
94
|
+
for(let configKey of helmetConfigKeys){
|
|
95
|
+
if('contentSecurityPolicy' === configKey){
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
this.helmetOptions[configKey] = this.helmetConfig[configKey];
|
|
99
|
+
}
|
|
65
100
|
if(config.developmentExternalDomains){
|
|
66
101
|
this.addExternalDomainsToCsp(
|
|
67
|
-
helmetOptions.contentSecurityPolicy.directives,
|
|
102
|
+
this.helmetOptions.contentSecurityPolicy.directives,
|
|
68
103
|
config.developmentExternalDomains
|
|
69
104
|
);
|
|
70
105
|
}
|
|
71
|
-
return helmetOptions;
|
|
106
|
+
return this.helmetOptions;
|
|
72
107
|
}
|
|
73
108
|
|
|
74
109
|
addExternalDomainsToCsp(directives, externalDomains)
|
|
@@ -79,11 +114,12 @@ class SecurityConfigurer
|
|
|
79
114
|
if(!Array.isArray(domains)){
|
|
80
115
|
continue;
|
|
81
116
|
}
|
|
117
|
+
let camelCaseKey = directiveKey.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
|
|
82
118
|
for(let domain of domains){
|
|
83
|
-
if(directives[
|
|
84
|
-
directives[
|
|
119
|
+
if(directives[camelCaseKey]){
|
|
120
|
+
directives[camelCaseKey].push(domain);
|
|
85
121
|
}
|
|
86
|
-
let elemKey =
|
|
122
|
+
let elemKey = camelCaseKey+'Elem';
|
|
87
123
|
if(directives[elemKey]){
|
|
88
124
|
directives[elemKey].push(domain);
|
|
89
125
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
res.set('
|
|
217
|
-
res.set('
|
|
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;
|