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/.claude/settings.local.json +3 -1
- package/MasterAction.js +137 -23
- package/MasterActionFilters.js +197 -92
- package/MasterControl.js +264 -45
- package/MasterHtml.js +226 -143
- package/MasterRequest.js +202 -24
- package/MasterSocket.js +6 -1
- package/MasterTools.js +388 -0
- package/README.md +2288 -369
- package/SECURITY-FIXES-v1.3.2.md +614 -0
- package/docs/SECURITY-AUDIT-ACTION-SYSTEM.md +1374 -0
- package/docs/SECURITY-AUDIT-HTTPS.md +1056 -0
- package/docs/SECURITY-QUICKSTART.md +375 -0
- package/docs/timeout-and-error-handling.md +8 -6
- package/package.json +1 -1
- package/security/SecurityEnforcement.js +241 -0
- package/security/SessionSecurity.js +3 -2
- package/test/security/filters.test.js +276 -0
- package/test/security/https.test.js +214 -0
- package/test/security/path-traversal.test.js +222 -0
- package/test/security/xss.test.js +190 -0
- package/MasterSession.js +0 -208
- package/docs/server-setup-hostname-binding.md +0 -24
- package/docs/server-setup-http.md +0 -32
- package/docs/server-setup-https-credentials.md +0 -32
- package/docs/server-setup-https-env-tls-sni.md +0 -62
- package/docs/server-setup-nginx-reverse-proxy.md +0 -46
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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(
|
|
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
|
}
|