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