mastercontroller 1.3.10 → 1.3.13

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
@@ -10,6 +10,7 @@ var fs = require('fs');
10
10
  var url = require('url');
11
11
  var path = require('path');
12
12
  var globSearch = require("glob");
13
+ var crypto = require('crypto'); // CRITICAL FIX: For ETag generation
13
14
 
14
15
  // Enhanced error handling - setup global handlers
15
16
  const { setupGlobalErrorHandlers } = require('./error/MasterErrorMiddleware');
@@ -779,26 +780,107 @@ class MasterControl {
779
780
  }
780
781
  }
781
782
 
782
- // Read and serve the file
783
- fs.readFile(finalPath, function(err, data) {
784
- if (err) {
783
+ // CRITICAL FIX: Stream large files instead of reading into memory
784
+ // Files >1MB are streamed to prevent memory exhaustion and improve performance
785
+ const STREAM_THRESHOLD = 1 * 1024 * 1024; // 1MB
786
+ const fileSize = stats.isDirectory() ? fs.statSync(finalPath).size : stats.size;
787
+ const ext = path.extname(finalPath);
788
+ const mimeType = $that.router.findMimeType(ext);
789
+
790
+ // CRITICAL FIX: Generate ETag for caching (based on file stats)
791
+ // ETag format: "size-mtime" (weak ETag for better performance)
792
+ const fileStats = stats.isDirectory() ? fs.statSync(finalPath) : stats;
793
+ const etag = `W/"${fileStats.size}-${fileStats.mtime.getTime()}"`;
794
+
795
+ // CRITICAL FIX: Check If-None-Match header for 304 Not Modified
796
+ const clientETag = ctx.request.headers['if-none-match'];
797
+ if (clientETag === etag) {
798
+ // File hasn't changed, return 304 Not Modified
799
+ logger.debug({
800
+ code: 'MC_STATIC_304',
801
+ message: 'Returning 304 Not Modified',
802
+ path: finalPath,
803
+ etag: etag
804
+ });
805
+ ctx.response.statusCode = 304;
806
+ ctx.response.setHeader('ETag', etag);
807
+ ctx.response.end();
808
+ return;
809
+ }
810
+
811
+ // Set common headers for both streaming and buffered responses
812
+ ctx.response.setHeader('Content-Type', mimeType || 'application/octet-stream');
813
+ ctx.response.setHeader('X-Content-Type-Options', 'nosniff');
814
+ ctx.response.setHeader('Content-Length', fileSize);
815
+
816
+ // CRITICAL FIX: Add caching headers
817
+ ctx.response.setHeader('ETag', etag);
818
+ ctx.response.setHeader('Last-Modified', fileStats.mtime.toUTCString());
819
+
820
+ // Cache-Control based on file type
821
+ const cacheableExtensions = ['.js', '.css', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
822
+ const isCacheable = cacheableExtensions.includes(ext.toLowerCase());
823
+
824
+ if (isCacheable) {
825
+ // PERFORMANCE: Cache static assets for 1 year (immutable pattern)
826
+ // Use versioned URLs (e.g., app.v123.js) for cache-busting
827
+ ctx.response.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
828
+ } else {
829
+ // SECURITY: Dynamic content should revalidate
830
+ ctx.response.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
831
+ }
832
+
833
+ if (fileSize > STREAM_THRESHOLD) {
834
+ // PERFORMANCE: Stream large files (>1MB) to avoid memory issues
835
+ logger.debug({
836
+ code: 'MC_STATIC_STREAMING',
837
+ message: 'Streaming large static file',
838
+ path: finalPath,
839
+ size: fileSize
840
+ });
841
+
842
+ const readStream = fs.createReadStream(finalPath);
843
+
844
+ readStream.on('error', (err) => {
785
845
  logger.error({
786
- code: 'MC_ERR_FILE_READ',
787
- message: 'Error reading static file',
846
+ code: 'MC_ERR_STREAM_READ',
847
+ message: 'Error streaming static file',
788
848
  path: finalPath,
789
849
  error: err.message
790
850
  });
791
- ctx.response.statusCode = 500;
792
- ctx.response.setHeader('Content-Type', 'text/plain');
793
- ctx.response.end('Internal Server Error');
794
- } else {
795
- const ext = path.extname(finalPath);
796
- const mimeType = $that.router.findMimeType(ext);
797
- ctx.response.setHeader('Content-Type', mimeType || 'application/octet-stream');
798
- ctx.response.setHeader('X-Content-Type-Options', 'nosniff');
799
- ctx.response.end(data);
800
- }
801
- });
851
+
852
+ // Only send error if headers not sent
853
+ if (!ctx.response.headersSent) {
854
+ ctx.response.statusCode = 500;
855
+ ctx.response.setHeader('Content-Type', 'text/plain');
856
+ ctx.response.end('Internal Server Error');
857
+ } else {
858
+ // Connection already started, just close it
859
+ ctx.response.end();
860
+ }
861
+ });
862
+
863
+ // Pipe the file stream to the response
864
+ readStream.pipe(ctx.response);
865
+
866
+ } else {
867
+ // PERFORMANCE: Small files (<1MB) can be buffered for better caching
868
+ fs.readFile(finalPath, function(err, data) {
869
+ if (err) {
870
+ logger.error({
871
+ code: 'MC_ERR_FILE_READ',
872
+ message: 'Error reading static file',
873
+ path: finalPath,
874
+ error: err.message
875
+ });
876
+ ctx.response.statusCode = 500;
877
+ ctx.response.setHeader('Content-Type', 'text/plain');
878
+ ctx.response.end('Internal Server Error');
879
+ } else {
880
+ ctx.response.end(data);
881
+ }
882
+ });
883
+ }
802
884
  });
803
885
 
804
886
  return; // Terminal - don't call next()
package/MasterRequest.js CHANGED
@@ -26,7 +26,20 @@ class MasterRequest{
26
26
  if(options){
27
27
  this.options = {};
28
28
  this.options.disableFormidableMultipartFormData = options.disableFormidableMultipartFormData === null? false : options.disableFormidableMultipartFormData;
29
- this.options.formidable = options.formidable === null? {}: options.formidable;
29
+
30
+ // CRITICAL FIX: Add file upload limits to prevent DoS attacks
31
+ // Default formidable configuration with security limits
32
+ this.options.formidable = {
33
+ maxFiles: 10, // CRITICAL: Limit number of files per request
34
+ maxFileSize: 50 * 1024 * 1024, // CRITICAL: 50MB per file (was unlimited)
35
+ maxTotalFileSize: 100 * 1024 * 1024, // CRITICAL: 100MB total for all files
36
+ maxFields: 1000, // Limit number of fields
37
+ maxFieldsSize: 20 * 1024 * 1024, // 20MB for all fields combined
38
+ allowEmptyFiles: false, // Reject empty file uploads
39
+ minFileSize: 1, // Minimum 1 byte
40
+ ...(options.formidable || {}) // Allow user overrides
41
+ };
42
+
30
43
  // Body size limits (DoS protection)
31
44
  this.options.maxBodySize = options.maxBodySize || 10 * 1024 * 1024; // 10MB default
32
45
  this.options.maxJsonSize = options.maxJsonSize || 1 * 1024 * 1024; // 1MB default for JSON
@@ -74,6 +87,7 @@ class MasterRequest{
74
87
  // Track uploaded files for cleanup on error
75
88
  const uploadedFiles = [];
76
89
  let uploadAborted = false;
90
+ let totalUploadedSize = 0; // CRITICAL: Track total upload size
77
91
 
78
92
  $that.form.on('field', function(field, value) {
79
93
  $that.parsedURL.formData.fields[field] = value;
@@ -87,6 +101,30 @@ class MasterRequest{
87
101
  $that.form.on('file', function(field, file) {
88
102
  file.extension = file.name === undefined ? path.extname(file.originalFilename) : path.extname(file.name);
89
103
 
104
+ // CRITICAL: Track total uploaded size across all files
105
+ totalUploadedSize += file.size || 0;
106
+
107
+ // CRITICAL: Enforce maxTotalFileSize limit
108
+ const maxTotalSize = $that.options.formidable.maxTotalFileSize || 100 * 1024 * 1024;
109
+ if (totalUploadedSize > maxTotalSize) {
110
+ uploadAborted = true;
111
+ console.error(`[MasterRequest] Total upload size (${totalUploadedSize} bytes) exceeds limit (${maxTotalSize} bytes)`);
112
+
113
+ // Cleanup all uploaded files
114
+ uploadedFiles.forEach(f => {
115
+ if (f.filepath) {
116
+ try {
117
+ $that.deleteFileBuffer(f.filepath);
118
+ } catch (err) {
119
+ console.error('[MasterRequest] Cleanup failed:', err.message);
120
+ }
121
+ }
122
+ });
123
+
124
+ reject(new Error(`Total upload size exceeds limit (${maxTotalSize} bytes)`));
125
+ return;
126
+ }
127
+
90
128
  if(Array.isArray($that.parsedURL.formData.files[field])){
91
129
  $that.parsedURL.formData.files[field].push(file);
92
130
  }
@@ -94,6 +132,9 @@ class MasterRequest{
94
132
  $that.parsedURL.formData.files[field] = [];
95
133
  $that.parsedURL.formData.files[field].push(file);
96
134
  }
135
+
136
+ // CRITICAL: Log file upload for security audit trail
137
+ console.log(`[MasterRequest] File uploaded: ${file.originalFilename || file.name} (${file.size} bytes)`);
97
138
  });
98
139
 
99
140
  $that.form.on('error', function(err) {
package/MasterRouter.js CHANGED
@@ -238,10 +238,14 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
238
238
  }
239
239
  };
240
240
 
241
- var loadScopedListClasses = function(){
241
+ // CRITICAL FIX: Race condition - Store scoped services in context instead of shared requestList
242
+ // Previously, concurrent requests would overwrite each other's services in the shared requestList object
243
+ // This caused unpredictable behavior and data corruption in production environments
244
+ var loadScopedListClasses = function(context){
242
245
  // FIXED: Use Object.entries() for safe iteration (prevents prototype pollution)
243
246
  for (const [key, className] of Object.entries(this._master._scopedList)) {
244
- this._master.requestList[key] = new className();
247
+ // Store scoped services in the context object (request-specific) instead of shared requestList
248
+ context[key] = new className();
245
249
  }
246
250
  };
247
251
 
@@ -419,8 +423,11 @@ class MasterRouter {
419
423
  const requestId = `${Date.now()}-${Math.random()}`;
420
424
  performanceTracker.start(requestId, requestObject);
421
425
 
422
- tools.combineObjects(this._master.requestList, requestObject);
423
- requestObject = this._master.requestList;
426
+ // CRITICAL FIX: Create a request-specific context instead of using shared requestList
427
+ // This prevents race conditions where concurrent requests overwrite each other's services
428
+ const requestContext = Object.create(this._master.requestList);
429
+ tools.combineObjects(requestContext, requestObject);
430
+ requestObject = requestContext;
424
431
  var Control = null;
425
432
 
426
433
  try{
@@ -528,9 +535,12 @@ class MasterRouter {
528
535
 
529
536
  load(rr){ // load the the router
530
537
 
531
- loadScopedListClasses.call(this);
532
538
  var $that = this;
533
539
  var requestObject = Object.create(rr);
540
+
541
+ // CRITICAL FIX: Load scoped services into request-specific context
542
+ // Pass requestObject so scoped services are stored per-request, not globally
543
+ loadScopedListClasses.call(this, requestObject);
534
544
  requestObject.pathName = requestObject.pathName.replace(/^\/|\/$/g, '').toLowerCase();
535
545
 
536
546
  var _loadEmit = new EventEmitter();