mastercontroller 1.3.0 → 1.3.2

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/MasterControl.js CHANGED
@@ -64,6 +64,9 @@ class MasterControl {
64
64
  _loadedFunc = null
65
65
  _tlsOptions = null
66
66
  _hstsEnabled = false
67
+ _hstsMaxAge = 31536000 // 1 year default
68
+ _hstsIncludeSubDomains = true
69
+ _hstsPreload = false
67
70
 
68
71
  #loadTransientListClasses(name, params){
69
72
  Object.defineProperty(this.requestList, name, {
@@ -228,6 +231,13 @@ class MasterControl {
228
231
 
229
232
  // adds all the server settings needed
230
233
  serverSettings(settings){
234
+ // Defensive: Check if server exists (may be called before master.start())
235
+ if (!this.server) {
236
+ console.warn('[MasterControl] serverSettings() called before master.start(server). Settings will be applied when server is set.');
237
+ // Store settings to apply later
238
+ this._pendingServerSettings = settings;
239
+ return;
240
+ }
231
241
 
232
242
  if(settings.httpPort || settings.requestTimeout){
233
243
  this.server.timeout = settings.requestTimeout;
@@ -244,6 +254,38 @@ class MasterControl {
244
254
 
245
255
  }
246
256
 
257
+ /**
258
+ * Enable HSTS (HTTP Strict Transport Security) for HTTPS
259
+ * Should only be called for production HTTPS servers
260
+ *
261
+ * @param {Object} options - HSTS configuration options
262
+ * @param {Number} options.maxAge - Max age in seconds (default: 31536000 = 1 year)
263
+ * @param {Boolean} options.includeSubDomains - Include subdomains (default: true)
264
+ * @param {Boolean} options.preload - Enable HSTS preload (default: false)
265
+ * @returns {MasterControl} - Returns this for chaining
266
+ *
267
+ * @example
268
+ * // Basic usage (1 year, includeSubDomains)
269
+ * master.enableHSTS();
270
+ *
271
+ * // Custom configuration
272
+ * master.enableHSTS({
273
+ * maxAge: 15552000, // 180 days
274
+ * includeSubDomains: true,
275
+ * preload: true // Submit to HSTS preload list
276
+ * });
277
+ */
278
+ enableHSTS(options = {}) {
279
+ this._hstsEnabled = true;
280
+ this._hstsMaxAge = options.maxAge || 31536000; // 1 year default (matches industry standard)
281
+ this._hstsIncludeSubDomains = options.includeSubDomains !== false; // true by default
282
+ this._hstsPreload = options.preload === true; // false by default
283
+
284
+ console.log(`[MasterControl] HSTS enabled: max-age=${this._hstsMaxAge}${this._hstsIncludeSubDomains ? ', includeSubDomains' : ''}${this._hstsPreload ? ', preload' : ''}`);
285
+
286
+ return this; // Chainable
287
+ }
288
+
247
289
  useHTTPServer(port, func){
248
290
  if (typeof func === 'function') {
249
291
  http.createServer(function (req, res) {
@@ -256,38 +298,56 @@ class MasterControl {
256
298
  setupServer(type, credentials ){
257
299
  try {
258
300
  var $that = this;
259
- // Auto-load internal master tools so services (request, error, router, etc.) are available
260
- // before user config initializes them.
261
- try {
262
- $that.addInternalTools([
263
- 'MasterPipeline',
264
- 'MasterTimeout',
265
- 'MasterErrorRenderer',
266
- 'MasterAction',
267
- 'MasterActionFilters',
268
- 'MasterRouter',
269
- 'MasterRequest',
270
- 'MasterError',
271
- 'MasterCors',
272
- 'MasterSession',
273
- 'MasterSocket',
274
- 'MasterHtml',
275
- 'MasterTemplate',
276
- 'MasterTools',
277
- 'TemplateOverwrite'
278
- ]);
279
- } catch (e) {
280
- console.error('[MasterControl] Failed to load internal tools:', e && e.message);
301
+
302
+ // AUTO-LOAD internal framework modules
303
+ // These are required for the framework to function and are loaded transparently
304
+ const internalModules = {
305
+ 'MasterPipeline': './MasterPipeline',
306
+ 'MasterTimeout': './MasterTimeout',
307
+ 'MasterErrorRenderer': './error/MasterErrorRenderer',
308
+ 'MasterAction': './MasterAction',
309
+ 'MasterActionFilters': './MasterActionFilters',
310
+ 'MasterRouter': './MasterRouter',
311
+ 'MasterRequest': './MasterRequest',
312
+ 'MasterError': './error/MasterError',
313
+ 'MasterCors': './MasterCors',
314
+ 'SessionSecurity': './security/SessionSecurity',
315
+ 'MasterSocket': './MasterSocket',
316
+ 'MasterHtml': './MasterHtml',
317
+ 'MasterTemplate': './MasterTemplate',
318
+ 'MasterTools': './MasterTools',
319
+ 'TemplateOverwrite': './TemplateOverwrite'
320
+ };
321
+
322
+ for (const moduleName in internalModules) {
323
+ try {
324
+ const modulePath = internalModules[moduleName];
325
+ const module = require(modulePath);
326
+
327
+ // Special handling for SessionSecurity to avoid circular dependency
328
+ if (moduleName === 'SessionSecurity' && module.MasterSessionSecurity) {
329
+ $that.session = new module.MasterSessionSecurity();
330
+ }
331
+ // Most modules auto-register via master.extend() at module load time
332
+ } catch (e) {
333
+ console.error(`[MasterControl] Failed to load ${moduleName}:`, e && e.message);
334
+ }
281
335
  }
282
336
 
337
+ // Initialize global error handlers
338
+ setupGlobalErrorHandlers();
339
+
283
340
  // Register core middleware that must run for framework to function
284
341
  $that._registerCoreMiddleware();
285
342
 
286
343
  if(type === "http"){
287
344
  $that.serverProtocol = "http";
288
- return http.createServer(async function(req, res) {
345
+ const server = http.createServer(async function(req, res) {
289
346
  $that.serverRun(req, res);
290
347
  });
348
+ // Set server immediately so config can access it
349
+ $that.server = server;
350
+ return server;
291
351
  }
292
352
  if(type === "https"){
293
353
  $that.serverProtocol = "https";
@@ -296,14 +356,42 @@ class MasterControl {
296
356
  $that._initializeTlsFromEnv();
297
357
  credentials = $that._tlsOptions;
298
358
  }
299
- // Apply secure defaults if missing
359
+ // Apply secure defaults if missing (2026 security standards)
300
360
  if(credentials){
301
- if(!credentials.minVersion){ credentials.minVersion = 'TLSv1.2'; }
361
+ // Default to TLS 1.3 for security (2026 standard)
362
+ // TLS 1.2 still supported but not default
363
+ if(!credentials.minVersion){
364
+ credentials.minVersion = 'TLSv1.3';
365
+ console.log('[MasterControl] TLS 1.3 enabled by default (recommended for 2026)');
366
+ }
367
+
368
+ // Secure cipher suites (Mozilla Intermediate configuration - 2026)
369
+ // Supports TLS 1.3 and TLS 1.2 for backward compatibility
370
+ if(!credentials.ciphers){
371
+ credentials.ciphers = [
372
+ // TLS 1.3 cipher suites (strongest)
373
+ 'TLS_AES_256_GCM_SHA384',
374
+ 'TLS_CHACHA20_POLY1305_SHA256',
375
+ 'TLS_AES_128_GCM_SHA256',
376
+ // TLS 1.2 cipher suites (backward compatibility)
377
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
378
+ 'ECDHE-RSA-AES256-GCM-SHA384',
379
+ 'ECDHE-ECDSA-CHACHA20-POLY1305',
380
+ 'ECDHE-RSA-CHACHA20-POLY1305',
381
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
382
+ 'ECDHE-RSA-AES128-GCM-SHA256'
383
+ ].join(':');
384
+ console.log('[MasterControl] Secure cipher suites configured (Mozilla Intermediate)');
385
+ }
386
+
302
387
  if(credentials.honorCipherOrder === undefined){ credentials.honorCipherOrder = true; }
303
388
  if(!credentials.ALPNProtocols){ credentials.ALPNProtocols = ['h2', 'http/1.1']; }
304
- return https.createServer(credentials, async function(req, res) {
389
+ const server = https.createServer(credentials, async function(req, res) {
305
390
  $that.serverRun(req, res);
306
391
  });
392
+ // Set server immediately so config can access it
393
+ $that.server = server;
394
+ return server;
307
395
  }else{
308
396
  throw "Credentials needed to setup https"
309
397
  }
@@ -315,18 +403,69 @@ class MasterControl {
315
403
  }
316
404
  }
317
405
 
318
- // Creates an HTTP server that 301-redirects to HTTPS counterpart
319
- startHttpToHttpsRedirect(redirectPort, bindHost){
406
+ /**
407
+ * Creates an HTTP server that 301-redirects to HTTPS counterpart
408
+ * SECURITY: Validates host header to prevent open redirect attacks
409
+ *
410
+ * @param {Number} redirectPort - Port to listen on (usually 80)
411
+ * @param {String} bindHost - Host to bind to (e.g., '0.0.0.0')
412
+ * @param {Array<String>} allowedHosts - Whitelist of allowed hostnames (REQUIRED for security)
413
+ * @returns {http.Server} - HTTP server instance
414
+ *
415
+ * @example
416
+ * // Production usage (MUST specify allowed hosts)
417
+ * const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
418
+ * 'example.com',
419
+ * 'www.example.com',
420
+ * 'api.example.com'
421
+ * ]);
422
+ *
423
+ * @security CRITICAL: Always provide allowedHosts in production to prevent open redirect attacks
424
+ */
425
+ startHttpToHttpsRedirect(redirectPort, bindHost, allowedHosts = []){
320
426
  var $that = this;
427
+
428
+ // Security warning if no hosts specified
429
+ if (allowedHosts.length === 0) {
430
+ console.warn('[MasterControl] ⚠️ SECURITY WARNING: startHttpToHttpsRedirect() called without allowedHosts.');
431
+ console.warn('[MasterControl] This is vulnerable to open redirect attacks. Specify allowed hosts:');
432
+ console.warn('[MasterControl] master.startHttpToHttpsRedirect(80, "0.0.0.0", ["example.com", "www.example.com"])');
433
+ }
434
+
321
435
  return http.createServer(function (req, res) {
322
436
  try{
323
437
  var host = req.headers['host'] || '';
324
- // Force original host, just change scheme
438
+ var hostname = host.split(':')[0]; // Remove port number
439
+
440
+ // CRITICAL SECURITY: Validate host header to prevent open redirect attacks
441
+ if (allowedHosts.length > 0) {
442
+ if (!allowedHosts.includes(hostname)) {
443
+ logger.warn({
444
+ code: 'MC_SECURITY_INVALID_HOST',
445
+ message: 'HTTP redirect blocked: invalid host header',
446
+ host: hostname,
447
+ ip: req.connection.remoteAddress
448
+ });
449
+ res.statusCode = 400;
450
+ res.setHeader('Content-Type', 'text/plain');
451
+ res.end('Bad Request: Invalid host header');
452
+ return;
453
+ }
454
+ }
455
+
456
+ // Redirect to HTTPS with validated host
325
457
  var location = 'https://' + host + req.url;
326
458
  res.statusCode = 301;
327
459
  res.setHeader('Location', location);
460
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
328
461
  res.end();
329
462
  }catch(e){
463
+ logger.error({
464
+ code: 'MC_ERR_REDIRECT',
465
+ message: 'HTTP to HTTPS redirect failed',
466
+ error: e.message,
467
+ stack: e.stack
468
+ });
330
469
  res.statusCode = 500;
331
470
  res.end();
332
471
  }
@@ -446,30 +585,90 @@ class MasterControl {
446
585
  _registerCoreMiddleware(){
447
586
  var $that = this;
448
587
 
449
- // 1. Static File Serving
588
+ // 1. Static File Serving (with path traversal protection)
450
589
  $that.pipeline.use(async (ctx, next) => {
451
590
  if (ctx.isStatic) {
452
- // Serve static files
453
- let pathname = `.${ctx.request.url}`;
591
+ // SECURITY: Prevent path traversal attacks
592
+ let requestedPath = ctx.request.url;
593
+
594
+ // Normalize the path and resolve it
595
+ const publicRoot = path.resolve($that.root || '.');
596
+ const safePath = path.join(publicRoot, requestedPath);
597
+ const resolvedPath = path.resolve(safePath);
598
+
599
+ // CRITICAL: Ensure resolved path is within public root (prevents ../ attacks)
600
+ if (!resolvedPath.startsWith(publicRoot)) {
601
+ logger.warn({
602
+ code: 'MC_SECURITY_PATH_TRAVERSAL',
603
+ message: 'Path traversal attack blocked',
604
+ requestedPath: requestedPath,
605
+ resolvedPath: resolvedPath,
606
+ ip: ctx.request.connection.remoteAddress
607
+ });
608
+ ctx.response.statusCode = 403;
609
+ ctx.response.setHeader('Content-Type', 'text/plain');
610
+ ctx.response.end('Forbidden');
611
+ return;
612
+ }
454
613
 
455
- fs.exists(pathname, function (exist) {
614
+ // SECURITY: Block dotfiles (.env, .git, .htaccess, etc.)
615
+ const filename = path.basename(resolvedPath);
616
+ if (filename.startsWith('.')) {
617
+ logger.warn({
618
+ code: 'MC_SECURITY_DOTFILE_BLOCKED',
619
+ message: 'Dotfile access blocked',
620
+ filename: filename,
621
+ ip: ctx.request.connection.remoteAddress
622
+ });
623
+ ctx.response.statusCode = 403;
624
+ ctx.response.setHeader('Content-Type', 'text/plain');
625
+ ctx.response.end('Forbidden');
626
+ return;
627
+ }
628
+
629
+ // Check if file exists
630
+ fs.exists(resolvedPath, function (exist) {
456
631
  if (!exist) {
457
632
  ctx.response.statusCode = 404;
458
- ctx.response.end(`File ${pathname} not found!`);
633
+ ctx.response.setHeader('Content-Type', 'text/plain');
634
+ ctx.response.end('Not Found');
459
635
  return;
460
636
  }
461
637
 
462
- if (fs.statSync(pathname).isDirectory()) {
463
- pathname += '/index' + path.parse(pathname).ext;
638
+ // Get file stats
639
+ let finalPath = resolvedPath;
640
+ const stats = fs.statSync(resolvedPath);
641
+
642
+ // If directory, try to serve index.html
643
+ if (stats.isDirectory()) {
644
+ finalPath = path.join(resolvedPath, 'index.html');
645
+
646
+ // Check if index.html exists
647
+ if (!fs.existsSync(finalPath)) {
648
+ ctx.response.statusCode = 403;
649
+ ctx.response.setHeader('Content-Type', 'text/plain');
650
+ ctx.response.end('Forbidden');
651
+ return;
652
+ }
464
653
  }
465
654
 
466
- fs.readFile(pathname, function(err, data) {
655
+ // Read and serve the file
656
+ fs.readFile(finalPath, function(err, data) {
467
657
  if (err) {
658
+ logger.error({
659
+ code: 'MC_ERR_FILE_READ',
660
+ message: 'Error reading static file',
661
+ path: finalPath,
662
+ error: err.message
663
+ });
468
664
  ctx.response.statusCode = 500;
469
- ctx.response.end(`Error getting the file: ${err}.`);
665
+ ctx.response.setHeader('Content-Type', 'text/plain');
666
+ ctx.response.end('Internal Server Error');
470
667
  } else {
471
- const mimeType = $that.router.findMimeType(path.parse(pathname).ext);
472
- ctx.response.setHeader('Content-type', mimeType || 'text/plain');
668
+ const ext = path.extname(finalPath);
669
+ const mimeType = $that.router.findMimeType(ext);
670
+ ctx.response.setHeader('Content-Type', mimeType || 'application/octet-stream');
671
+ ctx.response.setHeader('X-Content-Type-Options', 'nosniff');
473
672
  ctx.response.end(data);
474
673
  }
475
674
  });
@@ -488,7 +687,9 @@ class MasterControl {
488
687
  // 3. Request Body Parsing (always needed)
489
688
  $that.pipeline.use(async (ctx, next) => {
490
689
  // Parse body using MasterRequest
491
- const params = await $that.request.getRequestParam(ctx.request, ctx.response);
690
+ // Pass entire context for backward compatibility (v1.3.x)
691
+ // getRequestParam() will extract request and requrl from context
692
+ const params = await $that.request.getRequestParam(ctx, ctx.response);
492
693
 
493
694
  // Merge parsed params into context
494
695
  if (params && params.query) {
@@ -513,7 +714,15 @@ class MasterControl {
513
714
  // 4. HSTS Header (if enabled for HTTPS)
514
715
  $that.pipeline.use(async (ctx, next) => {
515
716
  if ($that.serverProtocol === 'https' && $that._hstsEnabled) {
516
- ctx.response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
717
+ // Use configured HSTS values (not hardcoded)
718
+ let hstsValue = `max-age=${$that._hstsMaxAge}`;
719
+ if ($that._hstsIncludeSubDomains) {
720
+ hstsValue += '; includeSubDomains';
721
+ }
722
+ if ($that._hstsPreload) {
723
+ hstsValue += '; preload';
724
+ }
725
+ ctx.response.setHeader('Strict-Transport-Security', hstsValue);
517
726
  }
518
727
  await next();
519
728
  });
@@ -584,6 +793,13 @@ class MasterControl {
584
793
 
585
794
  start(server){
586
795
  this.server = server;
796
+
797
+ // Apply any pending server settings that were called before start()
798
+ if (this._pendingServerSettings) {
799
+ console.log('[MasterControl] Applying pending server settings');
800
+ this.serverSettings(this._pendingServerSettings);
801
+ this._pendingServerSettings = null;
802
+ }
587
803
  }
588
804
 
589
805
  startMVC(foldername){
@@ -618,7 +834,7 @@ class MasterControl {
618
834
  'MasterRouter': './MasterRouter',
619
835
  'MasterRequest': './MasterRequest',
620
836
  'MasterCors': './MasterCors',
621
- 'MasterSession': './MasterSession',
837
+ 'SessionSecurity': './security/SessionSecurity',
622
838
  'MasterSocket': './MasterSocket',
623
839
  'MasterHtml': './MasterHtml',
624
840
  'MasterTemplate': './MasterTemplate',
@@ -629,7 +845,12 @@ class MasterControl {
629
845
  for(var i = 0; i < requiredList.length; i++){
630
846
  const moduleName = requiredList[i];
631
847
  const modulePath = modulePathMap[moduleName] || './' + moduleName;
632
- require(modulePath);
848
+ const module = require(modulePath);
849
+
850
+ // Special handling for SessionSecurity to avoid circular dependency
851
+ if (moduleName === 'SessionSecurity' && module.MasterSessionSecurity) {
852
+ this.session = new module.MasterSessionSecurity();
853
+ }
633
854
  }
634
855
  }
635
856
  }