mastercontroller 1.3.1 → 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,39 +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
- 'SessionSecurity',
274
- 'MasterSocket',
275
- 'MasterHtml',
276
- 'MasterTemplate',
277
- 'MasterTools',
278
- 'TemplateOverwrite'
279
- ]);
280
- } catch (e) {
281
- 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
+ }
282
335
  }
283
336
 
337
+ // Initialize global error handlers
338
+ setupGlobalErrorHandlers();
339
+
284
340
  // Register core middleware that must run for framework to function
285
341
  $that._registerCoreMiddleware();
286
342
 
287
343
  if(type === "http"){
288
344
  $that.serverProtocol = "http";
289
- return http.createServer(async function(req, res) {
345
+ const server = http.createServer(async function(req, res) {
290
346
  $that.serverRun(req, res);
291
347
  });
348
+ // Set server immediately so config can access it
349
+ $that.server = server;
350
+ return server;
292
351
  }
293
352
  if(type === "https"){
294
353
  $that.serverProtocol = "https";
@@ -297,14 +356,42 @@ class MasterControl {
297
356
  $that._initializeTlsFromEnv();
298
357
  credentials = $that._tlsOptions;
299
358
  }
300
- // Apply secure defaults if missing
359
+ // Apply secure defaults if missing (2026 security standards)
301
360
  if(credentials){
302
- 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
+
303
387
  if(credentials.honorCipherOrder === undefined){ credentials.honorCipherOrder = true; }
304
388
  if(!credentials.ALPNProtocols){ credentials.ALPNProtocols = ['h2', 'http/1.1']; }
305
- return https.createServer(credentials, async function(req, res) {
389
+ const server = https.createServer(credentials, async function(req, res) {
306
390
  $that.serverRun(req, res);
307
391
  });
392
+ // Set server immediately so config can access it
393
+ $that.server = server;
394
+ return server;
308
395
  }else{
309
396
  throw "Credentials needed to setup https"
310
397
  }
@@ -316,18 +403,69 @@ class MasterControl {
316
403
  }
317
404
  }
318
405
 
319
- // Creates an HTTP server that 301-redirects to HTTPS counterpart
320
- 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 = []){
321
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
+
322
435
  return http.createServer(function (req, res) {
323
436
  try{
324
437
  var host = req.headers['host'] || '';
325
- // 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
326
457
  var location = 'https://' + host + req.url;
327
458
  res.statusCode = 301;
328
459
  res.setHeader('Location', location);
460
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
329
461
  res.end();
330
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
+ });
331
469
  res.statusCode = 500;
332
470
  res.end();
333
471
  }
@@ -447,30 +585,90 @@ class MasterControl {
447
585
  _registerCoreMiddleware(){
448
586
  var $that = this;
449
587
 
450
- // 1. Static File Serving
588
+ // 1. Static File Serving (with path traversal protection)
451
589
  $that.pipeline.use(async (ctx, next) => {
452
590
  if (ctx.isStatic) {
453
- // Serve static files
454
- 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
+ }
455
613
 
456
- 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) {
457
631
  if (!exist) {
458
632
  ctx.response.statusCode = 404;
459
- ctx.response.end(`File ${pathname} not found!`);
633
+ ctx.response.setHeader('Content-Type', 'text/plain');
634
+ ctx.response.end('Not Found');
460
635
  return;
461
636
  }
462
637
 
463
- if (fs.statSync(pathname).isDirectory()) {
464
- 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
+ }
465
653
  }
466
654
 
467
- fs.readFile(pathname, function(err, data) {
655
+ // Read and serve the file
656
+ fs.readFile(finalPath, function(err, data) {
468
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
+ });
469
664
  ctx.response.statusCode = 500;
470
- ctx.response.end(`Error getting the file: ${err}.`);
665
+ ctx.response.setHeader('Content-Type', 'text/plain');
666
+ ctx.response.end('Internal Server Error');
471
667
  } else {
472
- const mimeType = $that.router.findMimeType(path.parse(pathname).ext);
473
- 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');
474
672
  ctx.response.end(data);
475
673
  }
476
674
  });
@@ -489,7 +687,9 @@ class MasterControl {
489
687
  // 3. Request Body Parsing (always needed)
490
688
  $that.pipeline.use(async (ctx, next) => {
491
689
  // Parse body using MasterRequest
492
- 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);
493
693
 
494
694
  // Merge parsed params into context
495
695
  if (params && params.query) {
@@ -514,7 +714,15 @@ class MasterControl {
514
714
  // 4. HSTS Header (if enabled for HTTPS)
515
715
  $that.pipeline.use(async (ctx, next) => {
516
716
  if ($that.serverProtocol === 'https' && $that._hstsEnabled) {
517
- 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);
518
726
  }
519
727
  await next();
520
728
  });
@@ -585,6 +793,13 @@ class MasterControl {
585
793
 
586
794
  start(server){
587
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
+ }
588
803
  }
589
804
 
590
805
  startMVC(foldername){
@@ -619,7 +834,6 @@ class MasterControl {
619
834
  'MasterRouter': './MasterRouter',
620
835
  'MasterRequest': './MasterRequest',
621
836
  'MasterCors': './MasterCors',
622
- 'MasterSession': './MasterSession',
623
837
  'SessionSecurity': './security/SessionSecurity',
624
838
  'MasterSocket': './MasterSocket',
625
839
  'MasterHtml': './MasterHtml',
@@ -631,7 +845,12 @@ class MasterControl {
631
845
  for(var i = 0; i < requiredList.length; i++){
632
846
  const moduleName = requiredList[i];
633
847
  const modulePath = modulePathMap[moduleName] || './' + moduleName;
634
- 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
+ }
635
854
  }
636
855
  }
637
856
  }