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.
@@ -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 = master.root + location;
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
- var json = JSON.stringify(data);
50
- if (!this.__response._headerSent) {
51
- this.__response.writeHead(200, {'Content-Type': 'application/json'});
52
- this.__response.end(json);
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
- var actionUrl = master.root + location;
59
- var getAction = fileserver.readFileSync(actionUrl, 'utf8');
60
- if(master.overwrite.isTemplate){
61
- return master.overwrite.templateRender( data, "returnPartialView");
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
- else{
64
- return temp.htmlBuilder(getAction, data);
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
- var requestObj = {
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
- var resp = this.__requestObject.response;
124
- var req = this.__requestObject.request;
125
- master.router.currentRoute = {root : `${master.root}/components/${namespace}`, toController : namespace, toAction : action, response : resp, request: req };
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 = {root : `${master.root}/${namespace}`, toController : namespace, toAction : action, response : resp, request: req };
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
- var actionUrl = master.root + location;
180
- var masterView = fileserver.readFileSync(actionUrl, 'utf8');
181
- if (!this.__requestObject.response._headerSent) {
182
- this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
183
- this.__requestObject.response.end(masterView);
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
- const httpsUrl = `https://${this.__requestObject.request.headers.host}${this.__requestObject.pathName}`;
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.extendController(MasterAction);
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
+ });
@@ -1,98 +1,218 @@
1
- // version 1.7
2
- var master = require('./MasterControl');
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
- // add function to list
21
- beforeAction(actionlist, func){
22
- if (typeof func === 'function') {
23
- _beforeActionFunc = {
24
- namespace : this.__namespace,
25
- actionList : actionlist,
26
- callBack : func,
27
- that : this
28
- };
29
- }
30
- else{
31
- master.error.log("beforeAction callback not a function", "warn");
32
- }
33
-
34
- }
35
-
36
- // add function to list
37
- afterAction(actionlist, func){
38
- if (typeof func === 'function') {
39
- _afterActionFunc = {
40
- namespace : this.__namespace,
41
- actionList : actionlist,
42
- callBack : func,
43
- that : this
44
- };
45
- }
46
- else{
47
- master.error.log("afterAction callback not a function", "warn");
48
- }
49
-
50
- }
51
-
52
- // check to see if that controller has a before Action method.
53
- __hasBeforeAction(obj, request){
54
- var flag = false;
55
- if(_beforeActionFunc.namespace === obj.__namespace){
56
- for (var a = 0; a < _beforeActionFunc.actionList.length; a++) {
57
- var action = request.toAction.replace(/\s/g, '');
58
- var incomingAction = _beforeActionFunc.actionList[a].replace(/\s/g, '');
59
- if(incomingAction === action){
60
- flag = true;
61
- }
62
- }
63
- }
64
- return flag;
65
- }
66
-
67
- __callBeforeAction(obj, request, emitter) {
68
- if(_beforeActionFunc.namespace === obj.__namespace){
69
- _beforeActionFunc.actionList.forEach(action => {
70
- var action = action.replace(/\s/g, '');
71
- var reqAction = request.toAction.replace(/\s/g, '');
72
- if(action === reqAction){
73
- emit = emitter;
74
- // call function inside controller
75
- _beforeActionFunc.callBack.call(_beforeActionFunc.that, request);
76
- }
77
- });
78
- };
79
- }
80
-
81
- __callAfterAction(obj, request) {
82
- if(_afterActionFunc.namespace === obj.__namespace){
83
- _afterActionFunc.actionList.forEach(action => {
84
- var action = action.replace(/\s/g, '');
85
- var reqAction = request.toAction.replace(/\s/g, '');
86
- if(action === reqAction){
87
- _afterActionFunc.callBack.call(_afterActionFunc.that, request);
88
- }
89
- });
90
- };
91
- }
92
-
93
- next(){
94
- emit.emit("controller");
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
- master.extendController( MasterActionFilters);
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
+ });