mastercontroller 1.3.1 → 1.3.3

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,83 @@ 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.
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
+ // Explicit module registration (prevents circular dependency issues)
323
+ // This is the Google-style dependency injection pattern
324
+ const moduleRegistry = {
325
+ 'pipeline': { path: './MasterPipeline', exportName: 'MasterPipeline' },
326
+ 'timeout': { path: './MasterTimeout', exportName: 'MasterTimeout' },
327
+ 'errorRenderer': { path: './error/MasterErrorRenderer', exportName: 'MasterErrorRenderer' },
328
+ 'error': { path: './error/MasterError', exportName: 'MasterError' },
329
+ 'router': { path: './MasterRouter', exportName: 'MasterRouter' },
330
+ 'request': { path: './MasterRequest', exportName: 'MasterRequest' },
331
+ 'cors': { path: './MasterCors', exportName: 'MasterCors' },
332
+ 'socket': { path: './MasterSocket', exportName: 'MasterSocket' },
333
+ 'tempdata': { path: './MasterTemp', exportName: 'MasterTemp' },
334
+ 'overwrite': { path: './TemplateOverwrite', exportName: 'TemplateOverwrite' },
335
+ 'session': { path: './security/SessionSecurity', exportName: 'MasterSessionSecurity' }
336
+ };
337
+
338
+ for (const [name, config] of Object.entries(moduleRegistry)) {
339
+ try {
340
+ const module = require(config.path);
341
+ const ClassConstructor = module[config.exportName] || module;
342
+
343
+ if (ClassConstructor) {
344
+ $that[name] = new ClassConstructor();
345
+ } else {
346
+ console.warn(`[MasterControl] Module ${name} does not export ${config.exportName}`);
347
+ }
348
+ } catch (e) {
349
+ console.error(`[MasterControl] Failed to load ${name}:`, e.message);
350
+ }
351
+ }
352
+
353
+ // Load view and controller extensions (these extend prototypes, not master instance)
261
354
  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
- ]);
355
+ require('./MasterAction');
356
+ require('./MasterActionFilters');
357
+ require('./MasterHtml');
358
+ require('./MasterTemplate');
359
+ require('./MasterTools');
280
360
  } catch (e) {
281
- console.error('[MasterControl] Failed to load internal tools:', e && e.message);
361
+ console.error('[MasterControl] Failed to load extensions:', e.message);
282
362
  }
283
363
 
364
+ // Initialize global error handlers
365
+ setupGlobalErrorHandlers();
366
+
284
367
  // Register core middleware that must run for framework to function
285
368
  $that._registerCoreMiddleware();
286
369
 
287
370
  if(type === "http"){
288
371
  $that.serverProtocol = "http";
289
- return http.createServer(async function(req, res) {
372
+ const server = http.createServer(async function(req, res) {
290
373
  $that.serverRun(req, res);
291
374
  });
375
+ // Set server immediately so config can access it
376
+ $that.server = server;
377
+ return server;
292
378
  }
293
379
  if(type === "https"){
294
380
  $that.serverProtocol = "https";
@@ -297,14 +383,42 @@ class MasterControl {
297
383
  $that._initializeTlsFromEnv();
298
384
  credentials = $that._tlsOptions;
299
385
  }
300
- // Apply secure defaults if missing
386
+ // Apply secure defaults if missing (2026 security standards)
301
387
  if(credentials){
302
- if(!credentials.minVersion){ credentials.minVersion = 'TLSv1.2'; }
388
+ // Default to TLS 1.3 for security (2026 standard)
389
+ // TLS 1.2 still supported but not default
390
+ if(!credentials.minVersion){
391
+ credentials.minVersion = 'TLSv1.3';
392
+ console.log('[MasterControl] TLS 1.3 enabled by default (recommended for 2026)');
393
+ }
394
+
395
+ // Secure cipher suites (Mozilla Intermediate configuration - 2026)
396
+ // Supports TLS 1.3 and TLS 1.2 for backward compatibility
397
+ if(!credentials.ciphers){
398
+ credentials.ciphers = [
399
+ // TLS 1.3 cipher suites (strongest)
400
+ 'TLS_AES_256_GCM_SHA384',
401
+ 'TLS_CHACHA20_POLY1305_SHA256',
402
+ 'TLS_AES_128_GCM_SHA256',
403
+ // TLS 1.2 cipher suites (backward compatibility)
404
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
405
+ 'ECDHE-RSA-AES256-GCM-SHA384',
406
+ 'ECDHE-ECDSA-CHACHA20-POLY1305',
407
+ 'ECDHE-RSA-CHACHA20-POLY1305',
408
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
409
+ 'ECDHE-RSA-AES128-GCM-SHA256'
410
+ ].join(':');
411
+ console.log('[MasterControl] Secure cipher suites configured (Mozilla Intermediate)');
412
+ }
413
+
303
414
  if(credentials.honorCipherOrder === undefined){ credentials.honorCipherOrder = true; }
304
415
  if(!credentials.ALPNProtocols){ credentials.ALPNProtocols = ['h2', 'http/1.1']; }
305
- return https.createServer(credentials, async function(req, res) {
416
+ const server = https.createServer(credentials, async function(req, res) {
306
417
  $that.serverRun(req, res);
307
418
  });
419
+ // Set server immediately so config can access it
420
+ $that.server = server;
421
+ return server;
308
422
  }else{
309
423
  throw "Credentials needed to setup https"
310
424
  }
@@ -316,18 +430,69 @@ class MasterControl {
316
430
  }
317
431
  }
318
432
 
319
- // Creates an HTTP server that 301-redirects to HTTPS counterpart
320
- startHttpToHttpsRedirect(redirectPort, bindHost){
433
+ /**
434
+ * Creates an HTTP server that 301-redirects to HTTPS counterpart
435
+ * SECURITY: Validates host header to prevent open redirect attacks
436
+ *
437
+ * @param {Number} redirectPort - Port to listen on (usually 80)
438
+ * @param {String} bindHost - Host to bind to (e.g., '0.0.0.0')
439
+ * @param {Array<String>} allowedHosts - Whitelist of allowed hostnames (REQUIRED for security)
440
+ * @returns {http.Server} - HTTP server instance
441
+ *
442
+ * @example
443
+ * // Production usage (MUST specify allowed hosts)
444
+ * const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
445
+ * 'example.com',
446
+ * 'www.example.com',
447
+ * 'api.example.com'
448
+ * ]);
449
+ *
450
+ * @security CRITICAL: Always provide allowedHosts in production to prevent open redirect attacks
451
+ */
452
+ startHttpToHttpsRedirect(redirectPort, bindHost, allowedHosts = []){
321
453
  var $that = this;
454
+
455
+ // Security warning if no hosts specified
456
+ if (allowedHosts.length === 0) {
457
+ console.warn('[MasterControl] ⚠️ SECURITY WARNING: startHttpToHttpsRedirect() called without allowedHosts.');
458
+ console.warn('[MasterControl] This is vulnerable to open redirect attacks. Specify allowed hosts:');
459
+ console.warn('[MasterControl] master.startHttpToHttpsRedirect(80, "0.0.0.0", ["example.com", "www.example.com"])');
460
+ }
461
+
322
462
  return http.createServer(function (req, res) {
323
463
  try{
324
464
  var host = req.headers['host'] || '';
325
- // Force original host, just change scheme
465
+ var hostname = host.split(':')[0]; // Remove port number
466
+
467
+ // CRITICAL SECURITY: Validate host header to prevent open redirect attacks
468
+ if (allowedHosts.length > 0) {
469
+ if (!allowedHosts.includes(hostname)) {
470
+ logger.warn({
471
+ code: 'MC_SECURITY_INVALID_HOST',
472
+ message: 'HTTP redirect blocked: invalid host header',
473
+ host: hostname,
474
+ ip: req.connection.remoteAddress
475
+ });
476
+ res.statusCode = 400;
477
+ res.setHeader('Content-Type', 'text/plain');
478
+ res.end('Bad Request: Invalid host header');
479
+ return;
480
+ }
481
+ }
482
+
483
+ // Redirect to HTTPS with validated host
326
484
  var location = 'https://' + host + req.url;
327
485
  res.statusCode = 301;
328
486
  res.setHeader('Location', location);
487
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
329
488
  res.end();
330
489
  }catch(e){
490
+ logger.error({
491
+ code: 'MC_ERR_REDIRECT',
492
+ message: 'HTTP to HTTPS redirect failed',
493
+ error: e.message,
494
+ stack: e.stack
495
+ });
331
496
  res.statusCode = 500;
332
497
  res.end();
333
498
  }
@@ -447,30 +612,90 @@ class MasterControl {
447
612
  _registerCoreMiddleware(){
448
613
  var $that = this;
449
614
 
450
- // 1. Static File Serving
615
+ // 1. Static File Serving (with path traversal protection)
451
616
  $that.pipeline.use(async (ctx, next) => {
452
617
  if (ctx.isStatic) {
453
- // Serve static files
454
- let pathname = `.${ctx.request.url}`;
618
+ // SECURITY: Prevent path traversal attacks
619
+ let requestedPath = ctx.request.url;
620
+
621
+ // Normalize the path and resolve it
622
+ const publicRoot = path.resolve($that.root || '.');
623
+ const safePath = path.join(publicRoot, requestedPath);
624
+ const resolvedPath = path.resolve(safePath);
625
+
626
+ // CRITICAL: Ensure resolved path is within public root (prevents ../ attacks)
627
+ if (!resolvedPath.startsWith(publicRoot)) {
628
+ logger.warn({
629
+ code: 'MC_SECURITY_PATH_TRAVERSAL',
630
+ message: 'Path traversal attack blocked',
631
+ requestedPath: requestedPath,
632
+ resolvedPath: resolvedPath,
633
+ ip: ctx.request.connection.remoteAddress
634
+ });
635
+ ctx.response.statusCode = 403;
636
+ ctx.response.setHeader('Content-Type', 'text/plain');
637
+ ctx.response.end('Forbidden');
638
+ return;
639
+ }
640
+
641
+ // SECURITY: Block dotfiles (.env, .git, .htaccess, etc.)
642
+ const filename = path.basename(resolvedPath);
643
+ if (filename.startsWith('.')) {
644
+ logger.warn({
645
+ code: 'MC_SECURITY_DOTFILE_BLOCKED',
646
+ message: 'Dotfile access blocked',
647
+ filename: filename,
648
+ ip: ctx.request.connection.remoteAddress
649
+ });
650
+ ctx.response.statusCode = 403;
651
+ ctx.response.setHeader('Content-Type', 'text/plain');
652
+ ctx.response.end('Forbidden');
653
+ return;
654
+ }
455
655
 
456
- fs.exists(pathname, function (exist) {
656
+ // Check if file exists
657
+ fs.exists(resolvedPath, function (exist) {
457
658
  if (!exist) {
458
659
  ctx.response.statusCode = 404;
459
- ctx.response.end(`File ${pathname} not found!`);
660
+ ctx.response.setHeader('Content-Type', 'text/plain');
661
+ ctx.response.end('Not Found');
460
662
  return;
461
663
  }
462
664
 
463
- if (fs.statSync(pathname).isDirectory()) {
464
- pathname += '/index' + path.parse(pathname).ext;
665
+ // Get file stats
666
+ let finalPath = resolvedPath;
667
+ const stats = fs.statSync(resolvedPath);
668
+
669
+ // If directory, try to serve index.html
670
+ if (stats.isDirectory()) {
671
+ finalPath = path.join(resolvedPath, 'index.html');
672
+
673
+ // Check if index.html exists
674
+ if (!fs.existsSync(finalPath)) {
675
+ ctx.response.statusCode = 403;
676
+ ctx.response.setHeader('Content-Type', 'text/plain');
677
+ ctx.response.end('Forbidden');
678
+ return;
679
+ }
465
680
  }
466
681
 
467
- fs.readFile(pathname, function(err, data) {
682
+ // Read and serve the file
683
+ fs.readFile(finalPath, function(err, data) {
468
684
  if (err) {
685
+ logger.error({
686
+ code: 'MC_ERR_FILE_READ',
687
+ message: 'Error reading static file',
688
+ path: finalPath,
689
+ error: err.message
690
+ });
469
691
  ctx.response.statusCode = 500;
470
- ctx.response.end(`Error getting the file: ${err}.`);
692
+ ctx.response.setHeader('Content-Type', 'text/plain');
693
+ ctx.response.end('Internal Server Error');
471
694
  } else {
472
- const mimeType = $that.router.findMimeType(path.parse(pathname).ext);
473
- ctx.response.setHeader('Content-type', mimeType || 'text/plain');
695
+ const ext = path.extname(finalPath);
696
+ const mimeType = $that.router.findMimeType(ext);
697
+ ctx.response.setHeader('Content-Type', mimeType || 'application/octet-stream');
698
+ ctx.response.setHeader('X-Content-Type-Options', 'nosniff');
474
699
  ctx.response.end(data);
475
700
  }
476
701
  });
@@ -489,7 +714,9 @@ class MasterControl {
489
714
  // 3. Request Body Parsing (always needed)
490
715
  $that.pipeline.use(async (ctx, next) => {
491
716
  // Parse body using MasterRequest
492
- const params = await $that.request.getRequestParam(ctx.request, ctx.response);
717
+ // Pass entire context for backward compatibility (v1.3.x)
718
+ // getRequestParam() will extract request and requrl from context
719
+ const params = await $that.request.getRequestParam(ctx, ctx.response);
493
720
 
494
721
  // Merge parsed params into context
495
722
  if (params && params.query) {
@@ -514,7 +741,15 @@ class MasterControl {
514
741
  // 4. HSTS Header (if enabled for HTTPS)
515
742
  $that.pipeline.use(async (ctx, next) => {
516
743
  if ($that.serverProtocol === 'https' && $that._hstsEnabled) {
517
- ctx.response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
744
+ // Use configured HSTS values (not hardcoded)
745
+ let hstsValue = `max-age=${$that._hstsMaxAge}`;
746
+ if ($that._hstsIncludeSubDomains) {
747
+ hstsValue += '; includeSubDomains';
748
+ }
749
+ if ($that._hstsPreload) {
750
+ hstsValue += '; preload';
751
+ }
752
+ ctx.response.setHeader('Strict-Transport-Security', hstsValue);
518
753
  }
519
754
  await next();
520
755
  });
@@ -585,6 +820,13 @@ class MasterControl {
585
820
 
586
821
  start(server){
587
822
  this.server = server;
823
+
824
+ // Apply any pending server settings that were called before start()
825
+ if (this._pendingServerSettings) {
826
+ console.log('[MasterControl] Applying pending server settings');
827
+ this.serverSettings(this._pendingServerSettings);
828
+ this._pendingServerSettings = null;
829
+ }
588
830
  }
589
831
 
590
832
  startMVC(foldername){
@@ -619,7 +861,6 @@ class MasterControl {
619
861
  'MasterRouter': './MasterRouter',
620
862
  'MasterRequest': './MasterRequest',
621
863
  'MasterCors': './MasterCors',
622
- 'MasterSession': './MasterSession',
623
864
  'SessionSecurity': './security/SessionSecurity',
624
865
  'MasterSocket': './MasterSocket',
625
866
  'MasterHtml': './MasterHtml',
@@ -631,7 +872,12 @@ class MasterControl {
631
872
  for(var i = 0; i < requiredList.length; i++){
632
873
  const moduleName = requiredList[i];
633
874
  const modulePath = modulePathMap[moduleName] || './' + moduleName;
634
- require(modulePath);
875
+ const module = require(modulePath);
876
+
877
+ // Special handling for SessionSecurity to avoid circular dependency
878
+ if (moduleName === 'SessionSecurity' && module.MasterSessionSecurity) {
879
+ this.session = new module.MasterSessionSecurity();
880
+ }
635
881
  }
636
882
  }
637
883
  }
package/MasterCors.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // version 0.0.3 - robust origin handling (all envs), creds-safe reflection, function origins, extended Vary
2
- var master = require('./MasterControl');
3
2
 
4
3
  // todo - res.setHeader('Access-Control-Request-Method', '*');
5
4
  class MasterCors{
@@ -198,4 +197,4 @@ class MasterCors{
198
197
  }
199
198
  }
200
199
 
201
- master.extend("cors", MasterCors);
200
+ module.exports = { MasterCors };