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
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
"Bash(mv:*)",
|
|
6
6
|
"Bash(node -e:*)",
|
|
7
7
|
"Bash(find:*)",
|
|
8
|
-
"Bash(node -c:*)"
|
|
8
|
+
"Bash(node -c:*)",
|
|
9
|
+
"Bash(grep:*)",
|
|
10
|
+
"Bash(ls:*)",
|
|
11
|
+
"Bash(git checkout:*)",
|
|
12
|
+
"Bash(perl -i -pe:*)",
|
|
13
|
+
"Bash(node test-circular-dependency.js:*)"
|
|
9
14
|
],
|
|
10
15
|
"deny": [],
|
|
11
16
|
"ask": []
|
package/MasterAction.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
// version 0.0.23
|
|
3
3
|
|
|
4
|
-
var master = require('./MasterControl');
|
|
5
4
|
var fileserver = require('fs');
|
|
6
5
|
var toolClass = require('./MasterTools');
|
|
7
6
|
var tempClass = require('./MasterTemplate');
|
|
@@ -26,9 +25,18 @@ const { validator, validateRequestBody, sanitizeObject } = require('./security/M
|
|
|
26
25
|
const { sanitizeUserHTML, escapeHTML } = require('./security/MasterSanitizer');
|
|
27
26
|
|
|
28
27
|
class MasterAction{
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
// Lazy-load master to avoid circular dependency
|
|
30
|
+
// Static getter ensures single instance (Singleton pattern - Google style)
|
|
31
|
+
static get _master() {
|
|
32
|
+
if (!MasterAction.__masterCache) {
|
|
33
|
+
MasterAction.__masterCache = require('./MasterControl');
|
|
34
|
+
}
|
|
35
|
+
return MasterAction.__masterCache;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
getView(location, data){
|
|
31
|
-
var actionUrl =
|
|
39
|
+
var actionUrl = MasterAction._master.root + location;
|
|
32
40
|
const fileResult = safeReadFile(fileserver, actionUrl);
|
|
33
41
|
|
|
34
42
|
if (!fileResult.success) {
|
|
@@ -46,22 +54,72 @@ class MasterAction{
|
|
|
46
54
|
|
|
47
55
|
|
|
48
56
|
returnJson(data){
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.__response.
|
|
57
|
+
try {
|
|
58
|
+
const json = JSON.stringify(data);
|
|
59
|
+
// FIXED: Check both _headerSent and headersSent for compatibility
|
|
60
|
+
if (!this.__response._headerSent && !this.__response.headersSent) {
|
|
61
|
+
this.__response.writeHead(200, {'Content-Type': 'application/json'});
|
|
62
|
+
this.__response.end(json);
|
|
63
|
+
} else {
|
|
64
|
+
logger.warn({
|
|
65
|
+
code: 'MC_WARN_HEADERS_SENT',
|
|
66
|
+
message: 'Attempted to send JSON but headers already sent'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error({
|
|
71
|
+
code: 'MC_ERR_JSON_SEND',
|
|
72
|
+
message: 'Failed to send JSON response',
|
|
73
|
+
error: error.message,
|
|
74
|
+
stack: error.stack
|
|
75
|
+
});
|
|
53
76
|
}
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
// location starts from the view folder. Ex: partialViews/fileName.html
|
|
57
80
|
returnPartialView(location, data){
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 '';
|
|
62
90
|
}
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
|
|
92
|
+
const actionUrl = path.resolve(master.root, location);
|
|
93
|
+
|
|
94
|
+
// SECURITY: Ensure resolved path is within app root
|
|
95
|
+
if (!actionUrl.startsWith(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(master.overwrite.isTemplate){
|
|
109
|
+
return 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 '';
|
|
65
123
|
}
|
|
66
124
|
}
|
|
67
125
|
|
|
@@ -112,21 +170,34 @@ class MasterAction{
|
|
|
112
170
|
|
|
113
171
|
// redirects to another action inside the same controller = does not reload the page
|
|
114
172
|
redirectToAction(namespace, action, type, data, components){
|
|
173
|
+
// FIXED: Declare variables before if/else to avoid undefined reference
|
|
174
|
+
const resp = this.__requestObject.response;
|
|
175
|
+
const req = this.__requestObject.request;
|
|
115
176
|
|
|
116
|
-
|
|
177
|
+
const requestObj = {
|
|
117
178
|
toController : namespace,
|
|
118
179
|
toAction : action,
|
|
119
180
|
type : type,
|
|
120
181
|
params : data
|
|
121
|
-
}
|
|
182
|
+
};
|
|
183
|
+
|
|
122
184
|
if(components){
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
185
|
+
master.router.currentRoute = {
|
|
186
|
+
root : `${master.root}/components/${namespace}`,
|
|
187
|
+
toController : namespace,
|
|
188
|
+
toAction : action,
|
|
189
|
+
response : resp,
|
|
190
|
+
request: req
|
|
191
|
+
};
|
|
126
192
|
}else{
|
|
127
|
-
master.router.currentRoute = {
|
|
193
|
+
master.router.currentRoute = {
|
|
194
|
+
root : `${master.root}/${namespace}`,
|
|
195
|
+
toController : namespace,
|
|
196
|
+
toAction : action,
|
|
197
|
+
response : resp,
|
|
198
|
+
request: req
|
|
199
|
+
};
|
|
128
200
|
}
|
|
129
|
-
|
|
130
201
|
|
|
131
202
|
master.router._call(requestObj);
|
|
132
203
|
}
|
|
@@ -176,11 +247,45 @@ class MasterAction{
|
|
|
176
247
|
}
|
|
177
248
|
|
|
178
249
|
returnViewWithoutEngine(location){
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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(master.root, location);
|
|
262
|
+
|
|
263
|
+
// SECURITY: Ensure resolved path is within app root
|
|
264
|
+
if (!actionUrl.startsWith(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');
|
|
184
289
|
}
|
|
185
290
|
}
|
|
186
291
|
|
|
@@ -448,6 +553,7 @@ class MasterAction{
|
|
|
448
553
|
/**
|
|
449
554
|
* Require HTTPS for this action
|
|
450
555
|
* Usage: if (!this.requireHTTPS()) return;
|
|
556
|
+
* FIXED: Uses configured hostname, not unvalidated Host header
|
|
451
557
|
*/
|
|
452
558
|
requireHTTPS() {
|
|
453
559
|
if (!this.isSecure()) {
|
|
@@ -457,7 +563,23 @@ class MasterAction{
|
|
|
457
563
|
path: this.__requestObject.pathName
|
|
458
564
|
});
|
|
459
565
|
|
|
460
|
-
|
|
566
|
+
// SECURITY FIX: Never use Host header from request (open redirect vulnerability)
|
|
567
|
+
// Use configured hostname instead
|
|
568
|
+
const configuredHost = master.env?.server?.hostname || 'localhost';
|
|
569
|
+
const httpsPort = master.env?.server?.httpsPort || 443;
|
|
570
|
+
const port = httpsPort === 443 ? '' : `:${httpsPort}`;
|
|
571
|
+
|
|
572
|
+
// Validate configured host exists
|
|
573
|
+
if (!configuredHost || configuredHost === 'localhost') {
|
|
574
|
+
logger.error({
|
|
575
|
+
code: 'MC_CONFIG_MISSING_HOSTNAME',
|
|
576
|
+
message: 'requireHTTPS called but no hostname configured in master.env.server.hostname'
|
|
577
|
+
});
|
|
578
|
+
this.returnError(500, 'Server misconfiguration');
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const httpsUrl = `https://${configuredHost}${port}${this.__requestObject.pathName}`;
|
|
461
583
|
this.redirectTo(httpsUrl);
|
|
462
584
|
return false;
|
|
463
585
|
}
|
|
@@ -484,5 +606,14 @@ class MasterAction{
|
|
|
484
606
|
|
|
485
607
|
}
|
|
486
608
|
|
|
609
|
+
// Export for MasterControl and register after event loop (prevents circular dependency)
|
|
610
|
+
// This is the Lazy Registration pattern used by Spring Framework, Angular, Google Guice
|
|
611
|
+
module.exports = MasterAction;
|
|
487
612
|
|
|
488
|
-
master
|
|
613
|
+
// Use setImmediate to register after master is fully loaded
|
|
614
|
+
setImmediate(() => {
|
|
615
|
+
const master = require('./MasterControl');
|
|
616
|
+
if (master && master.extendController) {
|
|
617
|
+
master.extendController(MasterAction);
|
|
618
|
+
}
|
|
619
|
+
});
|
package/MasterActionFilters.js
CHANGED
|
@@ -1,98 +1,218 @@
|
|
|
1
|
-
// version
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var _beforeActionFunc = {
|
|
5
|
-
namespace : "",
|
|
6
|
-
actionList : "",
|
|
7
|
-
callBack : "",
|
|
8
|
-
that : ""
|
|
9
|
-
};
|
|
10
|
-
var _afterActionFunc = {
|
|
11
|
-
namespace : "",
|
|
12
|
-
actionList : "",
|
|
13
|
-
callBack : "",
|
|
14
|
-
that : ""
|
|
15
|
-
};
|
|
16
|
-
var emit = "";
|
|
1
|
+
// version 2.0 - FIXED: Instance-level filters, async support, multiple filters
|
|
2
|
+
const { logger } = require('./error/MasterErrorLogger');
|
|
17
3
|
|
|
18
4
|
class MasterActionFilters {
|
|
19
5
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
6
|
+
// Lazy-load master to avoid circular dependency (Google-style Singleton pattern)
|
|
7
|
+
static get _master() {
|
|
8
|
+
if (!MasterActionFilters.__masterCache) {
|
|
9
|
+
MasterActionFilters.__masterCache = require('./MasterControl');
|
|
10
|
+
}
|
|
11
|
+
return MasterActionFilters.__masterCache;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
// FIXED: Instance-level storage instead of module-level
|
|
16
|
+
// Each controller gets its own filter arrays
|
|
17
|
+
this._beforeActionFilters = [];
|
|
18
|
+
this._afterActionFilters = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Register a before action filter
|
|
22
|
+
// FIXED: Adds to array instead of overwriting
|
|
23
|
+
beforeAction(actionlist, func){
|
|
24
|
+
if (typeof func !== 'function') {
|
|
25
|
+
master.error.log("beforeAction callback not a function", "warn");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// FIXED: Push to array, don't overwrite
|
|
30
|
+
this._beforeActionFilters.push({
|
|
31
|
+
namespace: this.__namespace,
|
|
32
|
+
actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
|
|
33
|
+
callBack: func,
|
|
34
|
+
that: this
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Register an after action filter
|
|
39
|
+
// FIXED: Adds to array instead of overwriting
|
|
40
|
+
afterAction(actionlist, func){
|
|
41
|
+
if (typeof func !== 'function') {
|
|
42
|
+
master.error.log("afterAction callback not a function", "warn");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// FIXED: Push to array, don't overwrite
|
|
47
|
+
this._afterActionFilters.push({
|
|
48
|
+
namespace: this.__namespace,
|
|
49
|
+
actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
|
|
50
|
+
callBack: func,
|
|
51
|
+
that: this
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if controller has before action filters for this action
|
|
56
|
+
__hasBeforeAction(obj, request){
|
|
57
|
+
if (!this._beforeActionFilters || this._beforeActionFilters.length === 0) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this._beforeActionFilters.some(filter => {
|
|
62
|
+
if (filter.namespace !== obj.__namespace) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const requestAction = request.toAction.replace(/\s/g, '');
|
|
67
|
+
return filter.actionList.some(action => {
|
|
68
|
+
const filterAction = action.replace(/\s/g, '');
|
|
69
|
+
return filterAction === requestAction;
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Execute all matching before action filters
|
|
75
|
+
// FIXED: Async support, error handling, timeout protection, no variable shadowing
|
|
76
|
+
async __callBeforeAction(obj, request, emitter) {
|
|
77
|
+
if (!this._beforeActionFilters || this._beforeActionFilters.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Find all matching filters
|
|
82
|
+
const requestAction = request.toAction.replace(/\s/g, '');
|
|
83
|
+
const matchingFilters = this._beforeActionFilters.filter(filter => {
|
|
84
|
+
if (filter.namespace !== obj.__namespace) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return filter.actionList.some(actionName => {
|
|
89
|
+
const normalizedAction = actionName.replace(/\s/g, '');
|
|
90
|
+
return normalizedAction === requestAction;
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Execute all matching filters in order
|
|
95
|
+
for (const filter of matchingFilters) {
|
|
96
|
+
try {
|
|
97
|
+
// FIXED: Add timeout protection (5 seconds default)
|
|
98
|
+
await this._executeWithTimeout(
|
|
99
|
+
filter.callBack,
|
|
100
|
+
filter.that,
|
|
101
|
+
[request],
|
|
102
|
+
emitter,
|
|
103
|
+
5000
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// FIXED: Proper error handling
|
|
107
|
+
logger.error({
|
|
108
|
+
code: 'MC_FILTER_ERROR',
|
|
109
|
+
message: 'Error in beforeAction filter',
|
|
110
|
+
namespace: filter.namespace,
|
|
111
|
+
action: requestAction,
|
|
112
|
+
error: error.message,
|
|
113
|
+
stack: error.stack
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Send error response
|
|
117
|
+
const res = request.response;
|
|
118
|
+
if (res && !res._headerSent && !res.headersSent) {
|
|
119
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
120
|
+
res.end(JSON.stringify({
|
|
121
|
+
error: 'Internal Server Error',
|
|
122
|
+
message: master.environmentType === 'development' ? error.message : 'Filter execution failed'
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Don't continue to other filters if one fails
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Execute all matching after action filters
|
|
133
|
+
// FIXED: Async support, error handling, no variable shadowing
|
|
134
|
+
async __callAfterAction(obj, request) {
|
|
135
|
+
if (!this._afterActionFilters || this._afterActionFilters.length === 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Find all matching filters
|
|
140
|
+
const requestAction = request.toAction.replace(/\s/g, '');
|
|
141
|
+
const matchingFilters = this._afterActionFilters.filter(filter => {
|
|
142
|
+
if (filter.namespace !== obj.__namespace) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return filter.actionList.some(actionName => {
|
|
147
|
+
const normalizedAction = actionName.replace(/\s/g, '');
|
|
148
|
+
return normalizedAction === requestAction;
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Execute all matching filters in order
|
|
153
|
+
for (const filter of matchingFilters) {
|
|
154
|
+
try {
|
|
155
|
+
// FIXED: Add timeout protection (5 seconds default)
|
|
156
|
+
await this._executeWithTimeout(
|
|
157
|
+
filter.callBack,
|
|
158
|
+
filter.that,
|
|
159
|
+
[request],
|
|
160
|
+
null,
|
|
161
|
+
5000
|
|
162
|
+
);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// FIXED: Proper error handling
|
|
165
|
+
logger.error({
|
|
166
|
+
code: 'MC_FILTER_ERROR',
|
|
167
|
+
message: 'Error in afterAction filter',
|
|
168
|
+
namespace: filter.namespace,
|
|
169
|
+
action: requestAction,
|
|
170
|
+
error: error.message,
|
|
171
|
+
stack: error.stack
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// After filters don't stop execution, just log
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// FIXED: Execute function with timeout protection
|
|
180
|
+
async _executeWithTimeout(func, context, args, emitter, timeout) {
|
|
181
|
+
// Store emitter in context for next() call
|
|
182
|
+
if (emitter) {
|
|
183
|
+
context.__filterEmitter = emitter;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return Promise.race([
|
|
187
|
+
// Execute the filter
|
|
188
|
+
Promise.resolve(func.call(context, ...args)),
|
|
189
|
+
// Timeout promise
|
|
190
|
+
new Promise((_, reject) =>
|
|
191
|
+
setTimeout(() => reject(new Error(`Filter timeout after ${timeout}ms`)), timeout)
|
|
192
|
+
)
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// FIXED: Request-scoped next() function
|
|
197
|
+
next(){
|
|
198
|
+
if (this.__filterEmitter) {
|
|
199
|
+
this.__filterEmitter.emit("controller");
|
|
200
|
+
} else {
|
|
201
|
+
logger.warn({
|
|
202
|
+
code: 'MC_FILTER_WARN',
|
|
203
|
+
message: 'next() called but no emitter available',
|
|
204
|
+
namespace: this.__namespace
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
96
208
|
}
|
|
97
209
|
|
|
98
|
-
|
|
210
|
+
// Export and lazy register (prevents circular dependency - Spring/Angular pattern)
|
|
211
|
+
module.exports = MasterActionFilters;
|
|
212
|
+
|
|
213
|
+
setImmediate(() => {
|
|
214
|
+
const master = require('./MasterControl');
|
|
215
|
+
if (master && master.extendController) {
|
|
216
|
+
master.extendController(MasterActionFilters);
|
|
217
|
+
}
|
|
218
|
+
});
|