mastercontroller 1.3.9 → 1.3.12
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/.eslintrc.json +50 -0
- package/.github/workflows/ci.yml +317 -0
- package/.prettierrc +10 -0
- package/CHANGES.md +296 -0
- package/DEPLOYMENT.md +956 -0
- package/FIXES_APPLIED.md +378 -0
- package/FORTUNE_500_UPGRADE.md +863 -0
- package/MasterAction.js +10 -263
- package/MasterControl.js +226 -43
- package/MasterRequest.js +42 -1
- package/MasterRouter.js +42 -37
- package/PERFORMANCE_SECURITY_AUDIT.md +677 -0
- package/README.md +602 -71
- package/SENIOR_ENGINEER_AUDIT.md +2477 -0
- package/VERIFICATION_CHECKLIST.md +726 -0
- package/error/README.md +2452 -0
- package/monitoring/HealthCheck.js +347 -0
- package/monitoring/PrometheusExporter.js +416 -0
- package/monitoring/README.md +3112 -0
- package/package.json +64 -11
- package/security/MasterValidator.js +140 -10
- package/security/README.md +1805 -0
- package/security/adapters/RedisCSRFStore.js +428 -0
- package/security/adapters/RedisRateLimiter.js +462 -0
- package/security/adapters/RedisSessionStore.js +476 -0
- package/MasterCors.js.tmp +0 -0
- package/MasterHtml.js +0 -649
- package/MasterPipeline.js.tmp +0 -0
- package/MasterRequest.js.tmp +0 -0
- package/MasterRouter.js.tmp +0 -0
- package/MasterSocket.js.tmp +0 -0
- package/MasterTemp.js.tmp +0 -0
- package/MasterTemplate.js +0 -230
- package/MasterTimeout.js.tmp +0 -0
- package/TemplateOverwrite.js +0 -41
- package/TemplateOverwrite.js.tmp +0 -0
- package/error/ErrorBoundary.js +0 -353
- package/error/HydrationMismatch.js +0 -265
- package/error/MasterError.js +0 -240
- package/error/MasterError.js.tmp +0 -0
- package/error/MasterErrorRenderer.js +0 -536
- package/error/MasterErrorRenderer.js.tmp +0 -0
- package/error/SSRErrorHandler.js +0 -273
- package/ssr/hydration-client.js +0 -93
- package/ssr/runtime-ssr.cjs +0 -553
- package/ssr/ssr-shims.js +0 -73
package/MasterAction.js
CHANGED
|
@@ -3,16 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
var fileserver = require('fs');
|
|
5
5
|
var toolClass = require('./MasterTools');
|
|
6
|
-
var tempClass = require('./MasterTemplate');
|
|
7
|
-
// Templating helpers
|
|
8
|
-
var temp = new tempClass();
|
|
9
6
|
var tools = new toolClass();
|
|
7
|
+
// View templating removed - handled by view engine (e.g., MasterView)
|
|
10
8
|
|
|
11
9
|
// Node utils
|
|
12
10
|
var path = require('path');
|
|
13
11
|
|
|
14
|
-
//
|
|
15
|
-
const compileWebComponentsHTML = require('./ssr/runtime-ssr.cjs');
|
|
12
|
+
// SSR runtime removed - handled by view engine
|
|
16
13
|
|
|
17
14
|
// Enhanced error handling
|
|
18
15
|
const { handleTemplateError, sendErrorResponse } = require('./error/MasterBackendErrorHandler');
|
|
@@ -35,22 +32,7 @@ class MasterAction{
|
|
|
35
32
|
return MasterAction.__masterCache;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
getView(
|
|
39
|
-
var actionUrl = MasterAction._master.root + location;
|
|
40
|
-
const fileResult = safeReadFile(fileserver, actionUrl);
|
|
41
|
-
|
|
42
|
-
if (!fileResult.success) {
|
|
43
|
-
const error = handleTemplateError(fileResult.error.originalError, actionUrl, data);
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
return temp.htmlBuilder(fileResult.content, data);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
const mcError = handleTemplateError(error, actionUrl, data);
|
|
51
|
-
throw mcError;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
35
|
+
// getView() removed - handled by view engine (register via master.useView())
|
|
54
36
|
|
|
55
37
|
|
|
56
38
|
returnJson(data){
|
|
@@ -76,52 +58,7 @@ class MasterAction{
|
|
|
76
58
|
}
|
|
77
59
|
}
|
|
78
60
|
|
|
79
|
-
//
|
|
80
|
-
returnPartialView(location, data){
|
|
81
|
-
// SECURITY: Validate path to prevent traversal attacks
|
|
82
|
-
if (!location || location.includes('..') || location.includes('~') || path.isAbsolute(location)) {
|
|
83
|
-
logger.warn({
|
|
84
|
-
code: 'MC_SECURITY_PATH_TRAVERSAL',
|
|
85
|
-
message: 'Path traversal attempt blocked in returnPartialView',
|
|
86
|
-
path: location
|
|
87
|
-
});
|
|
88
|
-
this.returnError(400, 'Invalid path');
|
|
89
|
-
return '';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const actionUrl = path.resolve(MasterAction._master.root, location);
|
|
93
|
-
|
|
94
|
-
// SECURITY: Ensure resolved path is within app root
|
|
95
|
-
if (!actionUrl.startsWith(MasterAction._master.root)) {
|
|
96
|
-
logger.warn({
|
|
97
|
-
code: 'MC_SECURITY_PATH_TRAVERSAL',
|
|
98
|
-
message: 'Path traversal blocked in returnPartialView',
|
|
99
|
-
requestedPath: location,
|
|
100
|
-
resolvedPath: actionUrl
|
|
101
|
-
});
|
|
102
|
-
this.returnError(403, 'Forbidden');
|
|
103
|
-
return '';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const getAction = fileserver.readFileSync(actionUrl, 'utf8');
|
|
108
|
-
if (MasterAction._master.overwrite.isTemplate){
|
|
109
|
-
return MasterAction._master.overwrite.templateRender( data, "returnPartialView");
|
|
110
|
-
}
|
|
111
|
-
else{
|
|
112
|
-
return temp.htmlBuilder(getAction, data);
|
|
113
|
-
}
|
|
114
|
-
} catch (error) {
|
|
115
|
-
logger.error({
|
|
116
|
-
code: 'MC_ERR_PARTIAL_VIEW',
|
|
117
|
-
message: 'Failed to read partial view',
|
|
118
|
-
path: location,
|
|
119
|
-
error: error.message
|
|
120
|
-
});
|
|
121
|
-
this.returnError(404, 'View not found');
|
|
122
|
-
return '';
|
|
123
|
-
}
|
|
124
|
-
}
|
|
61
|
+
// returnPartialView() removed - handled by view engine (register via master.useView())
|
|
125
62
|
|
|
126
63
|
redirectBack(fallback){
|
|
127
64
|
if(fallback === undefined){
|
|
@@ -202,154 +139,13 @@ class MasterAction{
|
|
|
202
139
|
MasterAction._master.router._call(requestObj);
|
|
203
140
|
}
|
|
204
141
|
|
|
205
|
-
//
|
|
206
|
-
returnViewWithoutMaster(location, data){
|
|
207
|
-
var masterView = null;
|
|
208
|
-
this.params = this.params === undefined ? {} : this.params;
|
|
209
|
-
this.params = tools.combineObjects(data, this.params);
|
|
210
|
-
var func = MasterAction._master.viewList;
|
|
211
|
-
this.params = tools.combineObjects(this.params, func);
|
|
212
|
-
// Prefer page.js module if present (no legacy .html file)
|
|
213
|
-
try {
|
|
214
|
-
const controller = this.__currentRoute.toController;
|
|
215
|
-
const action = this.__currentRoute.toAction;
|
|
216
|
-
const pageModuleAbs = path.join(MasterAction._master.root, 'app/views', controller, action, 'page.js');
|
|
217
|
-
if (fileserver.existsSync(pageModuleAbs)) {
|
|
218
|
-
if (this._renderPageModule(controller, action, data)) { return; }
|
|
219
|
-
}
|
|
220
|
-
} catch (_) {}
|
|
142
|
+
// returnViewWithoutMaster() removed - handled by view engine (register via master.useView())
|
|
221
143
|
|
|
222
|
-
|
|
223
|
-
var actionView = fileserver.readFileSync(actionUrl, 'utf8');
|
|
224
|
-
if (MasterAction._master.overwrite.isTemplate){
|
|
225
|
-
masterView = MasterAction._master.overwrite.templateRender(data, "returnViewWithoutMaster");
|
|
226
|
-
}
|
|
227
|
-
else{
|
|
228
|
-
masterView = temp.htmlBuilder(actionView, data);
|
|
229
|
-
}
|
|
230
|
-
if (!this.__requestObject.response._headerSent) {
|
|
231
|
-
const send = (htmlOut) => {
|
|
232
|
-
try {
|
|
233
|
-
this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
|
|
234
|
-
this.__requestObject.response.end(htmlOut);
|
|
235
|
-
} catch (e) {
|
|
236
|
-
// Fallback in case of double send
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
try {
|
|
240
|
-
Promise.resolve(compileWebComponentsHTML(masterView))
|
|
241
|
-
.then(send)
|
|
242
|
-
.catch(() => send(masterView));
|
|
243
|
-
} catch (_) {
|
|
244
|
-
send(masterView);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
144
|
+
// returnViewWithoutEngine() removed - handled by view engine (register via master.useView())
|
|
248
145
|
|
|
249
|
-
|
|
250
|
-
// SECURITY: Validate path to prevent traversal attacks
|
|
251
|
-
if (!location || location.includes('..') || location.includes('~') || path.isAbsolute(location)) {
|
|
252
|
-
logger.warn({
|
|
253
|
-
code: 'MC_SECURITY_PATH_TRAVERSAL',
|
|
254
|
-
message: 'Path traversal attempt blocked in returnViewWithoutEngine',
|
|
255
|
-
path: location
|
|
256
|
-
});
|
|
257
|
-
this.returnError(400, 'Invalid path');
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const actionUrl = path.resolve(MasterAction._master.root, location);
|
|
262
|
-
|
|
263
|
-
// SECURITY: Ensure resolved path is within app root
|
|
264
|
-
if (!actionUrl.startsWith(MasterAction._master.root)) {
|
|
265
|
-
logger.warn({
|
|
266
|
-
code: 'MC_SECURITY_PATH_TRAVERSAL',
|
|
267
|
-
message: 'Path traversal blocked in returnViewWithoutEngine',
|
|
268
|
-
requestedPath: location,
|
|
269
|
-
resolvedPath: actionUrl
|
|
270
|
-
});
|
|
271
|
-
this.returnError(403, 'Forbidden');
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
const masterView = fileserver.readFileSync(actionUrl, 'utf8');
|
|
277
|
-
if (!this.__requestObject.response._headerSent) {
|
|
278
|
-
this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
|
|
279
|
-
this.__requestObject.response.end(masterView);
|
|
280
|
-
}
|
|
281
|
-
} catch (error) {
|
|
282
|
-
logger.error({
|
|
283
|
-
code: 'MC_ERR_VIEW_READ',
|
|
284
|
-
message: 'Failed to read view file',
|
|
285
|
-
path: location,
|
|
286
|
-
error: error.message
|
|
287
|
-
});
|
|
288
|
-
this.returnError(404, 'View not found');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
returnReact(data, location){
|
|
293
|
-
|
|
294
|
-
var masterView = null;
|
|
295
|
-
data = data === undefined ? {} : data;
|
|
296
|
-
this.params = this.params === undefined ? {} : this.params;
|
|
297
|
-
this.params = tools.combineObjects(data, this.params);
|
|
298
|
-
var func = MasterAction._master.viewList;
|
|
299
|
-
this.params = tools.combineObjects(this.params, func);
|
|
300
|
-
var html = MasterAction._master.reactView.compile(this.__currentRoute.toController, this.__currentRoute.toAction, this.__currentRoute.root);
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
returnView(data, location){
|
|
305
|
-
|
|
306
|
-
var masterView = null;
|
|
307
|
-
data = data === undefined ? {} : data;
|
|
308
|
-
this.params = this.params === undefined ? {} : this.params;
|
|
309
|
-
this.params = tools.combineObjects(data, this.params);
|
|
310
|
-
var func = MasterAction._master.viewList;
|
|
311
|
-
this.params = tools.combineObjects(this.params, func);
|
|
312
|
-
// Prefer page.js module if present (no legacy .html file)
|
|
313
|
-
try {
|
|
314
|
-
const controller = this.__currentRoute.toController;
|
|
315
|
-
const action = this.__currentRoute.toAction;
|
|
316
|
-
const pageModuleAbs = path.join(MasterAction._master.root, 'app/views', controller, action, 'page.js');
|
|
317
|
-
if (fileserver.existsSync(pageModuleAbs)) {
|
|
318
|
-
if (this._renderPageModule(controller, action, data)) { return; }
|
|
319
|
-
}
|
|
320
|
-
} catch (_) {}
|
|
146
|
+
// returnReact() removed - handled by view engine (register via master.useView())
|
|
321
147
|
|
|
322
|
-
|
|
323
|
-
var viewFile = fileserver.readFileSync(viewUrl,'utf8');
|
|
324
|
-
var masterFile = fileserver.readFileSync(this.__currentRoute.root + "/app/views/layouts/master.html", 'utf8');
|
|
325
|
-
if (MasterAction._master.overwrite.isTemplate){
|
|
326
|
-
masterView = MasterAction._master.overwrite.templateRender(this.params, "returnView");
|
|
327
|
-
}
|
|
328
|
-
else{
|
|
329
|
-
var childView = temp.htmlBuilder(viewFile, this.params);
|
|
330
|
-
this.params.yield = childView;
|
|
331
|
-
masterView = temp.htmlBuilder(masterFile, this.params);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (!this.__response._headerSent) {
|
|
335
|
-
const send = (htmlOut) => {
|
|
336
|
-
try {
|
|
337
|
-
this.__response.writeHead(200, {'Content-Type': 'text/html'});
|
|
338
|
-
this.__response.end(htmlOut);
|
|
339
|
-
} catch (e) {
|
|
340
|
-
// Fallback in case of double send
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
try {
|
|
344
|
-
Promise.resolve(compileWebComponentsHTML(masterView))
|
|
345
|
-
.then(send)
|
|
346
|
-
.catch(() => send(masterView));
|
|
347
|
-
} catch (_) {
|
|
348
|
-
send(masterView);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
}
|
|
148
|
+
// returnView() removed - handled by view engine (register via master.useView())
|
|
353
149
|
|
|
354
150
|
close(response, code, content, end){
|
|
355
151
|
response.writeHead(code, content.type);
|
|
@@ -381,58 +177,9 @@ class MasterAction{
|
|
|
381
177
|
return false;
|
|
382
178
|
}
|
|
383
179
|
|
|
384
|
-
//
|
|
385
|
-
_renderPageModule(controller, action, data) {
|
|
386
|
-
try {
|
|
387
|
-
const pageModuleAbs = path.join(MasterAction._master.root, 'app/views', controller, action, 'page.js');
|
|
388
|
-
const layoutModuleAbs = path.join(MasterAction._master.root, 'app/views', 'layouts', 'master.js');
|
|
389
|
-
const stylesPath = '/app/assets/stylesheets/output.css';
|
|
390
|
-
const pageTag = `home-${action}-page`;
|
|
391
|
-
|
|
392
|
-
const htmlDoc =
|
|
393
|
-
`<!DOCTYPE html>
|
|
394
|
-
<html lang="en">
|
|
395
|
-
<head>
|
|
396
|
-
<meta charset="utf-8"/>
|
|
397
|
-
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
398
|
-
<title>${controller}/${action}</title>
|
|
399
|
-
<link rel="stylesheet" href="${stylesPath}"/>
|
|
400
|
-
</head>
|
|
401
|
-
<body class="geist-variable antialiased">
|
|
402
|
-
<root-layout>
|
|
403
|
-
<${pageTag}></${pageTag}>
|
|
404
|
-
</root-layout>
|
|
405
|
-
<script type="module" src="/app/views/layouts/master.js"></script>
|
|
406
|
-
<script type="module" src="/app/views/${controller}/${action}/page.js"></script>
|
|
407
|
-
</body>
|
|
408
|
-
</html>`;
|
|
409
|
-
|
|
410
|
-
const send = (htmlOut) => {
|
|
411
|
-
try {
|
|
412
|
-
const res = this.__response || (this.__requestObject && this.__requestObject.response);
|
|
413
|
-
if (res && !res._headerSent) {
|
|
414
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
415
|
-
res.end(htmlOut);
|
|
416
|
-
}
|
|
417
|
-
} catch (_) {}
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
Promise
|
|
421
|
-
.resolve(require('./ssr/runtime-ssr.cjs')(htmlDoc, [layoutModuleAbs, pageModuleAbs]))
|
|
422
|
-
.then(send)
|
|
423
|
-
.catch(() => send(htmlDoc));
|
|
424
|
-
} catch (e) {
|
|
425
|
-
// Fallback to legacy view if something goes wrong
|
|
426
|
-
console.warn('[SSR] _renderPageModule failed:', e && e.message);
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
return true;
|
|
430
|
-
}
|
|
180
|
+
// _renderPageModule() removed - handled by view engine (register via master.useView())
|
|
431
181
|
|
|
432
|
-
//
|
|
433
|
-
returnWebComponent(data) {
|
|
434
|
-
this.returnView(data);
|
|
435
|
-
}
|
|
182
|
+
// returnWebComponent() removed - handled by view engine (register via master.useView())
|
|
436
183
|
|
|
437
184
|
// ==================== Security Methods ====================
|
|
438
185
|
|
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');
|
|
@@ -67,6 +68,7 @@ class MasterControl {
|
|
|
67
68
|
_hstsMaxAge = 31536000 // 1 year default
|
|
68
69
|
_hstsIncludeSubDomains = true
|
|
69
70
|
_hstsPreload = false
|
|
71
|
+
_viewEngine = null // Pluggable view engine (MasterView, EJS, Pug, etc.)
|
|
70
72
|
|
|
71
73
|
#loadTransientListClasses(name, params){
|
|
72
74
|
Object.defineProperty(this.requestList, name, {
|
|
@@ -124,35 +126,127 @@ class MasterControl {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Initialize prototype pollution protection
|
|
131
|
+
* SECURITY: Prevents malicious modification of Object/Array prototypes
|
|
132
|
+
*/
|
|
133
|
+
_initPrototypePollutionProtection() {
|
|
134
|
+
// Only freeze in production to allow for easier debugging in development
|
|
135
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
136
|
+
|
|
137
|
+
if (isProduction) {
|
|
138
|
+
// Freeze prototypes to prevent prototype pollution attacks
|
|
139
|
+
try {
|
|
140
|
+
Object.freeze(Object.prototype);
|
|
141
|
+
Object.freeze(Array.prototype);
|
|
142
|
+
Object.freeze(Function.prototype);
|
|
143
|
+
|
|
144
|
+
logger.info({
|
|
145
|
+
code: 'MC_SECURITY_PROTOTYPE_FROZEN',
|
|
146
|
+
message: 'Prototypes frozen in production mode for security'
|
|
147
|
+
});
|
|
148
|
+
} catch (err) {
|
|
149
|
+
logger.warn({
|
|
150
|
+
code: 'MC_SECURITY_FREEZE_FAILED',
|
|
151
|
+
message: 'Failed to freeze prototypes',
|
|
152
|
+
error: err.message
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Add prototype pollution detection utility
|
|
158
|
+
this._detectPrototypePollution = (obj) => {
|
|
159
|
+
if (!obj || typeof obj !== 'object') {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
|
|
164
|
+
|
|
165
|
+
for (const key of dangerousKeys) {
|
|
166
|
+
if (key in obj) {
|
|
167
|
+
logger.error({
|
|
168
|
+
code: 'MC_SECURITY_PROTOTYPE_POLLUTION',
|
|
169
|
+
message: `Prototype pollution detected: ${key} in object`,
|
|
170
|
+
severity: 'CRITICAL'
|
|
171
|
+
});
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
console.log('[MasterControl] Prototype pollution protection initialized');
|
|
180
|
+
}
|
|
181
|
+
|
|
127
182
|
// extends class methods to be used inside of the view class using the THIS keyword
|
|
128
183
|
extendView( name, element){
|
|
129
184
|
element = new element();
|
|
130
|
-
|
|
131
|
-
var propertyNames = Object.getOwnPropertyNames( element.__proto__);
|
|
185
|
+
const propertyNames = Object.getOwnPropertyNames(element.__proto__);
|
|
132
186
|
this.viewList[name] = {};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
187
|
+
|
|
188
|
+
// Fixed: Use for...of instead of for...in for array iteration
|
|
189
|
+
// Filter out 'constructor' and iterate efficiently
|
|
190
|
+
for (const propName of propertyNames) {
|
|
191
|
+
if (propName !== "constructor") {
|
|
192
|
+
this.viewList[name][propName] = element[propName];
|
|
138
193
|
}
|
|
139
|
-
}
|
|
194
|
+
}
|
|
140
195
|
}
|
|
141
196
|
|
|
142
197
|
// extends class methods to be used inside of the controller class using the THIS keyword
|
|
143
198
|
extendController(element){
|
|
144
199
|
element = new element();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
200
|
+
const propertyNames = Object.getOwnPropertyNames(element.__proto__);
|
|
201
|
+
|
|
202
|
+
// Fixed: Use for...of instead of for...in for array iteration
|
|
203
|
+
// Filter out 'constructor' and iterate efficiently
|
|
204
|
+
for (const propName of propertyNames) {
|
|
205
|
+
if (propName !== "constructor") {
|
|
206
|
+
this.controllerList[propName] = element[propName];
|
|
152
207
|
}
|
|
153
|
-
}
|
|
208
|
+
}
|
|
154
209
|
}
|
|
155
|
-
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Register a view engine (MasterView, React, EJS, Pug, etc.)
|
|
213
|
+
* This allows for pluggable view rendering
|
|
214
|
+
*
|
|
215
|
+
* @param {Object|Function} ViewEngine - View engine class or instance
|
|
216
|
+
* @param {Object} options - Configuration options for the view engine
|
|
217
|
+
* @returns {MasterControl} - Returns this for chaining
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* // Use MasterView (official view engine)
|
|
221
|
+
* const MasterView = require('masterview');
|
|
222
|
+
* master.useView(MasterView, { ssr: true });
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* // Use EJS adapter
|
|
226
|
+
* const EJSAdapter = require('./adapters/ejs');
|
|
227
|
+
* master.useView(EJSAdapter);
|
|
228
|
+
*/
|
|
229
|
+
useView(ViewEngine, options = {}) {
|
|
230
|
+
if (typeof ViewEngine === 'function') {
|
|
231
|
+
this._viewEngine = new ViewEngine(options);
|
|
232
|
+
} else {
|
|
233
|
+
this._viewEngine = ViewEngine;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Let the view engine register itself
|
|
237
|
+
if (this._viewEngine && this._viewEngine.register) {
|
|
238
|
+
this._viewEngine.register(this);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
logger.info({
|
|
242
|
+
code: 'MC_INFO_VIEW_ENGINE_REGISTERED',
|
|
243
|
+
message: 'View engine registered',
|
|
244
|
+
engine: ViewEngine.name || 'Custom'
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
156
250
|
/*
|
|
157
251
|
Services are created each time they are requested.
|
|
158
252
|
It gets a new instance of the injected object, on each request of this object.
|
|
@@ -299,6 +393,9 @@ class MasterControl {
|
|
|
299
393
|
try {
|
|
300
394
|
var $that = this;
|
|
301
395
|
|
|
396
|
+
// SECURITY: Initialize prototype pollution protection
|
|
397
|
+
this._initPrototypePollutionProtection();
|
|
398
|
+
|
|
302
399
|
// AUTO-LOAD internal framework modules
|
|
303
400
|
// These are required for the framework to function and are loaded transparently
|
|
304
401
|
const internalModules = {
|
|
@@ -313,10 +410,11 @@ class MasterControl {
|
|
|
313
410
|
'MasterCors': './MasterCors',
|
|
314
411
|
'SessionSecurity': './security/SessionSecurity',
|
|
315
412
|
'MasterSocket': './MasterSocket',
|
|
316
|
-
'
|
|
317
|
-
|
|
318
|
-
'
|
|
319
|
-
'
|
|
413
|
+
'MasterTools': './MasterTools'
|
|
414
|
+
// View modules removed - use master.useView(MasterView) instead
|
|
415
|
+
// 'MasterHtml': './MasterHtml',
|
|
416
|
+
// 'MasterTemplate': './MasterTemplate',
|
|
417
|
+
// 'TemplateOverwrite': './TemplateOverwrite'
|
|
320
418
|
};
|
|
321
419
|
|
|
322
420
|
// Explicit module registration (prevents circular dependency issues)
|
|
@@ -331,8 +429,8 @@ class MasterControl {
|
|
|
331
429
|
'cors': { path: './MasterCors', exportName: 'MasterCors' },
|
|
332
430
|
'socket': { path: './MasterSocket', exportName: 'MasterSocket' },
|
|
333
431
|
'tempdata': { path: './MasterTemp', exportName: 'MasterTemp' },
|
|
334
|
-
'overwrite': { path: './TemplateOverwrite', exportName: 'TemplateOverwrite' },
|
|
335
432
|
'session': { path: './security/SessionSecurity', exportName: 'MasterSessionSecurity' }
|
|
433
|
+
// 'overwrite' removed - will be provided by view engine (master.useView())
|
|
336
434
|
};
|
|
337
435
|
|
|
338
436
|
for (const [name, config] of Object.entries(moduleRegistry)) {
|
|
@@ -354,13 +452,12 @@ class MasterControl {
|
|
|
354
452
|
// Legacy code uses master.sessions (plural), new API uses master.session (singular)
|
|
355
453
|
$that.sessions = $that.session;
|
|
356
454
|
|
|
357
|
-
// Load
|
|
455
|
+
// Load controller extensions (these extend prototypes, not master instance)
|
|
358
456
|
try {
|
|
359
457
|
require('./MasterAction');
|
|
360
458
|
require('./MasterActionFilters');
|
|
361
|
-
require('./MasterHtml');
|
|
362
|
-
require('./MasterTemplate');
|
|
363
459
|
require('./MasterTools');
|
|
460
|
+
// View extensions (MasterHtml, MasterTemplate) removed - use master.useView() instead
|
|
364
461
|
} catch (e) {
|
|
365
462
|
console.error('[MasterControl] Failed to load extensions:', e.message);
|
|
366
463
|
}
|
|
@@ -683,26 +780,107 @@ class MasterControl {
|
|
|
683
780
|
}
|
|
684
781
|
}
|
|
685
782
|
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
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) => {
|
|
689
845
|
logger.error({
|
|
690
|
-
code: '
|
|
691
|
-
message: 'Error
|
|
846
|
+
code: 'MC_ERR_STREAM_READ',
|
|
847
|
+
message: 'Error streaming static file',
|
|
692
848
|
path: finalPath,
|
|
693
849
|
error: err.message
|
|
694
850
|
});
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
ctx.response.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
+
}
|
|
706
884
|
});
|
|
707
885
|
|
|
708
886
|
return; // Terminal - don't call next()
|
|
@@ -734,9 +912,14 @@ class MasterControl {
|
|
|
734
912
|
});
|
|
735
913
|
|
|
736
914
|
// 4. Load Scoped Services (per request - always needed)
|
|
915
|
+
// Cache keys for performance (computed once, not on every request)
|
|
916
|
+
const scopedKeys = Object.keys($that._scopedList);
|
|
917
|
+
|
|
737
918
|
$that.pipeline.use(async (ctx, next) => {
|
|
738
|
-
|
|
739
|
-
|
|
919
|
+
// Fixed: Use cached keys with direct array iteration (faster & safer)
|
|
920
|
+
for (let i = 0; i < scopedKeys.length; i++) {
|
|
921
|
+
const key = scopedKeys[i];
|
|
922
|
+
const className = $that._scopedList[key];
|
|
740
923
|
$that.requestList[key] = new className();
|
|
741
924
|
}
|
|
742
925
|
await next();
|