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/MasterRequest.js CHANGED
@@ -20,15 +20,30 @@ class MasterRequest{
20
20
  this.options = {};
21
21
  this.options.disableFormidableMultipartFormData = options.disableFormidableMultipartFormData === null? false : options.disableFormidableMultipartFormData;
22
22
  this.options.formidable = options.formidable === null? {}: options.formidable;
23
+ // Body size limits (DoS protection)
24
+ this.options.maxBodySize = options.maxBodySize || 10 * 1024 * 1024; // 10MB default
25
+ this.options.maxJsonSize = options.maxJsonSize || 1 * 1024 * 1024; // 1MB default for JSON
26
+ this.options.maxTextSize = options.maxTextSize || 1 * 1024 * 1024; // 1MB default for text
23
27
  }
24
28
  }
25
29
 
26
- getRequestParam(request, res){
30
+ getRequestParam(requestOrContext, res){
27
31
  var $that = this;
28
32
  $that.response = res;
29
33
  try {
30
34
  return new Promise(function (resolve, reject) {
31
- var querydata = url.parse(request.requrl, true);
35
+ // BACKWARD COMPATIBILITY: Support both old and new patterns
36
+ // New pattern (v1.3.x pipeline): Pass context with requrl property
37
+ // Old pattern (pre-v1.3.x): Pass request with requrl property
38
+ const request = requestOrContext.request || requestOrContext;
39
+ let requrl = requestOrContext.requrl || request.requrl;
40
+
41
+ // Fallback: If requrl not set, parse from request.url
42
+ if (!requrl) {
43
+ requrl = url.parse(request.url, true);
44
+ }
45
+
46
+ var querydata = url.parse(requrl, true);
32
47
  $that.parsedURL.query = querydata.query;
33
48
  $that.form = new formidable.IncomingForm($that.options.formidable);
34
49
  if(request.headers['content-type'] || request.headers['transfer-encoding'] ){
@@ -43,16 +58,25 @@ class MasterRequest{
43
58
  case "multipart/form-data" :
44
59
  // Offer operturnity to add options. find a way to add dependecy injection. to request
45
60
  if(!$that.options.disableFormidableMultipartFormData){
46
-
61
+
47
62
  $that.parsedURL.formData = {
48
63
  files : {},
49
64
  fields : {}
50
65
  };
51
66
 
67
+ // Track uploaded files for cleanup on error
68
+ const uploadedFiles = [];
69
+ let uploadAborted = false;
70
+
52
71
  $that.form.on('field', function(field, value) {
53
72
  $that.parsedURL.formData.fields[field] = value;
54
73
  });
55
74
 
75
+ $that.form.on('fileBegin', function(formname, file) {
76
+ // Track file for potential cleanup
77
+ uploadedFiles.push(file);
78
+ });
79
+
56
80
  $that.form.on('file', function(field, file) {
57
81
  file.extension = file.name === undefined ? path.extname(file.originalFilename) : path.extname(file.name);
58
82
 
@@ -65,14 +89,55 @@ class MasterRequest{
65
89
  }
66
90
  });
67
91
 
92
+ $that.form.on('error', function(err) {
93
+ // CRITICAL: Handle upload errors
94
+ uploadAborted = true;
95
+ console.error('[MasterRequest] File upload error:', err.message);
96
+
97
+ // Cleanup temporary files
98
+ uploadedFiles.forEach(file => {
99
+ if (file.filepath) {
100
+ try {
101
+ $that.deleteFileBuffer(file.filepath);
102
+ } catch (cleanupErr) {
103
+ console.error('[MasterRequest] Failed to cleanup temp file:', cleanupErr.message);
104
+ }
105
+ }
106
+ });
107
+
108
+ reject(new Error(`File upload failed: ${err.message}`));
109
+ });
110
+
111
+ $that.form.on('aborted', function() {
112
+ // CRITICAL: Handle client abort (connection closed)
113
+ uploadAborted = true;
114
+ console.warn('[MasterRequest] File upload aborted by client');
115
+
116
+ // Cleanup temporary files
117
+ uploadedFiles.forEach(file => {
118
+ if (file.filepath) {
119
+ try {
120
+ $that.deleteFileBuffer(file.filepath);
121
+ } catch (cleanupErr) {
122
+ console.error('[MasterRequest] Failed to cleanup temp file:', cleanupErr.message);
123
+ }
124
+ }
125
+ });
126
+
127
+ reject(new Error('File upload aborted by client'));
128
+ });
129
+
68
130
  $that.form.on('end', function() {
69
- resolve($that.parsedURL);
131
+ // Only resolve if upload wasn't aborted
132
+ if (!uploadAborted) {
133
+ resolve($that.parsedURL);
134
+ }
70
135
  });
71
136
 
72
137
  $that.form.parse(request);
73
-
138
+
74
139
  }else{
75
-
140
+
76
141
  resolve($that.parsedURL);
77
142
  console.log("skipped multipart/form-data")
78
143
  }
@@ -124,38 +189,59 @@ class MasterRequest{
124
189
  let body = '';
125
190
  let receivedBytes = 0;
126
191
  const maxBytes = 1 * 1024 * 1024; // 1MB limit
127
-
128
-
192
+ let errorOccurred = false;
193
+
129
194
  try {
130
195
 
131
196
  request.on('data', (chunk) => {
197
+ if (errorOccurred) return;
198
+
132
199
  receivedBytes += chunk.length;
133
-
200
+
134
201
  // Prevent memory overload
135
202
  if (receivedBytes > maxBytes) {
136
- req.destroy(); // Close the connection
203
+ errorOccurred = true;
204
+ request.destroy(); // ✅ Fixed: was 'req', now 'request'
205
+ console.error(`Plain text payload too large: ${receivedBytes} bytes (max: ${maxBytes})`);
206
+ func({ error: 'Payload too large', maxSize: maxBytes });
137
207
  return;
138
208
  }
139
-
209
+
140
210
  // Append chunk to body
141
211
  body += chunk.toString('utf8');
142
212
  });
143
-
213
+
144
214
  request.on('end', () => {
215
+ if (errorOccurred) return;
216
+
145
217
  try {
146
218
  // Process the plain text data here
147
219
  const responseData = body;
148
- func(responseData );
220
+ func(responseData);
149
221
  } catch (err) {
150
-
151
222
  console.error('Processing error handling text/plain:', err);
152
- throw err;
223
+ func({ error: err.message });
153
224
  }
154
225
 
155
226
  });
227
+
228
+ request.on('error', (err) => {
229
+ if (errorOccurred) return;
230
+ errorOccurred = true;
231
+ console.error('[MasterRequest] Stream error in fetchData:', err.message);
232
+ func({ error: err.message });
233
+ });
234
+
235
+ request.on('aborted', () => {
236
+ if (errorOccurred) return;
237
+ errorOccurred = true;
238
+ console.warn('[MasterRequest] Request aborted in fetchData');
239
+ func({ error: 'Request aborted' });
240
+ });
241
+
156
242
  } catch (error) {
157
243
  console.error("Failed to fetch data:", error);
158
- throw error;
244
+ func({ error: error.message });
159
245
  }
160
246
  }
161
247
 
@@ -170,56 +256,148 @@ class MasterRequest{
170
256
 
171
257
  urlEncodeStream(request, func){
172
258
  const decoder = new StringDecoder('utf-8');
173
- //request.pipe(decoder);
174
259
  let buffer = '';
260
+ let receivedBytes = 0;
261
+ const maxBytes = this.options.maxBodySize || 10 * 1024 * 1024; // 10MB limit
262
+ let errorOccurred = false;
263
+
175
264
  request.on('data', (chunk) => {
265
+ if (errorOccurred) return;
266
+
267
+ receivedBytes += chunk.length;
268
+
269
+ // Prevent memory overload (DoS protection)
270
+ if (receivedBytes > maxBytes) {
271
+ errorOccurred = true;
272
+ request.destroy();
273
+ console.error(`Form data too large: ${receivedBytes} bytes (max: ${maxBytes})`);
274
+ func({ error: 'Payload too large', maxSize: maxBytes });
275
+ return;
276
+ }
277
+
176
278
  buffer += decoder.write(chunk);
177
279
  });
178
280
 
179
281
  request.on('end', () => {
282
+ if (errorOccurred) return;
283
+
180
284
  buffer += decoder.end();
181
285
  var buff = qs.parse(buffer);
182
286
  func(buff);
183
287
  });
184
288
 
185
- }
289
+ request.on('error', (err) => {
290
+ if (errorOccurred) return;
291
+ errorOccurred = true;
292
+ console.error('[MasterRequest] Stream error in urlEncodeStream:', err.message);
293
+ func({ error: err.message });
294
+ });
186
295
 
187
- stringToJson(request, func){
296
+ request.on('aborted', () => {
297
+ if (errorOccurred) return;
298
+ errorOccurred = true;
299
+ console.warn('[MasterRequest] Request aborted in urlEncodeStream');
300
+ func({ error: 'Request aborted' });
301
+ });
188
302
 
189
303
  }
190
-
304
+
191
305
  jsonStream(request, func){
192
- //request.pipe(decoder);
193
306
  let buffer = '';
307
+ let receivedBytes = 0;
308
+ const maxBytes = this.options.maxJsonSize || 1 * 1024 * 1024; // 1MB limit
309
+ let errorOccurred = false;
310
+
194
311
  request.on('data', (chunk) => {
312
+ if (errorOccurred) return;
313
+
314
+ receivedBytes += chunk.length;
315
+
316
+ // Prevent memory overload (DoS protection)
317
+ if (receivedBytes > maxBytes) {
318
+ errorOccurred = true;
319
+ request.destroy();
320
+ console.error(`JSON payload too large: ${receivedBytes} bytes (max: ${maxBytes})`);
321
+ func({ error: 'JSON payload too large', maxSize: maxBytes });
322
+ return;
323
+ }
324
+
195
325
  buffer += chunk;
196
326
  });
197
327
 
198
328
  request.on('end', () => {
329
+ if (errorOccurred) return;
330
+
199
331
  try {
200
332
  var buff = JSON.parse(buffer);
201
333
  func(buff);
202
334
  } catch (e) {
203
- var buff = qs.parse(buffer);
204
- func(buff);
335
+ // Security: Don't fallback to qs.parse to avoid prototype pollution
336
+ console.error('Invalid JSON payload:', e.message);
337
+ func({ error: 'Invalid JSON', details: e.message });
205
338
  }
206
339
  });
207
340
 
341
+ request.on('error', (err) => {
342
+ if (errorOccurred) return;
343
+ errorOccurred = true;
344
+ console.error('[MasterRequest] Stream error in jsonStream:', err.message);
345
+ func({ error: err.message });
346
+ });
347
+
348
+ request.on('aborted', () => {
349
+ if (errorOccurred) return;
350
+ errorOccurred = true;
351
+ console.warn('[MasterRequest] Request aborted in jsonStream');
352
+ func({ error: 'Request aborted' });
353
+ });
354
+
208
355
  }
209
356
 
210
357
  textStream(request, func){
211
358
  const decoder = new StringDecoder('utf-8');
212
- //request.pipe(decoder);
213
359
  let buffer = '';
360
+ let receivedBytes = 0;
361
+ const maxBytes = this.options.maxTextSize || 1 * 1024 * 1024; // 1MB limit
362
+ let errorOccurred = false;
363
+
214
364
  request.on('data', (chunk) => {
365
+ if (errorOccurred) return;
366
+
367
+ receivedBytes += chunk.length;
368
+
369
+ // Prevent memory overload (DoS protection)
370
+ if (receivedBytes > maxBytes) {
371
+ errorOccurred = true;
372
+ request.destroy();
373
+ console.error(`Text payload too large: ${receivedBytes} bytes (max: ${maxBytes})`);
374
+ func({ error: 'Text payload too large', maxSize: maxBytes });
375
+ return;
376
+ }
377
+
215
378
  buffer += decoder.write(chunk);
216
379
  });
217
380
 
218
381
  request.on('end', () => {
382
+ if (errorOccurred) return;
219
383
  buffer += decoder.end();
220
384
  func(buffer);
221
385
  });
222
386
 
387
+ request.on('error', (err) => {
388
+ if (errorOccurred) return;
389
+ errorOccurred = true;
390
+ console.error('[MasterRequest] Stream error in textStream:', err.message);
391
+ func({ error: err.message });
392
+ });
393
+
394
+ request.on('aborted', () => {
395
+ if (errorOccurred) return;
396
+ errorOccurred = true;
397
+ console.warn('[MasterRequest] Request aborted in textStream');
398
+ func({ error: 'Request aborted' });
399
+ });
400
+
223
401
  }
224
402
 
225
403
  // have a clear all object that you can run that will delete all rununing objects
package/MasterSocket.js CHANGED
@@ -26,7 +26,12 @@ class MasterSocket{
26
26
  // Prefer explicit server, fallback to master.server
27
27
  const httpServer = serverOrIo || master.server;
28
28
  if (!httpServer) {
29
- throw new Error('MasterSocket.init requires an HTTP server or a pre-created Socket.IO instance');
29
+ throw new Error(
30
+ 'MasterSocket.init requires an HTTP server. ' +
31
+ 'Either pass the server explicitly: master.socket.init(server) ' +
32
+ 'or call master.start(server) before socket.init(). ' +
33
+ 'Current initialization order issue: socket.init() called before master.start()'
34
+ );
30
35
  }
31
36
  this.io = new Server(httpServer, ioOptions);
32
37
  }