@reldens/server-utils 0.26.0 → 0.28.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.
@@ -1,499 +1,516 @@
1
- /**
2
- *
3
- * Reldens - AppServerFactory
4
- *
5
- */
6
-
7
- const { FileHandler } = require('./file-handler');
8
- const { DevelopmentModeDetector } = require('./app-server-factory/development-mode-detector');
9
- const { ProtocolEnforcer } = require('./app-server-factory/protocol-enforcer');
10
- const { SecurityConfigurer } = require('./app-server-factory/security-configurer');
11
- const { CorsConfigurer } = require('./app-server-factory/cors-configurer');
12
- const { RateLimitConfigurer } = require('./app-server-factory/rate-limit-configurer');
13
- const http = require('http');
14
- const https = require('https');
15
- const express = require('express');
16
- const bodyParser = require('body-parser');
17
- const session = require('express-session');
18
- const compression = require('compression');
19
-
20
- class AppServerFactory
21
- {
22
-
23
- constructor()
24
- {
25
- this.applicationFramework = express;
26
- this.bodyParser = bodyParser;
27
- this.session = session;
28
- this.compression = compression;
29
- this.appServer = false;
30
- this.app = express();
31
- this.useCors = true;
32
- this.useExpressJson = true;
33
- this.useUrlencoded = true;
34
- this.encoding = 'utf-8';
35
- this.useHttps = false;
36
- this.passphrase = '';
37
- this.httpsChain = '';
38
- this.keyPath = '';
39
- this.certPath = '';
40
- this.trustedProxy = '';
41
- this.windowMs = 60000;
42
- this.maxRequests = 30;
43
- this.applyKeyGenerator = false;
44
- this.jsonLimit = '1mb';
45
- this.urlencodedLimit = '1mb';
46
- this.useHelmet = true;
47
- this.helmetConfig = false;
48
- this.useXssProtection = true;
49
- this.globalRateLimit = 0;
50
- this.corsOrigin = '*';
51
- this.corsMethods = ['GET','POST'];
52
- this.corsHeaders = ['Content-Type','Authorization'];
53
- this.tooManyRequestsMessage = 'Too many requests, please try again later.';
54
- this.error = {};
55
- this.processErrorResponse = false;
56
- this.port = 3000;
57
- this.autoListen = false;
58
- this.domains = [];
59
- this.useVirtualHosts = false;
60
- this.defaultDomain = '';
61
- this.maxRequestSize = '10mb';
62
- this.sanitizeOptions = {allowedTags: [], allowedAttributes: {}};
63
- this.staticOptions = {
64
- maxAge: '1d',
65
- etag: true,
66
- lastModified: true,
67
- index: false,
68
- setHeaders: function(res){
69
- res.set('X-Content-Type-Options', 'nosniff');
70
- res.set('X-Frame-Options', 'DENY');
71
- }
72
- };
73
- this.isDevelopmentMode = false;
74
- this.developmentDomains = [];
75
- this.domainMapping = {};
76
- this.enforceProtocol = true;
77
- this.developmentPatterns = [
78
- 'localhost',
79
- '127.0.0.1',
80
- '.local',
81
- '.test',
82
- '.dev',
83
- '.staging'
84
- ];
85
- this.developmentEnvironments = ['development', 'dev', 'test'];
86
- this.developmentPorts = [3000, 8080, 8081];
87
- this.developmentMultiplier = 10;
88
- this.developmentModeDetector = new DevelopmentModeDetector();
89
- this.protocolEnforcer = new ProtocolEnforcer();
90
- this.securityConfigurer = new SecurityConfigurer();
91
- this.corsConfigurer = new CorsConfigurer();
92
- this.rateLimitConfigurer = new RateLimitConfigurer();
93
- this.useCompression = true;
94
- this.compressionOptions = {
95
- level: 6,
96
- threshold: 1024,
97
- filter: function(req, res){
98
- if(req.headers['x-no-compression']){
99
- return false;
100
- }
101
- return compression.filter(req, res);
102
- }
103
- };
104
- }
105
-
106
- createAppServer(appServerConfig)
107
- {
108
- if(appServerConfig){
109
- Object.assign(this, appServerConfig);
110
- }
111
- this.detectDevelopmentMode();
112
- this.setupDevelopmentConfiguration();
113
- this.setupProtocolEnforcement();
114
- this.setupSecurity();
115
- this.setupCompression();
116
- this.setupVirtualHosts();
117
- this.setupCors();
118
- this.setupRateLimiting();
119
- this.setupRequestParsing();
120
- this.setupTrustedProxy();
121
- this.appServer = this.createServer();
122
- if(!this.appServer){
123
- this.error = {message: 'Failed to create app server'};
124
- return false;
125
- }
126
- if(this.autoListen){
127
- this.listen();
128
- }
129
- return {app: this.app, appServer: this.appServer};
130
- }
131
-
132
- detectDevelopmentMode()
133
- {
134
- this.isDevelopmentMode = this.developmentModeDetector.detect({
135
- developmentPatterns: this.developmentPatterns,
136
- developmentEnvironments: this.developmentEnvironments,
137
- developmentDomains: this.developmentDomains,
138
- domains: this.domains
139
- });
140
- }
141
-
142
- setupDevelopmentConfiguration()
143
- {
144
- if(!this.isDevelopmentMode){
145
- return;
146
- }
147
- this.staticOptions.setHeaders = (res, path) => {
148
- res.set('X-Content-Type-Options', 'nosniff');
149
- res.set('X-Frame-Options', 'SAMEORIGIN');
150
- res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
151
- res.set('Pragma', 'no-cache');
152
- res.set('Expires', '0');
153
- };
154
- }
155
-
156
- setupProtocolEnforcement()
157
- {
158
- this.protocolEnforcer.setup(this.app, {
159
- isDevelopmentMode: this.isDevelopmentMode,
160
- useHttps: this.useHttps,
161
- enforceProtocol: this.enforceProtocol
162
- });
163
- }
164
-
165
- setupSecurity()
166
- {
167
- this.securityConfigurer.setupHelmet(this.app, {
168
- isDevelopmentMode: this.isDevelopmentMode,
169
- useHelmet: this.useHelmet,
170
- helmetConfig: this.helmetConfig,
171
- developmentExternalDomains: this.developmentExternalDomains
172
- });
173
- this.securityConfigurer.setupXssProtection(this.app, {
174
- useXssProtection: this.useXssProtection,
175
- sanitizeOptions: this.sanitizeOptions
176
- });
177
- }
178
-
179
- setupCompression()
180
- {
181
- if(!this.useCompression){
182
- return;
183
- }
184
- this.app.use(this.compression(this.compressionOptions));
185
- }
186
-
187
- setupCors()
188
- {
189
- this.corsConfigurer.setup(this.app, {
190
- isDevelopmentMode: this.isDevelopmentMode,
191
- useCors: this.useCors,
192
- corsOrigin: this.corsOrigin,
193
- corsMethods: this.corsMethods,
194
- corsHeaders: this.corsHeaders,
195
- domainMapping: this.domainMapping,
196
- developmentPorts: this.developmentPorts
197
- });
198
- }
199
-
200
- setupRateLimiting()
201
- {
202
- this.rateLimitConfigurer.setup(this.app, {
203
- isDevelopmentMode: this.isDevelopmentMode,
204
- globalRateLimit: this.globalRateLimit,
205
- windowMs: this.windowMs,
206
- maxRequests: this.maxRequests,
207
- developmentMultiplier: this.developmentMultiplier,
208
- applyKeyGenerator: this.applyKeyGenerator,
209
- tooManyRequestsMessage: this.tooManyRequestsMessage
210
- });
211
- }
212
-
213
- setupRequestParsing()
214
- {
215
- if(this.maxRequestSize){
216
- this.jsonLimit = this.maxRequestSize;
217
- this.urlencodedLimit = this.maxRequestSize;
218
- }
219
- if(this.useExpressJson){
220
- this.app.use(this.applicationFramework.json({
221
- limit: this.jsonLimit,
222
- verify: this.verifyContentTypeJson.bind(this)
223
- }));
224
- }
225
- if(this.useUrlencoded){
226
- this.app.use(this.bodyParser.urlencoded({
227
- extended: true,
228
- limit: this.urlencodedLimit
229
- }));
230
- }
231
- }
232
-
233
- setupTrustedProxy()
234
- {
235
- if('' !== this.trustedProxy){
236
- this.app.enable('trust proxy', this.trustedProxy);
237
- }
238
- }
239
-
240
- verifyContentTypeJson(req, res, buf)
241
- {
242
- let contentType = req.headers['content-type'] || '';
243
- if(
244
- 'POST' === req.method
245
- && 0 < buf.length
246
- && !contentType.includes('application/json')
247
- && !contentType.includes('multipart/form-data')
248
- ){
249
- this.error = {message: 'Invalid content-type for JSON request'};
250
- return false;
251
- }
252
- }
253
-
254
- setupVirtualHosts()
255
- {
256
- if(!this.useVirtualHosts || 0 === this.domains.length){
257
- return;
258
- }
259
- this.app.use((req, res, next) => {
260
- let hostname = req.get('host');
261
- if(!hostname){
262
- if(this.defaultDomain){
263
- req.domain = this.defaultDomain;
264
- return next();
265
- }
266
- this.error = {message: 'No hostname provided and no default domain configured'};
267
- return res.status(400).send('Bad Request');
268
- }
269
- let domain = this.findDomainConfig(hostname);
270
- if(!domain){
271
- if(this.defaultDomain){
272
- req.domain = this.defaultDomain;
273
- return next();
274
- }
275
- this.error = {message: 'Unknown domain: ' + hostname};
276
- return res.status(404).send('Domain not found');
277
- }
278
- req.domain = domain;
279
- next();
280
- });
281
- }
282
-
283
- findDomainConfig(hostname)
284
- {
285
- if(!hostname || 'string' !== typeof hostname){
286
- return false;
287
- }
288
- let cleanHostname = hostname.toLowerCase().trim();
289
- for(let i = 0; i < this.domains.length; i++){
290
- let domain = this.domains[i];
291
- if(domain.hostname === cleanHostname){
292
- return domain;
293
- }
294
- if(domain.aliases && domain.aliases.includes(cleanHostname)){
295
- return domain;
296
- }
297
- }
298
- return false;
299
- }
300
-
301
- createServer()
302
- {
303
- if(!this.useHttps){
304
- return http.createServer(this.app);
305
- }
306
- if(this.useVirtualHosts && 0 < this.domains.length){
307
- return this.createHttpsServerWithSNI();
308
- }
309
- return this.createSingleHttpsServer();
310
- }
311
-
312
- createSingleHttpsServer()
313
- {
314
- let key = FileHandler.readFile(this.keyPath, 'Key');
315
- if(!key){
316
- this.error = {message: 'Could not read SSL key file: ' + this.keyPath};
317
- return false;
318
- }
319
- let cert = FileHandler.readFile(this.certPath, 'Cert');
320
- if(!cert){
321
- this.error = {message: 'Could not read SSL certificate file: ' + this.certPath};
322
- return false;
323
- }
324
- let credentials = {key, cert, passphrase: this.passphrase};
325
- if('' !== this.httpsChain){
326
- let ca = FileHandler.readFile(this.httpsChain, 'Certificate Authority');
327
- if(ca){
328
- credentials.ca = ca;
329
- }
330
- }
331
- return https.createServer(credentials, this.app);
332
- }
333
-
334
- createHttpsServerWithSNI()
335
- {
336
- let defaultCredentials = this.loadDefaultCredentials();
337
- if(!defaultCredentials){
338
- return false;
339
- }
340
- let httpsOptions = Object.assign({}, defaultCredentials);
341
- httpsOptions.SNICallback = (hostname, callback) => {
342
- let domain = this.findDomainConfig(hostname);
343
- if(!domain || !domain.keyPath || !domain.certPath){
344
- return callback(null, null);
345
- }
346
- let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
347
- if(!key){
348
- this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
349
- return callback(null, null);
350
- }
351
- let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
352
- if(!cert){
353
- this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
354
- return callback(null, null);
355
- }
356
- let ctx = require('tls').createSecureContext({key, cert});
357
- callback(null, ctx);
358
- };
359
- return https.createServer(httpsOptions, this.app);
360
- }
361
-
362
- loadDefaultCredentials()
363
- {
364
- let key = FileHandler.readFile(this.keyPath, 'Default Key');
365
- if(!key){
366
- this.error = {message: 'Could not read default SSL key file: '+this.keyPath};
367
- return false;
368
- }
369
- let cert = FileHandler.readFile(this.certPath, 'Default Cert');
370
- if(!cert){
371
- this.error = {message: 'Could not read default SSL certificate file: '+this.certPath};
372
- return false;
373
- }
374
- return {key, cert, passphrase: this.passphrase};
375
- }
376
-
377
- listen(port)
378
- {
379
- let listenPort = port || this.port;
380
- if(!this.appServer){
381
- this.error = {message: 'Cannot listen: app server not created'};
382
- return false;
383
- }
384
- this.appServer.listen(listenPort);
385
- return true;
386
- }
387
-
388
- async enableServeHome(app, homePageLoadCallback)
389
- {
390
- let limiter = this.rateLimitConfigurer.createHomeLimiter();
391
- app.post('/', limiter);
392
- app.post('/', async (req, res, next) => {
393
- if('/' === req._parsedUrl.pathname){
394
- return res.redirect('/');
395
- }
396
- next();
397
- });
398
- app.get('/', limiter);
399
- app.get('/', async (req, res, next) => {
400
- if('/' === req._parsedUrl.pathname){
401
- if('function' !== typeof homePageLoadCallback){
402
- let errorMessage = 'Homepage contents could not be loaded.';
403
- if('function' === typeof this.processErrorResponse){
404
- return this.processErrorResponse(500, errorMessage, req, res);
405
- }
406
- return res.status(500).send(errorMessage);
407
- }
408
- let homepageContent = await homePageLoadCallback(req);
409
- if(!homepageContent){
410
- let message = 'Error loading homepage content';
411
- this.error = {message};
412
- if('function' === typeof this.processErrorResponse){
413
- return this.processErrorResponse(500, message, req, res);
414
- }
415
- return res.status(500).send(message);
416
- }
417
- return res.send(homepageContent);
418
- }
419
- next();
420
- });
421
- }
422
-
423
- async serveStatics(app, statics)
424
- {
425
- app.use(this.applicationFramework.static(statics, this.staticOptions));
426
- return true;
427
- }
428
-
429
- async serveStaticsPath(app, staticsPath, statics)
430
- {
431
- app.use(staticsPath, this.applicationFramework.static(statics, this.staticOptions));
432
- return true;
433
- }
434
-
435
- addDomain(domainConfig)
436
- {
437
- if(!domainConfig || !domainConfig.hostname){
438
- this.error = {message: 'Domain configuration missing hostname'};
439
- return false;
440
- }
441
- if('string' !== typeof domainConfig.hostname){
442
- this.error = {message: 'Domain hostname must be a string'};
443
- return false;
444
- }
445
- this.domains.push(domainConfig);
446
- return true;
447
- }
448
-
449
- addDevelopmentDomain(domain)
450
- {
451
- if(!domain || 'string' !== typeof domain){
452
- return false;
453
- }
454
- this.developmentDomains.push(domain);
455
- return true;
456
- }
457
-
458
- setDomainMapping(mapping)
459
- {
460
- if(!mapping || 'object' !== typeof mapping){
461
- return false;
462
- }
463
- this.domainMapping = mapping;
464
- return true;
465
- }
466
-
467
- async close()
468
- {
469
- if(!this.appServer){
470
- return true;
471
- }
472
- return this.appServer.close();
473
- }
474
-
475
- enableCSP(cspOptions)
476
- {
477
- return this.securityConfigurer.enableCSP(this.app, cspOptions);
478
- }
479
-
480
- validateInput(input, type)
481
- {
482
- if('string' !== typeof input){
483
- return false;
484
- }
485
- let patterns = {
486
- email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
487
- username: /^[a-zA-Z0-9_-]{3,30}$/,
488
- strongPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
489
- alphanumeric: /^[a-zA-Z0-9]+$/,
490
- numeric: /^\d+$/,
491
- hexColor: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
492
- ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
493
- };
494
- return patterns[type] ? patterns[type].test(input) : false;
495
- }
496
-
497
- }
498
-
499
- module.exports.AppServerFactory = AppServerFactory;
1
+ /**
2
+ *
3
+ * Reldens - AppServerFactory
4
+ *
5
+ */
6
+
7
+ const { FileHandler } = require('./file-handler');
8
+ const { DevelopmentModeDetector } = require('./app-server-factory/development-mode-detector');
9
+ const { ProtocolEnforcer } = require('./app-server-factory/protocol-enforcer');
10
+ const { SecurityConfigurer } = require('./app-server-factory/security-configurer');
11
+ const { CorsConfigurer } = require('./app-server-factory/cors-configurer');
12
+ const { RateLimitConfigurer } = require('./app-server-factory/rate-limit-configurer');
13
+ const http = require('http');
14
+ const https = require('https');
15
+ const express = require('express');
16
+ const bodyParser = require('body-parser');
17
+ const session = require('express-session');
18
+ const compression = require('compression');
19
+
20
+ class AppServerFactory
21
+ {
22
+
23
+ constructor()
24
+ {
25
+ this.applicationFramework = express;
26
+ this.bodyParser = bodyParser;
27
+ this.session = session;
28
+ this.compression = compression;
29
+ this.appServer = false;
30
+ this.app = express();
31
+ this.useCors = true;
32
+ this.useExpressJson = true;
33
+ this.useUrlencoded = true;
34
+ this.encoding = 'utf-8';
35
+ this.useHttps = false;
36
+ this.passphrase = '';
37
+ this.httpsChain = '';
38
+ this.keyPath = '';
39
+ this.certPath = '';
40
+ this.trustedProxy = '';
41
+ this.windowMs = 60000;
42
+ this.maxRequests = 30;
43
+ this.applyKeyGenerator = false;
44
+ this.jsonLimit = '1mb';
45
+ this.urlencodedLimit = '1mb';
46
+ this.useHelmet = true;
47
+ this.helmetConfig = false;
48
+ this.useXssProtection = true;
49
+ this.globalRateLimit = 0;
50
+ this.corsOrigin = '*';
51
+ this.corsMethods = [];
52
+ this.corsHeaders = [];
53
+ this.tooManyRequestsMessage = 'Too many requests, please try again later.';
54
+ this.error = {};
55
+ this.processErrorResponse = false;
56
+ this.port = 3000;
57
+ this.autoListen = false;
58
+ this.domains = [];
59
+ this.useVirtualHosts = false;
60
+ this.defaultDomain = '';
61
+ this.maxRequestSize = '10mb';
62
+ this.sanitizeOptions = {allowedTags: [], allowedAttributes: {}};
63
+ this.staticOptions = {
64
+ maxAge: '1d',
65
+ etag: true,
66
+ lastModified: true,
67
+ index: false,
68
+ setHeaders: function(res){
69
+ res.set('X-Content-Type-Options', 'nosniff');
70
+ res.set('X-Frame-Options', 'DENY');
71
+ }
72
+ };
73
+ this.isDevelopmentMode = false;
74
+ this.developmentDomains = [];
75
+ this.domainMapping = {};
76
+ this.enforceProtocol = true;
77
+ this.developmentPatterns = [];
78
+ this.developmentEnvironments = [];
79
+ this.developmentPorts = [];
80
+ this.developmentMultiplier = 10;
81
+ this.developmentExternalDomains = {};
82
+ this.developmentModeDetector = new DevelopmentModeDetector();
83
+ this.protocolEnforcer = new ProtocolEnforcer();
84
+ this.securityConfigurer = new SecurityConfigurer();
85
+ this.corsConfigurer = new CorsConfigurer();
86
+ this.rateLimitConfigurer = new RateLimitConfigurer();
87
+ this.useCompression = true;
88
+ this.compressionOptions = {
89
+ level: 6,
90
+ threshold: 1024,
91
+ filter: function(req, res){
92
+ if(req.headers['x-no-compression']){
93
+ return false;
94
+ }
95
+ return compression.filter(req, res);
96
+ }
97
+ };
98
+ }
99
+
100
+ createAppServer(appServerConfig)
101
+ {
102
+ if(appServerConfig){
103
+ Object.assign(this, appServerConfig);
104
+ }
105
+ this.addHttpDomainsAsDevelopment();
106
+ this.detectDevelopmentMode();
107
+ this.setupDevelopmentConfiguration();
108
+ this.setupProtocolEnforcement();
109
+ this.setupSecurity();
110
+ this.setupCompression();
111
+ this.setupVirtualHosts();
112
+ this.setupCors();
113
+ this.setupRateLimiting();
114
+ this.setupRequestParsing();
115
+ this.setupTrustedProxy();
116
+ this.appServer = this.createServer();
117
+ if(!this.appServer){
118
+ this.error = {message: 'Failed to create app server'};
119
+ return false;
120
+ }
121
+ if(this.autoListen){
122
+ this.listen();
123
+ }
124
+ return {app: this.app, appServer: this.appServer};
125
+ }
126
+
127
+ extractDomainFromHttpUrl(url)
128
+ {
129
+ if(!url || !url.startsWith('http://')){
130
+ return false;
131
+ }
132
+ return url.replace(/^http:\/\//, '').split(':')[0];
133
+ }
134
+
135
+ addHttpDomainsAsDevelopment()
136
+ {
137
+ let hostDomain = this.extractDomainFromHttpUrl(process.env.RELDENS_APP_HOST);
138
+ let publicDomain = this.extractDomainFromHttpUrl(process.env.RELDENS_PUBLIC_URL);
139
+ if(hostDomain && !this.developmentDomains.includes(hostDomain)){
140
+ this.developmentDomains.push(hostDomain);
141
+ }
142
+ if(publicDomain && !this.developmentDomains.includes(publicDomain)){
143
+ this.developmentDomains.push(publicDomain);
144
+ }
145
+ }
146
+
147
+ detectDevelopmentMode()
148
+ {
149
+ let detectConfig = {
150
+ developmentDomains: this.developmentDomains,
151
+ domains: this.domains
152
+ };
153
+ if(0 < this.developmentPatterns.length){
154
+ detectConfig['developmentPatterns'] = this.developmentPatterns;
155
+ }
156
+ if(0 < this.developmentEnvironments.length){
157
+ detectConfig['developmentEnvironments'] = this.developmentEnvironments;
158
+ }
159
+ this.isDevelopmentMode = this.developmentModeDetector.detect(detectConfig);
160
+ }
161
+
162
+ setupDevelopmentConfiguration()
163
+ {
164
+ if(!this.isDevelopmentMode){
165
+ return;
166
+ }
167
+ this.staticOptions.setHeaders = (res) => {
168
+ res.set('X-Content-Type-Options', 'nosniff');
169
+ res.set('X-Frame-Options', 'SAMEORIGIN');
170
+ res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
171
+ res.set('Pragma', 'no-cache');
172
+ res.set('Expires', '0');
173
+ };
174
+ }
175
+
176
+ setupProtocolEnforcement()
177
+ {
178
+ this.protocolEnforcer.setup(this.app, {
179
+ isDevelopmentMode: this.isDevelopmentMode,
180
+ useHttps: this.useHttps,
181
+ enforceProtocol: this.enforceProtocol
182
+ });
183
+ }
184
+
185
+ setupSecurity()
186
+ {
187
+ this.securityConfigurer.setupHelmet(this.app, {
188
+ isDevelopmentMode: this.isDevelopmentMode,
189
+ useHelmet: this.useHelmet,
190
+ helmetConfig: this.helmetConfig,
191
+ developmentExternalDomains: this.developmentExternalDomains
192
+ });
193
+ this.securityConfigurer.setupXssProtection(this.app, {
194
+ useXssProtection: this.useXssProtection,
195
+ sanitizeOptions: this.sanitizeOptions
196
+ });
197
+ }
198
+
199
+ setupCompression()
200
+ {
201
+ if(!this.useCompression){
202
+ return;
203
+ }
204
+ this.app.use(this.compression(this.compressionOptions));
205
+ }
206
+
207
+ setupCors()
208
+ {
209
+ let corsConfig = {
210
+ isDevelopmentMode: this.isDevelopmentMode,
211
+ useCors: this.useCors,
212
+ corsOrigin: this.corsOrigin
213
+ };
214
+ if(0 < this.corsMethods.length){
215
+ corsConfig['corsMethods'] = this.corsMethods;
216
+ }
217
+ if(0 < this.corsHeaders.length){
218
+ corsConfig['corsHeaders'] = this.corsHeaders;
219
+ }
220
+ if(0 < this.developmentPorts.length){
221
+ corsConfig['developmentPorts'] = this.developmentPorts;
222
+ }
223
+ if(
224
+ 'object' === typeof this.domainMapping
225
+ && null !== this.domainMapping
226
+ && 0 < Object.keys(this.domainMapping)
227
+ ){
228
+ corsConfig['domainMapping'] = this.domainMapping;
229
+ }
230
+ this.corsConfigurer.setup(this.app, corsConfig);
231
+ }
232
+
233
+ setupRateLimiting()
234
+ {
235
+ this.rateLimitConfigurer.setup(this.app, {
236
+ isDevelopmentMode: this.isDevelopmentMode,
237
+ globalRateLimit: this.globalRateLimit,
238
+ windowMs: this.windowMs,
239
+ maxRequests: this.maxRequests,
240
+ developmentMultiplier: this.developmentMultiplier,
241
+ applyKeyGenerator: this.applyKeyGenerator,
242
+ tooManyRequestsMessage: this.tooManyRequestsMessage
243
+ });
244
+ }
245
+
246
+ setupRequestParsing()
247
+ {
248
+ if(this.maxRequestSize){
249
+ this.jsonLimit = this.maxRequestSize;
250
+ this.urlencodedLimit = this.maxRequestSize;
251
+ }
252
+ if(this.useExpressJson){
253
+ this.app.use(this.applicationFramework.json({
254
+ limit: this.jsonLimit,
255
+ verify: this.verifyContentTypeJson.bind(this)
256
+ }));
257
+ }
258
+ if(this.useUrlencoded){
259
+ this.app.use(this.bodyParser.urlencoded({
260
+ extended: true,
261
+ limit: this.urlencodedLimit
262
+ }));
263
+ }
264
+ }
265
+
266
+ setupTrustedProxy()
267
+ {
268
+ if('' !== this.trustedProxy){
269
+ this.app.set('trust proxy', this.trustedProxy);
270
+ }
271
+ }
272
+
273
+ verifyContentTypeJson(req, res, buf)
274
+ {
275
+ let contentType = req.headers['content-type'] || '';
276
+ if(
277
+ 'POST' === req.method
278
+ && 0 < buf.length
279
+ && !contentType.includes('application/json')
280
+ && !contentType.includes('multipart/form-data')
281
+ ){
282
+ this.error = {message: 'Invalid content-type for JSON request'};
283
+ return false;
284
+ }
285
+ }
286
+
287
+ setupVirtualHosts()
288
+ {
289
+ if(!this.useVirtualHosts || 0 === this.domains.length){
290
+ return;
291
+ }
292
+ this.app.use((req, res, next) => {
293
+ let hostname = req.get('host');
294
+ if(!hostname){
295
+ if(this.defaultDomain){
296
+ req.domain = this.defaultDomain;
297
+ return next();
298
+ }
299
+ this.error = {message: 'No hostname provided and no default domain configured'};
300
+ return res.status(400).send('Bad Request');
301
+ }
302
+ let domain = this.findDomainConfig(hostname);
303
+ if(!domain){
304
+ if(this.defaultDomain){
305
+ req.domain = this.defaultDomain;
306
+ return next();
307
+ }
308
+ this.error = {message: 'Unknown domain: ' + hostname};
309
+ return res.status(404).send('Domain not found');
310
+ }
311
+ req.domain = domain;
312
+ next();
313
+ });
314
+ }
315
+
316
+ findDomainConfig(hostname)
317
+ {
318
+ if(!hostname || 'string' !== typeof hostname){
319
+ return false;
320
+ }
321
+ let cleanHostname = hostname.toLowerCase().trim();
322
+ let hostWithoutPort = cleanHostname.split(':')[0];
323
+ for(let i = 0; i < this.domains.length; i++){
324
+ let domain = this.domains[i];
325
+ if(domain.hostname === hostWithoutPort){
326
+ return domain;
327
+ }
328
+ if(domain.aliases && domain.aliases.includes(hostWithoutPort)){
329
+ return domain;
330
+ }
331
+ }
332
+ return false;
333
+ }
334
+
335
+ createServer()
336
+ {
337
+ if(!this.useHttps){
338
+ return http.createServer(this.app);
339
+ }
340
+ if(this.useVirtualHosts && 0 < this.domains.length){
341
+ return this.createHttpsServerWithSNI();
342
+ }
343
+ return this.createSingleHttpsServer();
344
+ }
345
+
346
+ createSingleHttpsServer()
347
+ {
348
+ let key = FileHandler.readFile(this.keyPath, 'Key');
349
+ if(!key){
350
+ this.error = {message: 'Could not read SSL key file: ' + this.keyPath};
351
+ return false;
352
+ }
353
+ let cert = FileHandler.readFile(this.certPath, 'Cert');
354
+ if(!cert){
355
+ this.error = {message: 'Could not read SSL certificate file: ' + this.certPath};
356
+ return false;
357
+ }
358
+ let credentials = {key, cert, passphrase: this.passphrase};
359
+ if('' !== this.httpsChain){
360
+ let ca = FileHandler.readFile(this.httpsChain, 'Certificate Authority');
361
+ if(ca){
362
+ credentials.ca = ca;
363
+ }
364
+ }
365
+ return https.createServer(credentials, this.app);
366
+ }
367
+
368
+ createHttpsServerWithSNI()
369
+ {
370
+ let defaultCredentials = this.loadDefaultCredentials();
371
+ if(!defaultCredentials){
372
+ return false;
373
+ }
374
+ let httpsOptions = Object.assign({}, defaultCredentials);
375
+ httpsOptions.SNICallback = (hostname, callback) => {
376
+ let domain = this.findDomainConfig(hostname);
377
+ if(!domain || !domain.keyPath || !domain.certPath){
378
+ return callback(null, null);
379
+ }
380
+ let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
381
+ if(!key){
382
+ this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
383
+ return callback(null, null);
384
+ }
385
+ let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
386
+ if(!cert){
387
+ this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
388
+ return callback(null, null);
389
+ }
390
+ let ctx = require('tls').createSecureContext({key, cert});
391
+ callback(null, ctx);
392
+ };
393
+ return https.createServer(httpsOptions, this.app);
394
+ }
395
+
396
+ loadDefaultCredentials()
397
+ {
398
+ let key = FileHandler.readFile(this.keyPath, 'Default Key');
399
+ if(!key){
400
+ this.error = {message: 'Could not read default SSL key file: '+this.keyPath};
401
+ return false;
402
+ }
403
+ let cert = FileHandler.readFile(this.certPath, 'Default Cert');
404
+ if(!cert){
405
+ this.error = {message: 'Could not read default SSL certificate file: '+this.certPath};
406
+ return false;
407
+ }
408
+ return {key, cert, passphrase: this.passphrase};
409
+ }
410
+
411
+ listen(port)
412
+ {
413
+ let listenPort = port || this.port;
414
+ if(!this.appServer){
415
+ this.error = {message: 'Cannot listen: app server not created'};
416
+ return false;
417
+ }
418
+ this.appServer.listen(listenPort);
419
+ return true;
420
+ }
421
+
422
+ async enableServeHome(app, homePageLoadCallback)
423
+ {
424
+ let limiter = this.rateLimitConfigurer.createHomeLimiter();
425
+ app.post('/', limiter);
426
+ app.post('/', async (req, res, next) => {
427
+ if('/' === req._parsedUrl.pathname){
428
+ return res.redirect('/');
429
+ }
430
+ next();
431
+ });
432
+ app.get('/', limiter);
433
+ app.get('/', async (req, res, next) => {
434
+ if('/' === req._parsedUrl.pathname){
435
+ if('function' !== typeof homePageLoadCallback){
436
+ let errorMessage = 'Homepage contents could not be loaded.';
437
+ if('function' === typeof this.processErrorResponse){
438
+ return this.processErrorResponse(500, errorMessage, req, res);
439
+ }
440
+ return res.status(500).send(errorMessage);
441
+ }
442
+ let homepageContent = await homePageLoadCallback(req);
443
+ if(!homepageContent){
444
+ let message = 'Error loading homepage content';
445
+ this.error = {message};
446
+ if('function' === typeof this.processErrorResponse){
447
+ return this.processErrorResponse(500, message, req, res);
448
+ }
449
+ return res.status(500).send(message);
450
+ }
451
+ return res.send(homepageContent);
452
+ }
453
+ next();
454
+ });
455
+ }
456
+
457
+ async serveStatics(app, statics)
458
+ {
459
+ app.use(this.applicationFramework.static(statics, this.staticOptions));
460
+ return true;
461
+ }
462
+
463
+ async serveStaticsPath(app, staticsPath, statics)
464
+ {
465
+ app.use(staticsPath, this.applicationFramework.static(statics, this.staticOptions));
466
+ return true;
467
+ }
468
+
469
+ addDomain(domainConfig)
470
+ {
471
+ if(!domainConfig || !domainConfig.hostname){
472
+ this.error = {message: 'Domain configuration missing hostname'};
473
+ return false;
474
+ }
475
+ if('string' !== typeof domainConfig.hostname){
476
+ this.error = {message: 'Domain hostname must be a string'};
477
+ return false;
478
+ }
479
+ this.domains.push(domainConfig);
480
+ return true;
481
+ }
482
+
483
+ addDevelopmentDomain(domain)
484
+ {
485
+ if(!domain || 'string' !== typeof domain){
486
+ return false;
487
+ }
488
+ this.developmentDomains.push(domain);
489
+ return true;
490
+ }
491
+
492
+ setDomainMapping(mapping)
493
+ {
494
+ if(!mapping || 'object' !== typeof mapping){
495
+ return false;
496
+ }
497
+ this.domainMapping = mapping;
498
+ return true;
499
+ }
500
+
501
+ async close()
502
+ {
503
+ if(!this.appServer){
504
+ return true;
505
+ }
506
+ return this.appServer.close();
507
+ }
508
+
509
+ enableCSP(cspOptions)
510
+ {
511
+ return this.securityConfigurer.enableCSP(this.app, cspOptions);
512
+ }
513
+
514
+ }
515
+
516
+ module.exports.AppServerFactory = AppServerFactory;