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
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ MasterController is a lightweight MVC-style server framework for Node.js with mi
|
|
|
14
14
|
- [CORS](#cors)
|
|
15
15
|
- [Sessions](#sessions)
|
|
16
16
|
- [Security](#security)
|
|
17
|
+
- [File Conversion & Binary Data](#file-conversion--binary-data)
|
|
17
18
|
- [Components](#components)
|
|
18
19
|
- [Timeout System](#timeout-system)
|
|
19
20
|
- [Error Handling](#error-handling)
|
|
@@ -55,7 +56,13 @@ const cors = require('./cors.json');
|
|
|
55
56
|
master.cors.init(cors);
|
|
56
57
|
|
|
57
58
|
// Initialize sessions (auto-registers with pipeline)
|
|
58
|
-
master.
|
|
59
|
+
master.session.init({
|
|
60
|
+
cookieName: 'mc_session',
|
|
61
|
+
maxAge: 3600000,
|
|
62
|
+
httpOnly: true,
|
|
63
|
+
secure: true,
|
|
64
|
+
sameSite: 'strict'
|
|
65
|
+
});
|
|
59
66
|
|
|
60
67
|
// Auto-discover custom middleware from middleware/ folder
|
|
61
68
|
master.pipeline.discoverMiddleware('middleware');
|
|
@@ -790,11 +797,9 @@ master.cors.init({
|
|
|
790
797
|
|
|
791
798
|
## Sessions
|
|
792
799
|
|
|
793
|
-
MasterController provides
|
|
794
|
-
- **`master.session`** (NEW) - Secure, Rails/Django-style sessions with automatic regeneration and protection (RECOMMENDED)
|
|
795
|
-
- **`master.sessions`** (LEGACY) - Original cookie-based session API (backward compatibility only)
|
|
800
|
+
MasterController provides secure, Rails/Django-style sessions with automatic regeneration and protection.
|
|
796
801
|
|
|
797
|
-
### Secure Sessions
|
|
802
|
+
### Secure Sessions
|
|
798
803
|
|
|
799
804
|
#### `master.session.init(options)`
|
|
800
805
|
|
|
@@ -914,105 +919,6 @@ master.session.init(settings);
|
|
|
914
919
|
- MaxAge: 24 hours (convenient for development)
|
|
915
920
|
- RegenerateInterval: 1 hour
|
|
916
921
|
|
|
917
|
-
---
|
|
918
|
-
|
|
919
|
-
### Legacy Sessions (Backward Compatibility)
|
|
920
|
-
|
|
921
|
-
**⚠️ DEPRECATED: Use `master.session` (singular) for new projects.**
|
|
922
|
-
|
|
923
|
-
The original `master.sessions` (plural) API is maintained for backward compatibility but lacks modern security features.
|
|
924
|
-
|
|
925
|
-
#### `master.sessions.init(options)`
|
|
926
|
-
|
|
927
|
-
Initialize legacy sessions (auto-registers with middleware pipeline).
|
|
928
|
-
|
|
929
|
-
```javascript
|
|
930
|
-
master.sessions.init({
|
|
931
|
-
secret: 'your-secret-key',
|
|
932
|
-
maxAge: 900000, // 15 minutes
|
|
933
|
-
httpOnly: true,
|
|
934
|
-
secure: true, // HTTPS only
|
|
935
|
-
sameSite: 'strict', // Must be string: 'strict', 'lax', or 'none'
|
|
936
|
-
path: '/'
|
|
937
|
-
});
|
|
938
|
-
```
|
|
939
|
-
|
|
940
|
-
#### Legacy Session API
|
|
941
|
-
|
|
942
|
-
**`master.sessions.set(name, data, response, secret, options)`** - Create a session
|
|
943
|
-
|
|
944
|
-
```javascript
|
|
945
|
-
master.sessions.set('user', userData, obj.response);
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
**`master.sessions.get(name, request, secret)`** - Retrieve session data
|
|
949
|
-
|
|
950
|
-
```javascript
|
|
951
|
-
const user = master.sessions.get('user', obj.request);
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
**`master.sessions.delete(name, response)`** - Delete a session
|
|
955
|
-
|
|
956
|
-
```javascript
|
|
957
|
-
master.sessions.delete('user', obj.response);
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
**`master.sessions.reset()`** - Clear all sessions
|
|
961
|
-
|
|
962
|
-
```javascript
|
|
963
|
-
master.sessions.reset();
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
#### Legacy Cookie Methods
|
|
967
|
-
|
|
968
|
-
**`master.sessions.setCookie(name, value, response, options)`**
|
|
969
|
-
```javascript
|
|
970
|
-
master.sessions.setCookie('theme', 'dark', obj.response);
|
|
971
|
-
```
|
|
972
|
-
|
|
973
|
-
**`master.sessions.getCookie(name, request, secret)`**
|
|
974
|
-
```javascript
|
|
975
|
-
const theme = master.sessions.getCookie('theme', obj.request);
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
**`master.sessions.deleteCookie(name, response, options)`**
|
|
979
|
-
```javascript
|
|
980
|
-
master.sessions.deleteCookie('theme', obj.response);
|
|
981
|
-
```
|
|
982
|
-
|
|
983
|
-
#### Migration Guide: Legacy → Secure Sessions
|
|
984
|
-
|
|
985
|
-
**Old (master.sessions):**
|
|
986
|
-
```javascript
|
|
987
|
-
// Set
|
|
988
|
-
master.sessions.set('user', userData, obj.response);
|
|
989
|
-
|
|
990
|
-
// Get
|
|
991
|
-
const user = master.sessions.get('user', obj.request);
|
|
992
|
-
|
|
993
|
-
// Delete
|
|
994
|
-
master.sessions.delete('user', obj.response);
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
**New (master.session):**
|
|
998
|
-
```javascript
|
|
999
|
-
// Set (Rails/Express style)
|
|
1000
|
-
obj.request.session.user = userData;
|
|
1001
|
-
|
|
1002
|
-
// Get
|
|
1003
|
-
const user = obj.request.session.user;
|
|
1004
|
-
|
|
1005
|
-
// Delete
|
|
1006
|
-
master.session.destroy(obj.request, obj.response);
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
**Benefits of migration:**
|
|
1010
|
-
- ✅ Automatic session regeneration (prevents fixation)
|
|
1011
|
-
- ✅ 32-byte session IDs (stronger than 20-byte)
|
|
1012
|
-
- ✅ Rolling sessions (better UX)
|
|
1013
|
-
- ✅ Automatic cleanup (no memory leaks)
|
|
1014
|
-
- ✅ Rails/Express-style API (more familiar)
|
|
1015
|
-
- ✅ No broken encryption (legacy has crypto bugs)
|
|
1016
922
|
|
|
1017
923
|
---
|
|
1018
924
|
|
|
@@ -1123,220 +1029,1025 @@ class UsersController {
|
|
|
1123
1029
|
- `detectSQLInjection(input)` - Detect SQL injection
|
|
1124
1030
|
- `detectCommandInjection(input)` - Detect command injection
|
|
1125
1031
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
## Components
|
|
1032
|
+
### File Upload Security
|
|
1129
1033
|
|
|
1130
|
-
|
|
1034
|
+
MasterController v1.3.1 includes built-in protection against file upload attacks and DoS.
|
|
1131
1035
|
|
|
1132
|
-
|
|
1036
|
+
#### Request Body Size Limits
|
|
1133
1037
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1038
|
+
**config/initializers/request.json:**
|
|
1039
|
+
```json
|
|
1040
|
+
{
|
|
1041
|
+
"disableFormidableMultipartFormData": false,
|
|
1042
|
+
"formidable": {
|
|
1043
|
+
"multiples": true,
|
|
1044
|
+
"keepExtensions": true,
|
|
1045
|
+
"maxFileSize": 10485760, // 10MB per file
|
|
1046
|
+
"maxFieldsSize": 2097152, // 2MB total form fields
|
|
1047
|
+
"maxFields": 1000, // Max number of fields
|
|
1048
|
+
"allowEmptyFiles": false, // Reject empty files
|
|
1049
|
+
"minFileSize": 1 // Reject 0-byte files
|
|
1050
|
+
},
|
|
1051
|
+
"maxBodySize": 10485760, // 10MB for form-urlencoded
|
|
1052
|
+
"maxJsonSize": 1048576, // 1MB for JSON payloads
|
|
1053
|
+
"maxTextSize": 1048576 // 1MB for text/plain
|
|
1054
|
+
}
|
|
1149
1055
|
```
|
|
1150
1056
|
|
|
1151
|
-
|
|
1057
|
+
**DoS Protection:**
|
|
1058
|
+
- All request bodies are size-limited (prevents memory exhaustion)
|
|
1059
|
+
- Connections destroyed if limits exceeded
|
|
1060
|
+
- Configurable per content-type
|
|
1152
1061
|
|
|
1153
|
-
|
|
1154
|
-
// In config/initializers/config.js
|
|
1155
|
-
master.component('components', 'user');
|
|
1156
|
-
master.component('components', 'mail');
|
|
1157
|
-
```
|
|
1062
|
+
#### File Type Validation
|
|
1158
1063
|
|
|
1159
|
-
|
|
1064
|
+
**Always validate file types in your controllers:**
|
|
1160
1065
|
|
|
1161
1066
|
```javascript
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1067
|
+
class UploadController {
|
|
1068
|
+
uploadImage(obj) {
|
|
1069
|
+
const file = obj.params.formData.files.avatar[0];
|
|
1165
1070
|
|
|
1166
|
-
|
|
1071
|
+
// 1. Validate MIME type
|
|
1072
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
1073
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
1074
|
+
this.json({ error: 'Only images allowed (JPEG, PNG, GIF, WebP)' });
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1167
1077
|
|
|
1168
|
-
|
|
1078
|
+
// 2. Validate file extension
|
|
1079
|
+
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
|
1080
|
+
if (!allowedExts.includes(file.extension.toLowerCase())) {
|
|
1081
|
+
this.json({ error: 'Invalid file extension' });
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1169
1084
|
|
|
1170
|
-
|
|
1085
|
+
// 3. Validate file size (additional check)
|
|
1086
|
+
const maxSize = 5 * 1024 * 1024; // 5MB
|
|
1087
|
+
if (file.size > maxSize) {
|
|
1088
|
+
this.json({ error: 'File too large (max 5MB)' });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1171
1091
|
|
|
1172
|
-
|
|
1092
|
+
// 4. Generate safe filename (prevent path traversal)
|
|
1093
|
+
const crypto = require('crypto');
|
|
1094
|
+
const safeFilename = crypto.randomBytes(16).toString('hex') + file.extension;
|
|
1095
|
+
const uploadPath = path.join(master.root, 'uploads', safeFilename);
|
|
1173
1096
|
|
|
1174
|
-
|
|
1097
|
+
// 5. Move file
|
|
1098
|
+
fs.renameSync(file.filepath, uploadPath);
|
|
1175
1099
|
|
|
1176
|
-
|
|
1177
|
-
// config/initializers/config.js
|
|
1178
|
-
master.timeout.init({
|
|
1179
|
-
globalTimeout: 120000, // 120 seconds (2 minutes) default
|
|
1180
|
-
enabled: true,
|
|
1181
|
-
onTimeout: (ctx, timeoutInfo) => {
|
|
1182
|
-
// Optional custom timeout handler
|
|
1183
|
-
console.log(`Request timeout: ${timeoutInfo.path}`);
|
|
1100
|
+
this.json({ success: true, filename: safeFilename });
|
|
1184
1101
|
}
|
|
1185
|
-
});
|
|
1186
1102
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
```
|
|
1103
|
+
uploadDocument(obj) {
|
|
1104
|
+
const file = obj.params.formData.files.document[0];
|
|
1190
1105
|
|
|
1191
|
-
|
|
1106
|
+
// Allow PDF, DOC, DOCX only
|
|
1107
|
+
const allowedTypes = [
|
|
1108
|
+
'application/pdf',
|
|
1109
|
+
'application/msword',
|
|
1110
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
1111
|
+
];
|
|
1192
1112
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1113
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
1114
|
+
this.json({ error: 'Only PDF and Word documents allowed' });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Process upload...
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1196
1122
|
|
|
1197
|
-
|
|
1198
|
-
master.timeout.setRouteTimeout('/admin/reports', 300000); // 5 minutes
|
|
1123
|
+
#### Formidable Custom Filter
|
|
1199
1124
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1125
|
+
**Add file filter in request.json (formidable v3+):**
|
|
1126
|
+
|
|
1127
|
+
```json
|
|
1128
|
+
{
|
|
1129
|
+
"formidable": {
|
|
1130
|
+
"filter": "function({ name, originalFilename, mimetype }) { return mimetype && mimetype.startsWith('image/'); }"
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1202
1133
|
```
|
|
1203
1134
|
|
|
1204
|
-
|
|
1135
|
+
**Note:** JSON doesn't support functions, so filters must be configured in code:
|
|
1205
1136
|
|
|
1206
1137
|
```javascript
|
|
1207
|
-
|
|
1138
|
+
// config/initializers/config.js
|
|
1139
|
+
const formidableOptions = master.env.request.formidable;
|
|
1208
1140
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
// {
|
|
1219
|
-
// requestId: 'req_1234567890_abc123',
|
|
1220
|
-
// path: 'api/users',
|
|
1221
|
-
// method: 'get',
|
|
1222
|
-
// timeout: 30000,
|
|
1223
|
-
// elapsed: 15000,
|
|
1224
|
-
// remaining: 15000
|
|
1225
|
-
// }
|
|
1226
|
-
// ]
|
|
1227
|
-
// }
|
|
1141
|
+
// Add runtime filter for images only
|
|
1142
|
+
formidableOptions.filter = function({ name, originalFilename, mimetype }) {
|
|
1143
|
+
return mimetype && mimetype.startsWith('image/');
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
master.request.init({
|
|
1147
|
+
...master.env.request,
|
|
1148
|
+
formidable: formidableOptions
|
|
1149
|
+
});
|
|
1228
1150
|
```
|
|
1229
1151
|
|
|
1230
|
-
|
|
1152
|
+
#### Security Best Practices
|
|
1153
|
+
|
|
1154
|
+
1. **Always validate both MIME type AND file extension** (double check)
|
|
1155
|
+
2. **Generate random filenames** (prevents overwriting and path traversal)
|
|
1156
|
+
3. **Store uploads outside public directory** (prevent direct execution)
|
|
1157
|
+
4. **Scan files for viruses** (use ClamAV or similar)
|
|
1158
|
+
5. **Set proper file permissions** (chmod 644 for files, 755 for dirs)
|
|
1159
|
+
6. **Never trust user-provided filenames** (can contain `../` or null bytes)
|
|
1160
|
+
7. **Limit file sizes** (prevent disk space exhaustion)
|
|
1161
|
+
8. **Delete temporary files** after processing
|
|
1162
|
+
|
|
1163
|
+
#### Delete Temporary Files
|
|
1231
1164
|
|
|
1232
1165
|
```javascript
|
|
1233
|
-
|
|
1234
|
-
|
|
1166
|
+
class UploadController {
|
|
1167
|
+
upload(obj) {
|
|
1168
|
+
const file = obj.params.formData.files.upload[0];
|
|
1235
1169
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1170
|
+
try {
|
|
1171
|
+
// Validate and process...
|
|
1172
|
+
|
|
1173
|
+
// Delete temp file after processing
|
|
1174
|
+
master.request.deleteFileBuffer(file.filepath);
|
|
1175
|
+
|
|
1176
|
+
this.json({ success: true });
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
// Always cleanup on error
|
|
1179
|
+
master.request.deleteFileBuffer(file.filepath);
|
|
1180
|
+
this.json({ error: error.message });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1238
1184
|
```
|
|
1239
1185
|
|
|
1240
1186
|
---
|
|
1241
1187
|
|
|
1242
|
-
##
|
|
1188
|
+
## File Conversion & Binary Data
|
|
1243
1189
|
|
|
1244
|
-
MasterController
|
|
1190
|
+
MasterController v1.3.1 includes production-grade utilities for converting between files, base64, and binary data. These are essential for working with uploaded files, API responses, and data storage.
|
|
1245
1191
|
|
|
1246
|
-
###
|
|
1192
|
+
### Quick Start
|
|
1247
1193
|
|
|
1248
1194
|
```javascript
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
showStackTrace: master.environmentType === 'development' // Dev only
|
|
1254
|
-
});
|
|
1255
|
-
```
|
|
1195
|
+
// Convert uploaded file to base64 for API response
|
|
1196
|
+
class UploadController {
|
|
1197
|
+
uploadImage(obj) {
|
|
1198
|
+
const file = obj.params.formData.files.image[0];
|
|
1256
1199
|
|
|
1257
|
-
|
|
1200
|
+
// Convert to base64 (with data URI for <img> src)
|
|
1201
|
+
const base64 = master.tools.fileToBase64(file, {
|
|
1202
|
+
includeDataURI: true, // Adds "data:image/jpeg;base64," prefix
|
|
1203
|
+
maxSize: 5 * 1024 * 1024 // 5MB limit
|
|
1204
|
+
});
|
|
1258
1205
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if (!isAuthenticated(ctx)) {
|
|
1263
|
-
master.errorRenderer.send(ctx, 401, {
|
|
1264
|
-
message: 'Please log in to access this resource',
|
|
1265
|
-
suggestions: [
|
|
1266
|
-
'Sign in with your credentials',
|
|
1267
|
-
'Request a password reset if forgotten'
|
|
1268
|
-
]
|
|
1206
|
+
this.json({
|
|
1207
|
+
success: true,
|
|
1208
|
+
imageData: base64 // Can be used directly in <img src="">
|
|
1269
1209
|
});
|
|
1270
|
-
return;
|
|
1271
1210
|
}
|
|
1272
|
-
|
|
1211
|
+
}
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### File to Base64
|
|
1215
|
+
|
|
1216
|
+
#### `master.tools.fileToBase64(filePathOrFile, options)`
|
|
1217
|
+
|
|
1218
|
+
Convert a file to base64 string (binary-safe for all file types).
|
|
1219
|
+
|
|
1220
|
+
**Parameters:**
|
|
1221
|
+
- `filePathOrFile`: File path string OR formidable file object
|
|
1222
|
+
- `options`:
|
|
1223
|
+
- `includeDataURI` (boolean) - Prepend data URI (e.g., `data:image/jpeg;base64,`)
|
|
1224
|
+
- `maxSize` (number) - Maximum file size in bytes (default: 10MB)
|
|
1225
|
+
|
|
1226
|
+
**Returns:** Base64 string
|
|
1227
|
+
|
|
1228
|
+
**Examples:**
|
|
1229
|
+
|
|
1230
|
+
```javascript
|
|
1231
|
+
// Convert file from file path
|
|
1232
|
+
const base64 = master.tools.fileToBase64('/path/to/image.jpg');
|
|
1233
|
+
|
|
1234
|
+
// Convert uploaded file with data URI
|
|
1235
|
+
const file = obj.params.formData.files.avatar[0];
|
|
1236
|
+
const dataURI = master.tools.fileToBase64(file, {
|
|
1237
|
+
includeDataURI: true,
|
|
1238
|
+
maxSize: 5 * 1024 * 1024 // 5MB
|
|
1273
1239
|
});
|
|
1274
1240
|
|
|
1275
|
-
//
|
|
1276
|
-
|
|
1277
|
-
async show(obj) {
|
|
1278
|
-
const userId = obj.params.userId;
|
|
1279
|
-
const user = await this.db.query('SELECT * FROM users WHERE id = ?', [userId]);
|
|
1241
|
+
// Use in HTML email or response
|
|
1242
|
+
const html = `<img src="${dataURI}" alt="Avatar">`;
|
|
1280
1243
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
suggestions: [
|
|
1285
|
-
'Check the user ID',
|
|
1286
|
-
'Browse all users',
|
|
1287
|
-
'Search for the user by name'
|
|
1288
|
-
]
|
|
1289
|
-
});
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1244
|
+
// Store in database
|
|
1245
|
+
await db.query('UPDATE users SET avatar = ? WHERE id = ?', [base64, userId]);
|
|
1246
|
+
```
|
|
1292
1247
|
|
|
1293
|
-
|
|
1248
|
+
**Error Handling:**
|
|
1249
|
+
|
|
1250
|
+
```javascript
|
|
1251
|
+
try {
|
|
1252
|
+
const base64 = master.tools.fileToBase64(file);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
if (error.message.includes('not found')) {
|
|
1255
|
+
console.error('File does not exist');
|
|
1256
|
+
} else if (error.message.includes('exceeds maximum')) {
|
|
1257
|
+
console.error('File too large');
|
|
1258
|
+
} else if (error.message.includes('directory')) {
|
|
1259
|
+
console.error('Path is a directory, not a file');
|
|
1294
1260
|
}
|
|
1295
1261
|
}
|
|
1296
1262
|
```
|
|
1297
1263
|
|
|
1298
|
-
|
|
1264
|
+
---
|
|
1299
1265
|
|
|
1300
|
-
|
|
1266
|
+
### Base64 to File
|
|
1301
1267
|
|
|
1302
|
-
|
|
1303
|
-
public/errors/
|
|
1304
|
-
├── 400.html # Bad Request
|
|
1305
|
-
├── 401.html # Unauthorized
|
|
1306
|
-
├── 403.html # Forbidden
|
|
1307
|
-
├── 404.html # Not Found
|
|
1308
|
-
├── 405.html # Method Not Allowed
|
|
1309
|
-
├── 422.html # Unprocessable Entity
|
|
1310
|
-
├── 429.html # Too Many Requests
|
|
1311
|
-
├── 500.html # Internal Server Error
|
|
1312
|
-
├── 502.html # Bad Gateway
|
|
1313
|
-
├── 503.html # Service Unavailable
|
|
1314
|
-
└── 504.html # Gateway Timeout
|
|
1315
|
-
```
|
|
1268
|
+
#### `master.tools.base64ToFile(base64String, outputPath, options)`
|
|
1316
1269
|
|
|
1317
|
-
|
|
1270
|
+
Convert base64 string to a file on disk (binary-safe).
|
|
1318
1271
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
<body>
|
|
1326
|
-
<h1>{{statusCode}} - {{title}}</h1>
|
|
1327
|
-
<p>{{message}}</p>
|
|
1272
|
+
**Parameters:**
|
|
1273
|
+
- `base64String`: Base64 encoded string (with or without data URI prefix)
|
|
1274
|
+
- `outputPath`: Destination file path
|
|
1275
|
+
- `options`:
|
|
1276
|
+
- `overwrite` (boolean) - Allow overwriting existing files (default: false)
|
|
1277
|
+
- `createDir` (boolean) - Create parent directories if needed (default: true)
|
|
1328
1278
|
|
|
1329
|
-
|
|
1330
|
-
{{#if showStackTrace}}
|
|
1331
|
-
<pre>{{stack}}</pre>
|
|
1332
|
-
{{/if}}
|
|
1279
|
+
**Returns:** `{ success: true, filePath: outputPath, size: number }`
|
|
1333
1280
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1281
|
+
**Examples:**
|
|
1282
|
+
|
|
1283
|
+
```javascript
|
|
1284
|
+
// Save base64 from API to file
|
|
1285
|
+
class ApiController {
|
|
1286
|
+
async saveImage(obj) {
|
|
1287
|
+
const base64Data = obj.params.formData.imageData;
|
|
1288
|
+
|
|
1289
|
+
// Save to disk
|
|
1290
|
+
const result = master.tools.base64ToFile(
|
|
1291
|
+
base64Data,
|
|
1292
|
+
'./uploads/images/photo.jpg',
|
|
1293
|
+
{ overwrite: false, createDir: true }
|
|
1294
|
+
);
|
|
1295
|
+
|
|
1296
|
+
this.json({
|
|
1297
|
+
success: true,
|
|
1298
|
+
path: result.filePath,
|
|
1299
|
+
size: result.size
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Data URI with prefix (automatically handled)
|
|
1305
|
+
const dataURI = 'data:image/png;base64,iVBORw0KGgoAAAANS...';
|
|
1306
|
+
master.tools.base64ToFile(dataURI, './output.png');
|
|
1307
|
+
|
|
1308
|
+
// Pure base64 without prefix
|
|
1309
|
+
const pureBase64 = 'iVBORw0KGgoAAAANS...';
|
|
1310
|
+
master.tools.base64ToFile(pureBase64, './output.png');
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
---
|
|
1314
|
+
|
|
1315
|
+
### Buffer Operations
|
|
1316
|
+
|
|
1317
|
+
#### `master.tools.fileToBuffer(filePathOrFile, options)`
|
|
1318
|
+
|
|
1319
|
+
Convert file to Node.js Buffer (for in-memory processing).
|
|
1320
|
+
|
|
1321
|
+
**Parameters:**
|
|
1322
|
+
- `filePathOrFile`: File path string OR formidable file object
|
|
1323
|
+
- `options`:
|
|
1324
|
+
- `maxSize` (number) - Maximum file size (default: 10MB)
|
|
1325
|
+
|
|
1326
|
+
**Returns:** Node.js Buffer
|
|
1327
|
+
|
|
1328
|
+
**Examples:**
|
|
1329
|
+
|
|
1330
|
+
```javascript
|
|
1331
|
+
// Read file into buffer
|
|
1332
|
+
const buffer = master.tools.fileToBuffer('./image.jpg');
|
|
1333
|
+
|
|
1334
|
+
// Process image with sharp library
|
|
1335
|
+
const sharp = require('sharp');
|
|
1336
|
+
const resized = await sharp(buffer)
|
|
1337
|
+
.resize(800, 600)
|
|
1338
|
+
.toBuffer();
|
|
1339
|
+
|
|
1340
|
+
// Convert buffer back to base64
|
|
1341
|
+
const base64 = master.tools.bytesToBase64(resized);
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
---
|
|
1345
|
+
|
|
1346
|
+
#### `master.tools.fileToBytes(filePathOrFile, options)`
|
|
1347
|
+
|
|
1348
|
+
Convert file to Uint8Array (for Web APIs and TypedArrays).
|
|
1349
|
+
|
|
1350
|
+
**Parameters:**
|
|
1351
|
+
- `filePathOrFile`: File path string OR formidable file object
|
|
1352
|
+
- `options`:
|
|
1353
|
+
- `maxSize` (number) - Maximum file size (default: 10MB)
|
|
1354
|
+
|
|
1355
|
+
**Returns:** Uint8Array
|
|
1356
|
+
|
|
1357
|
+
**Examples:**
|
|
1358
|
+
|
|
1359
|
+
```javascript
|
|
1360
|
+
// Get raw bytes
|
|
1361
|
+
const bytes = master.tools.fileToBytes('./document.pdf');
|
|
1362
|
+
|
|
1363
|
+
// Send over WebSocket as binary
|
|
1364
|
+
websocket.send(bytes);
|
|
1365
|
+
|
|
1366
|
+
// Use with crypto
|
|
1367
|
+
const crypto = require('crypto');
|
|
1368
|
+
const hash = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
---
|
|
1372
|
+
|
|
1373
|
+
#### `master.tools.bytesToBase64(bufferOrBytes, options)`
|
|
1374
|
+
|
|
1375
|
+
Convert Buffer or Uint8Array to base64 string.
|
|
1376
|
+
|
|
1377
|
+
**Parameters:**
|
|
1378
|
+
- `bufferOrBytes`: Node.js Buffer OR Uint8Array
|
|
1379
|
+
- `options`:
|
|
1380
|
+
- `includeDataURI` (boolean) - Prepend data URI
|
|
1381
|
+
- `mimetype` (string) - MIME type for data URI (required if includeDataURI=true)
|
|
1382
|
+
|
|
1383
|
+
**Returns:** Base64 string
|
|
1384
|
+
|
|
1385
|
+
**Examples:**
|
|
1386
|
+
|
|
1387
|
+
```javascript
|
|
1388
|
+
const buffer = Buffer.from('Hello World');
|
|
1389
|
+
const base64 = master.tools.bytesToBase64(buffer);
|
|
1390
|
+
// → 'SGVsbG8gV29ybGQ='
|
|
1391
|
+
|
|
1392
|
+
// With data URI
|
|
1393
|
+
const base64WithURI = master.tools.bytesToBase64(buffer, {
|
|
1394
|
+
includeDataURI: true,
|
|
1395
|
+
mimetype: 'text/plain'
|
|
1396
|
+
});
|
|
1397
|
+
// → 'data:text/plain;base64,SGVsbG8gV29ybGQ='
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
---
|
|
1401
|
+
|
|
1402
|
+
#### `master.tools.base64ToBytes(base64String)`
|
|
1403
|
+
|
|
1404
|
+
Convert base64 string to Node.js Buffer.
|
|
1405
|
+
|
|
1406
|
+
**Parameters:**
|
|
1407
|
+
- `base64String`: Base64 string (with or without data URI prefix)
|
|
1408
|
+
|
|
1409
|
+
**Returns:** Node.js Buffer
|
|
1410
|
+
|
|
1411
|
+
**Examples:**
|
|
1412
|
+
|
|
1413
|
+
```javascript
|
|
1414
|
+
const base64 = 'SGVsbG8gV29ybGQ=';
|
|
1415
|
+
const buffer = master.tools.base64ToBytes(base64);
|
|
1416
|
+
console.log(buffer.toString('utf8')); // → 'Hello World'
|
|
1417
|
+
|
|
1418
|
+
// Handles data URIs automatically
|
|
1419
|
+
const dataURI = 'data:text/plain;base64,SGVsbG8gV29ybGQ=';
|
|
1420
|
+
const buffer2 = master.tools.base64ToBytes(dataURI);
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
---
|
|
1424
|
+
|
|
1425
|
+
### Streaming Large Files
|
|
1426
|
+
|
|
1427
|
+
#### `master.tools.streamFileToBase64(filePathOrFile, options)`
|
|
1428
|
+
|
|
1429
|
+
Stream large files to base64 without loading into memory (async).
|
|
1430
|
+
|
|
1431
|
+
**Parameters:**
|
|
1432
|
+
- `filePathOrFile`: File path string OR formidable file object
|
|
1433
|
+
- `options`:
|
|
1434
|
+
- `includeDataURI` (boolean) - Prepend data URI
|
|
1435
|
+
- `chunkSize` (number) - Read chunk size (default: 64KB)
|
|
1436
|
+
- `onProgress` (function) - Progress callback: `(bytesRead, totalBytes, percent) => {}`
|
|
1437
|
+
|
|
1438
|
+
**Returns:** Promise<base64 string>
|
|
1439
|
+
|
|
1440
|
+
**Examples:**
|
|
1441
|
+
|
|
1442
|
+
```javascript
|
|
1443
|
+
// Stream large video file to base64
|
|
1444
|
+
class VideoController {
|
|
1445
|
+
async processVideo(obj) {
|
|
1446
|
+
const file = obj.params.formData.files.video[0];
|
|
1447
|
+
|
|
1448
|
+
// Stream with progress tracking
|
|
1449
|
+
const base64 = await master.tools.streamFileToBase64(file, {
|
|
1450
|
+
includeDataURI: true,
|
|
1451
|
+
chunkSize: 128 * 1024, // 128KB chunks
|
|
1452
|
+
onProgress: (bytesRead, total, percent) => {
|
|
1453
|
+
console.log(`Processing: ${percent.toFixed(1)}% (${bytesRead}/${total} bytes)`);
|
|
1454
|
+
|
|
1455
|
+
// Send progress to client via WebSocket
|
|
1456
|
+
master.socket.emit('upload-progress', { percent });
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
this.json({ success: true, videoData: base64 });
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Process 500MB file without memory issues
|
|
1465
|
+
const largeFile = '/path/to/500mb-video.mp4';
|
|
1466
|
+
const base64 = await master.tools.streamFileToBase64(largeFile, {
|
|
1467
|
+
onProgress: (read, total, percent) => {
|
|
1468
|
+
console.log(`${percent.toFixed(1)}% complete`);
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
---
|
|
1474
|
+
|
|
1475
|
+
### Common Use Cases
|
|
1476
|
+
|
|
1477
|
+
#### Use Case 1: API Response with Embedded Image
|
|
1478
|
+
|
|
1479
|
+
```javascript
|
|
1480
|
+
class ProductController {
|
|
1481
|
+
show(obj) {
|
|
1482
|
+
const product = db.getProduct(obj.params.id);
|
|
1483
|
+
const imagePath = `./uploads/products/${product.imageFilename}`;
|
|
1484
|
+
|
|
1485
|
+
// Convert image to base64 for API
|
|
1486
|
+
const imageData = master.tools.fileToBase64(imagePath, {
|
|
1487
|
+
includeDataURI: true,
|
|
1488
|
+
maxSize: 2 * 1024 * 1024 // 2MB limit
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
this.json({
|
|
1492
|
+
id: product.id,
|
|
1493
|
+
name: product.name,
|
|
1494
|
+
image: imageData // Client can use directly in <img src="">
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
#### Use Case 2: Store File in Database
|
|
1501
|
+
|
|
1502
|
+
```javascript
|
|
1503
|
+
class DocumentController {
|
|
1504
|
+
async upload(obj) {
|
|
1505
|
+
const file = obj.params.formData.files.document[0];
|
|
1506
|
+
|
|
1507
|
+
// Validate file type
|
|
1508
|
+
const allowedTypes = ['application/pdf', 'application/msword'];
|
|
1509
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
1510
|
+
this.json({ error: 'Only PDF and Word documents allowed' });
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// Convert to base64 for database storage
|
|
1515
|
+
const base64 = master.tools.fileToBase64(file, {
|
|
1516
|
+
maxSize: 10 * 1024 * 1024 // 10MB
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
// Store in database
|
|
1520
|
+
await this.db.query(
|
|
1521
|
+
'INSERT INTO documents (filename, mimetype, data) VALUES (?, ?, ?)',
|
|
1522
|
+
[file.originalFilename, file.mimetype, base64]
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1525
|
+
// Delete temp file
|
|
1526
|
+
master.request.deleteFileBuffer(file.filepath);
|
|
1527
|
+
|
|
1528
|
+
this.json({ success: true });
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
#### Use Case 3: Retrieve File from Database
|
|
1534
|
+
|
|
1535
|
+
```javascript
|
|
1536
|
+
class DocumentController {
|
|
1537
|
+
async download(obj) {
|
|
1538
|
+
const docId = obj.params.id;
|
|
1539
|
+
|
|
1540
|
+
// Get from database
|
|
1541
|
+
const doc = await this.db.query(
|
|
1542
|
+
'SELECT filename, mimetype, data FROM documents WHERE id = ?',
|
|
1543
|
+
[docId]
|
|
1544
|
+
);
|
|
1545
|
+
|
|
1546
|
+
if (!doc) {
|
|
1547
|
+
master.errorRenderer.send(obj, 404, {
|
|
1548
|
+
message: 'Document not found'
|
|
1549
|
+
});
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Convert base64 back to file
|
|
1554
|
+
const tempPath = `./temp/${Date.now()}-${doc.filename}`;
|
|
1555
|
+
master.tools.base64ToFile(doc.data, tempPath);
|
|
1556
|
+
|
|
1557
|
+
// Send file to client
|
|
1558
|
+
obj.response.setHeader('Content-Type', doc.mimetype);
|
|
1559
|
+
obj.response.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
|
|
1560
|
+
|
|
1561
|
+
const fs = require('fs');
|
|
1562
|
+
const fileStream = fs.createReadStream(tempPath);
|
|
1563
|
+
fileStream.pipe(obj.response);
|
|
1564
|
+
|
|
1565
|
+
// Cleanup after sending
|
|
1566
|
+
fileStream.on('end', () => {
|
|
1567
|
+
fs.unlinkSync(tempPath);
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
#### Use Case 4: Image Processing Pipeline
|
|
1574
|
+
|
|
1575
|
+
```javascript
|
|
1576
|
+
const sharp = require('sharp');
|
|
1577
|
+
|
|
1578
|
+
class ImageController {
|
|
1579
|
+
async processThumbnail(obj) {
|
|
1580
|
+
const file = obj.params.formData.files.image[0];
|
|
1581
|
+
|
|
1582
|
+
// Read file to buffer
|
|
1583
|
+
const buffer = master.tools.fileToBuffer(file, {
|
|
1584
|
+
maxSize: 10 * 1024 * 1024
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
// Process with sharp
|
|
1588
|
+
const thumbnail = await sharp(buffer)
|
|
1589
|
+
.resize(200, 200, { fit: 'cover' })
|
|
1590
|
+
.jpeg({ quality: 80 })
|
|
1591
|
+
.toBuffer();
|
|
1592
|
+
|
|
1593
|
+
// Convert thumbnail to base64
|
|
1594
|
+
const base64 = master.tools.bytesToBase64(thumbnail, {
|
|
1595
|
+
includeDataURI: true,
|
|
1596
|
+
mimetype: 'image/jpeg'
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
// Cleanup temp file
|
|
1600
|
+
master.request.deleteFileBuffer(file.filepath);
|
|
1601
|
+
|
|
1602
|
+
this.json({
|
|
1603
|
+
success: true,
|
|
1604
|
+
thumbnail: base64
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
#### Use Case 5: Email with Embedded Images
|
|
1611
|
+
|
|
1612
|
+
```javascript
|
|
1613
|
+
const nodemailer = require('nodemailer');
|
|
1614
|
+
|
|
1615
|
+
class EmailController {
|
|
1616
|
+
async sendWithImage(obj) {
|
|
1617
|
+
const file = obj.params.formData.files.logo[0];
|
|
1618
|
+
|
|
1619
|
+
// Convert to base64 data URI
|
|
1620
|
+
const logoData = master.tools.fileToBase64(file, {
|
|
1621
|
+
includeDataURI: true
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
// Send email with embedded image
|
|
1625
|
+
const transporter = nodemailer.createTransport({/* config */});
|
|
1626
|
+
await transporter.sendMail({
|
|
1627
|
+
to: 'user@example.com',
|
|
1628
|
+
subject: 'Welcome!',
|
|
1629
|
+
html: `
|
|
1630
|
+
<h1>Welcome to our platform!</h1>
|
|
1631
|
+
<img src="${logoData}" alt="Logo">
|
|
1632
|
+
<p>Thanks for joining.</p>
|
|
1633
|
+
`
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
// Cleanup
|
|
1637
|
+
master.request.deleteFileBuffer(file.filepath);
|
|
1638
|
+
|
|
1639
|
+
this.json({ success: true });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
```
|
|
1643
|
+
|
|
1644
|
+
---
|
|
1645
|
+
|
|
1646
|
+
### Security Best Practices
|
|
1647
|
+
|
|
1648
|
+
1. **Always set size limits:**
|
|
1649
|
+
```javascript
|
|
1650
|
+
const base64 = master.tools.fileToBase64(file, {
|
|
1651
|
+
maxSize: 5 * 1024 * 1024 // Prevent DoS
|
|
1652
|
+
});
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
2. **Validate file types before conversion:**
|
|
1656
|
+
```javascript
|
|
1657
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
1658
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
1659
|
+
throw new Error('Invalid file type');
|
|
1660
|
+
}
|
|
1661
|
+
const base64 = master.tools.fileToBase64(file);
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
3. **Delete temporary files after processing:**
|
|
1665
|
+
```javascript
|
|
1666
|
+
try {
|
|
1667
|
+
const base64 = master.tools.fileToBase64(file);
|
|
1668
|
+
// ... process ...
|
|
1669
|
+
} finally {
|
|
1670
|
+
master.request.deleteFileBuffer(file.filepath);
|
|
1671
|
+
}
|
|
1672
|
+
```
|
|
1673
|
+
|
|
1674
|
+
4. **Use streaming for large files:**
|
|
1675
|
+
```javascript
|
|
1676
|
+
// ❌ Bad: Loads entire 500MB file into memory
|
|
1677
|
+
const base64 = master.tools.fileToBase64(largeFile);
|
|
1678
|
+
|
|
1679
|
+
// ✅ Good: Streams in chunks
|
|
1680
|
+
const base64 = await master.tools.streamFileToBase64(largeFile, {
|
|
1681
|
+
chunkSize: 128 * 1024
|
|
1682
|
+
});
|
|
1683
|
+
```
|
|
1684
|
+
|
|
1685
|
+
5. **Validate base64 before decoding:**
|
|
1686
|
+
```javascript
|
|
1687
|
+
try {
|
|
1688
|
+
const buffer = master.tools.base64ToBytes(untrustedBase64);
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
console.error('Invalid base64 data');
|
|
1691
|
+
}
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
---
|
|
1695
|
+
|
|
1696
|
+
### Deprecated Methods
|
|
1697
|
+
|
|
1698
|
+
#### `master.tools.base64()` - DEPRECATED
|
|
1699
|
+
|
|
1700
|
+
**⚠️ WARNING:** The original `base64()` method is **BROKEN for binary files** and should not be used. It uses `charCodeAt()` which only works correctly for text files (UTF-8). Binary files like images, PDFs, and videos will be corrupted.
|
|
1701
|
+
|
|
1702
|
+
**Do NOT use:**
|
|
1703
|
+
```javascript
|
|
1704
|
+
// ❌ BROKEN - Corrupts binary files
|
|
1705
|
+
const broken = master.tools.base64(file.filepath);
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
**Use instead:**
|
|
1709
|
+
```javascript
|
|
1710
|
+
// ✅ CORRECT - Binary-safe
|
|
1711
|
+
const correct = master.tools.fileToBase64(file);
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
The old method is kept for backward compatibility with text-only use cases, but all new code should use the production-grade methods documented above.
|
|
1715
|
+
|
|
1716
|
+
---
|
|
1717
|
+
|
|
1718
|
+
## Components
|
|
1719
|
+
|
|
1720
|
+
Components are self-contained modules with their own routes, controllers, and views.
|
|
1721
|
+
|
|
1722
|
+
### Structure
|
|
1723
|
+
|
|
1724
|
+
```
|
|
1725
|
+
components/
|
|
1726
|
+
user/
|
|
1727
|
+
config/
|
|
1728
|
+
initializers/
|
|
1729
|
+
config.js
|
|
1730
|
+
routes.js
|
|
1731
|
+
app/
|
|
1732
|
+
controllers/
|
|
1733
|
+
authController.js
|
|
1734
|
+
views/
|
|
1735
|
+
auth/
|
|
1736
|
+
login.html
|
|
1737
|
+
models/
|
|
1738
|
+
userContext.js
|
|
1739
|
+
```
|
|
1740
|
+
|
|
1741
|
+
### Register Component
|
|
1742
|
+
|
|
1743
|
+
```javascript
|
|
1744
|
+
// In config/initializers/config.js
|
|
1745
|
+
master.component('components', 'user');
|
|
1746
|
+
master.component('components', 'mail');
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
### Absolute Path Components
|
|
1750
|
+
|
|
1751
|
+
```javascript
|
|
1752
|
+
// Load component from absolute path
|
|
1753
|
+
master.component('/var/www/shared-components', 'analytics');
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
Components are isolated and can be reused across projects.
|
|
1757
|
+
|
|
1758
|
+
---
|
|
1759
|
+
|
|
1760
|
+
## Timeout System
|
|
1761
|
+
|
|
1762
|
+
MasterController includes a production-ready timeout system with per-request tracking (Rails/Django style).
|
|
1763
|
+
|
|
1764
|
+
### Quick Start
|
|
1765
|
+
|
|
1766
|
+
```javascript
|
|
1767
|
+
// config/initializers/config.js
|
|
1768
|
+
const master = require('mastercontroller');
|
|
1769
|
+
|
|
1770
|
+
// Initialize timeout system
|
|
1771
|
+
master.timeout.init({
|
|
1772
|
+
globalTimeout: 120000, // 120 seconds (2 minutes) default
|
|
1773
|
+
enabled: true,
|
|
1774
|
+
onTimeout: (ctx, timeoutInfo) => {
|
|
1775
|
+
// Optional custom timeout handler
|
|
1776
|
+
console.log(`Request timeout: ${timeoutInfo.path}`);
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
// Register timeout middleware
|
|
1781
|
+
master.pipeline.use(master.timeout.middleware());
|
|
1782
|
+
```
|
|
1783
|
+
|
|
1784
|
+
### Route-Specific Timeouts
|
|
1785
|
+
|
|
1786
|
+
Configure different timeouts for different routes:
|
|
1787
|
+
|
|
1788
|
+
```javascript
|
|
1789
|
+
// Short timeout for API endpoints (30 seconds)
|
|
1790
|
+
master.timeout.setRouteTimeout('/api/*', 30000);
|
|
1791
|
+
|
|
1792
|
+
// Long timeout for reports (5 minutes)
|
|
1793
|
+
master.timeout.setRouteTimeout('/admin/reports', 300000);
|
|
1794
|
+
|
|
1795
|
+
// Very long timeout for batch operations (10 minutes)
|
|
1796
|
+
master.timeout.setRouteTimeout('/batch/process', 600000);
|
|
1797
|
+
|
|
1798
|
+
// Critical operations (1 minute)
|
|
1799
|
+
master.timeout.setRouteTimeout('/checkout/*', 60000);
|
|
1800
|
+
```
|
|
1801
|
+
|
|
1802
|
+
### Environment-Specific Configuration
|
|
1803
|
+
|
|
1804
|
+
**config/environments/env.development.json:**
|
|
1805
|
+
```json
|
|
1806
|
+
{
|
|
1807
|
+
"server": {
|
|
1808
|
+
"requestTimeout": 300000
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
```
|
|
1812
|
+
|
|
1813
|
+
**config/environments/env.production.json:**
|
|
1814
|
+
```json
|
|
1815
|
+
{
|
|
1816
|
+
"server": {
|
|
1817
|
+
"requestTimeout": 120000
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1822
|
+
### Timeout Response
|
|
1823
|
+
|
|
1824
|
+
When a request times out, the client receives:
|
|
1825
|
+
|
|
1826
|
+
```json
|
|
1827
|
+
{
|
|
1828
|
+
"error": "Request Timeout",
|
|
1829
|
+
"message": "The server did not receive a complete request within the allowed time",
|
|
1830
|
+
"code": "MC_REQUEST_TIMEOUT",
|
|
1831
|
+
"timeout": 120000
|
|
1832
|
+
}
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
### Monitoring Active Requests
|
|
1836
|
+
|
|
1837
|
+
```javascript
|
|
1838
|
+
const stats = master.timeout.getStats();
|
|
1839
|
+
|
|
1840
|
+
console.log(stats);
|
|
1841
|
+
// {
|
|
1842
|
+
// enabled: true,
|
|
1843
|
+
// globalTimeout: 120000,
|
|
1844
|
+
// routeTimeouts: [
|
|
1845
|
+
// { pattern: '/api/*', timeout: 30000 },
|
|
1846
|
+
// { pattern: '/admin/reports', timeout: 300000 }
|
|
1847
|
+
// ],
|
|
1848
|
+
// activeRequests: 5,
|
|
1849
|
+
// requests: [
|
|
1850
|
+
// {
|
|
1851
|
+
// requestId: 'req_1234567890_abc123',
|
|
1852
|
+
// path: 'api/users',
|
|
1853
|
+
// method: 'get',
|
|
1854
|
+
// timeout: 30000,
|
|
1855
|
+
// elapsed: 15000,
|
|
1856
|
+
// remaining: 15000
|
|
1857
|
+
// }
|
|
1858
|
+
// ]
|
|
1859
|
+
// }
|
|
1860
|
+
|
|
1861
|
+
// Check for slow requests
|
|
1862
|
+
stats.requests.forEach(req => {
|
|
1863
|
+
if (req.elapsed > req.timeout * 0.8) {
|
|
1864
|
+
console.warn(`Request close to timeout: ${req.path} (${req.elapsed}ms/${req.timeout}ms)`);
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
```
|
|
1868
|
+
|
|
1869
|
+
### Disable/Enable Timeouts
|
|
1870
|
+
|
|
1871
|
+
```javascript
|
|
1872
|
+
// Disable for debugging
|
|
1873
|
+
master.timeout.disable();
|
|
1874
|
+
|
|
1875
|
+
// Re-enable
|
|
1876
|
+
master.timeout.enable();
|
|
1877
|
+
|
|
1878
|
+
// Check status
|
|
1879
|
+
console.log(master.timeout.getStats().enabled); // true/false
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
### Complete Setup Example
|
|
1883
|
+
|
|
1884
|
+
```javascript
|
|
1885
|
+
// config/initializers/config.js
|
|
1886
|
+
const master = require('mastercontroller');
|
|
1887
|
+
|
|
1888
|
+
// Initialize timeout system
|
|
1889
|
+
master.timeout.init({
|
|
1890
|
+
globalTimeout: master.env.server.requestTimeout || 120000,
|
|
1891
|
+
enabled: true,
|
|
1892
|
+
onTimeout: (ctx, timeoutInfo) => {
|
|
1893
|
+
// Log timeout
|
|
1894
|
+
console.error(`Request timeout: ${timeoutInfo.path} (${timeoutInfo.duration}ms)`);
|
|
1895
|
+
|
|
1896
|
+
// Send to monitoring service
|
|
1897
|
+
sendToMonitoring('timeout', timeoutInfo);
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
// Configure route-specific timeouts
|
|
1902
|
+
master.timeout.setRouteTimeout('/api/*', 30000); // API: 30s
|
|
1903
|
+
master.timeout.setRouteTimeout('/admin/reports', 300000); // Reports: 5m
|
|
1904
|
+
master.timeout.setRouteTimeout('/batch/*', 600000); // Batch: 10m
|
|
1905
|
+
|
|
1906
|
+
// Register middleware
|
|
1907
|
+
master.pipeline.use(master.timeout.middleware());
|
|
1908
|
+
|
|
1909
|
+
// Monitor timeouts periodically
|
|
1910
|
+
setInterval(() => {
|
|
1911
|
+
const stats = master.timeout.getStats();
|
|
1912
|
+
|
|
1913
|
+
if (stats.activeRequests > 100) {
|
|
1914
|
+
console.warn(`High number of active requests: ${stats.activeRequests}`);
|
|
1915
|
+
}
|
|
1916
|
+
}, 60000); // Every minute
|
|
1917
|
+
```
|
|
1918
|
+
|
|
1919
|
+
### Best Practices
|
|
1920
|
+
|
|
1921
|
+
1. **Set appropriate global timeout**: 120 seconds (2 minutes) is a good default
|
|
1922
|
+
2. **Use route-specific timeouts**: APIs should have shorter timeouts (30s)
|
|
1923
|
+
3. **Long operations**: Use background jobs instead of long timeouts
|
|
1924
|
+
4. **Disable in development**: For debugging, temporarily disable timeouts
|
|
1925
|
+
5. **Monitor statistics**: Regularly check active requests and slow requests
|
|
1926
|
+
|
|
1927
|
+
---
|
|
1928
|
+
|
|
1929
|
+
## Error Handling
|
|
1930
|
+
|
|
1931
|
+
MasterController includes a professional error template system inspired by Rails and Django.
|
|
1932
|
+
|
|
1933
|
+
### Quick Start
|
|
1934
|
+
|
|
1935
|
+
```javascript
|
|
1936
|
+
// config/initializers/config.js
|
|
1937
|
+
const master = require('mastercontroller');
|
|
1938
|
+
|
|
1939
|
+
// Initialize error renderer
|
|
1940
|
+
master.errorRenderer.init({
|
|
1941
|
+
templateDir: 'public/errors', // Error templates directory
|
|
1942
|
+
environment: master.environmentType,
|
|
1943
|
+
showStackTrace: master.environmentType === 'development' // Dev only
|
|
1944
|
+
});
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
### Using Error Renderer
|
|
1948
|
+
|
|
1949
|
+
**In Middleware:**
|
|
1950
|
+
```javascript
|
|
1951
|
+
master.pipeline.use(async (ctx, next) => {
|
|
1952
|
+
if (!isAuthenticated(ctx)) {
|
|
1953
|
+
master.errorRenderer.send(ctx, 401, {
|
|
1954
|
+
message: 'Please log in to access this resource',
|
|
1955
|
+
suggestions: [
|
|
1956
|
+
'Sign in with your credentials',
|
|
1957
|
+
'Request a password reset if forgotten',
|
|
1958
|
+
'Contact support for account issues'
|
|
1959
|
+
]
|
|
1960
|
+
});
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
await next();
|
|
1964
|
+
});
|
|
1965
|
+
```
|
|
1966
|
+
|
|
1967
|
+
**In Controllers:**
|
|
1968
|
+
```javascript
|
|
1969
|
+
class UsersController {
|
|
1970
|
+
async show(obj) {
|
|
1971
|
+
const userId = obj.params.userId;
|
|
1972
|
+
const user = await this.db.query('SELECT * FROM users WHERE id = ?', [userId]);
|
|
1973
|
+
|
|
1974
|
+
if (!user) {
|
|
1975
|
+
master.errorRenderer.send(obj, 404, {
|
|
1976
|
+
message: `User #${userId} not found`,
|
|
1977
|
+
suggestions: [
|
|
1978
|
+
'Check the user ID',
|
|
1979
|
+
'Browse all users',
|
|
1980
|
+
'Search for the user by name'
|
|
1981
|
+
]
|
|
1982
|
+
});
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
this.render('show', { user });
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
async update(obj) {
|
|
1990
|
+
try {
|
|
1991
|
+
const userId = obj.params.id;
|
|
1992
|
+
const updates = obj.params.formData;
|
|
1993
|
+
|
|
1994
|
+
await this.db.query('UPDATE users SET ? WHERE id = ?', [updates, userId]);
|
|
1995
|
+
this.redirect(`/users/${userId}`);
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
console.error('Update failed:', error);
|
|
1998
|
+
|
|
1999
|
+
master.errorRenderer.send(obj, 500, {
|
|
2000
|
+
message: 'Failed to update user',
|
|
2001
|
+
code: 'DB_ERROR',
|
|
2002
|
+
stack: error.stack
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
```
|
|
2008
|
+
|
|
2009
|
+
### Error Templates
|
|
2010
|
+
|
|
2011
|
+
Create templates in `public/errors/`:
|
|
2012
|
+
|
|
2013
|
+
```
|
|
2014
|
+
public/errors/
|
|
2015
|
+
├── 400.html # Bad Request
|
|
2016
|
+
├── 401.html # Unauthorized
|
|
2017
|
+
├── 403.html # Forbidden
|
|
2018
|
+
├── 404.html # Not Found
|
|
2019
|
+
├── 405.html # Method Not Allowed
|
|
2020
|
+
├── 422.html # Unprocessable Entity
|
|
2021
|
+
├── 429.html # Too Many Requests
|
|
2022
|
+
├── 500.html # Internal Server Error
|
|
2023
|
+
├── 502.html # Bad Gateway
|
|
2024
|
+
├── 503.html # Service Unavailable
|
|
2025
|
+
└── 504.html # Gateway Timeout
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
**Template Variables:**
|
|
2029
|
+
|
|
2030
|
+
```html
|
|
2031
|
+
<!DOCTYPE html>
|
|
2032
|
+
<html>
|
|
2033
|
+
<head>
|
|
2034
|
+
<title>{{title}} ({{statusCode}})</title>
|
|
2035
|
+
</head>
|
|
2036
|
+
<body>
|
|
2037
|
+
<h1>{{statusCode}} - {{title}}</h1>
|
|
2038
|
+
<p>{{message}}</p>
|
|
2039
|
+
|
|
2040
|
+
<!-- Conditionals (dev only) -->
|
|
2041
|
+
{{#if showStackTrace}}
|
|
2042
|
+
<pre>{{stack}}</pre>
|
|
2043
|
+
{{/if}}
|
|
2044
|
+
|
|
2045
|
+
<!-- Loops -->
|
|
2046
|
+
{{#each suggestions}}
|
|
2047
|
+
<li>{{this}}</li>
|
|
2048
|
+
{{/each}}
|
|
2049
|
+
</body>
|
|
2050
|
+
</html>
|
|
1340
2051
|
```
|
|
1341
2052
|
|
|
1342
2053
|
**Available Variables:**
|
|
@@ -1348,125 +2059,1143 @@ public/errors/
|
|
|
1348
2059
|
- `{{suggestions}}` - Array of suggestions
|
|
1349
2060
|
- `{{environment}}` - Current environment
|
|
1350
2061
|
|
|
1351
|
-
### Custom Error Handlers
|
|
2062
|
+
### Custom Error Handlers
|
|
2063
|
+
|
|
2064
|
+
Register custom error handlers for specific status codes:
|
|
2065
|
+
|
|
2066
|
+
```javascript
|
|
2067
|
+
// Custom 404 handler
|
|
2068
|
+
master.errorRenderer.registerHandler(404, (ctx, errorData) => {
|
|
2069
|
+
return `
|
|
2070
|
+
<!DOCTYPE html>
|
|
2071
|
+
<html>
|
|
2072
|
+
<head>
|
|
2073
|
+
<title>Page Not Found</title>
|
|
2074
|
+
<style>
|
|
2075
|
+
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
|
2076
|
+
.icon { font-size: 100px; }
|
|
2077
|
+
h1 { color: #333; }
|
|
2078
|
+
</style>
|
|
2079
|
+
</head>
|
|
2080
|
+
<body>
|
|
2081
|
+
<div class="icon">🔍</div>
|
|
2082
|
+
<h1>Page Not Found</h1>
|
|
2083
|
+
<p>${errorData.message}</p>
|
|
2084
|
+
<a href="/">Go Home</a>
|
|
2085
|
+
</body>
|
|
2086
|
+
</html>
|
|
2087
|
+
`;
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
// Custom 500 handler with logging
|
|
2091
|
+
master.errorRenderer.registerHandler(500, (ctx, errorData) => {
|
|
2092
|
+
// Log to external service
|
|
2093
|
+
logToSentry(errorData);
|
|
2094
|
+
|
|
2095
|
+
return `
|
|
2096
|
+
<!DOCTYPE html>
|
|
2097
|
+
<html>
|
|
2098
|
+
<body>
|
|
2099
|
+
<h1>Oops! Something went wrong</h1>
|
|
2100
|
+
<p>Our team has been notified.</p>
|
|
2101
|
+
<p>Reference: ${errorData.code}</p>
|
|
2102
|
+
</body>
|
|
2103
|
+
</html>
|
|
2104
|
+
`;
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
// Custom 503 handler (maintenance mode)
|
|
2108
|
+
master.errorRenderer.registerHandler(503, (ctx, errorData) => {
|
|
2109
|
+
return `
|
|
2110
|
+
<!DOCTYPE html>
|
|
2111
|
+
<html>
|
|
2112
|
+
<head>
|
|
2113
|
+
<title>Maintenance Mode</title>
|
|
2114
|
+
<style>
|
|
2115
|
+
body {
|
|
2116
|
+
font-family: Arial, sans-serif;
|
|
2117
|
+
text-align: center;
|
|
2118
|
+
padding: 50px;
|
|
2119
|
+
}
|
|
2120
|
+
.icon { font-size: 100px; }
|
|
2121
|
+
h1 { color: #333; }
|
|
2122
|
+
</style>
|
|
2123
|
+
</head>
|
|
2124
|
+
<body>
|
|
2125
|
+
<div class="icon">🔧</div>
|
|
2126
|
+
<h1>We'll be back soon!</h1>
|
|
2127
|
+
<p>We're performing scheduled maintenance.</p>
|
|
2128
|
+
<p>Expected completion: 2:00 PM EST</p>
|
|
2129
|
+
</body>
|
|
2130
|
+
</html>
|
|
2131
|
+
`;
|
|
2132
|
+
});
|
|
2133
|
+
```
|
|
2134
|
+
|
|
2135
|
+
### Content Negotiation
|
|
2136
|
+
|
|
2137
|
+
The error renderer automatically detects API requests and returns JSON:
|
|
2138
|
+
|
|
2139
|
+
```javascript
|
|
2140
|
+
// Browser request → HTML
|
|
2141
|
+
GET /users/999
|
|
2142
|
+
Accept: text/html
|
|
2143
|
+
→ Returns beautiful HTML error page
|
|
2144
|
+
|
|
2145
|
+
// API request → JSON
|
|
2146
|
+
GET /api/users/999
|
|
2147
|
+
Accept: application/json
|
|
2148
|
+
→ Returns JSON error response
|
|
2149
|
+
{
|
|
2150
|
+
"error": "Page Not Found",
|
|
2151
|
+
"statusCode": 404,
|
|
2152
|
+
"code": "MC_HTTP_ERROR",
|
|
2153
|
+
"message": "The user you're looking for doesn't exist."
|
|
2154
|
+
}
|
|
2155
|
+
```
|
|
2156
|
+
|
|
2157
|
+
### Global Error Handler (Pipeline)
|
|
2158
|
+
|
|
2159
|
+
```javascript
|
|
2160
|
+
master.pipeline.useError(async (error, ctx, next) => {
|
|
2161
|
+
console.error('Pipeline error:', error);
|
|
2162
|
+
|
|
2163
|
+
// Use error renderer for HTTP errors
|
|
2164
|
+
master.errorRenderer.send(ctx, 500, {
|
|
2165
|
+
message: error.message,
|
|
2166
|
+
code: error.code,
|
|
2167
|
+
stack: error.stack
|
|
2168
|
+
});
|
|
2169
|
+
});
|
|
2170
|
+
```
|
|
2171
|
+
|
|
2172
|
+
### Controller Error Handling
|
|
2173
|
+
|
|
2174
|
+
```javascript
|
|
2175
|
+
class UsersController {
|
|
2176
|
+
async index(obj) {
|
|
2177
|
+
try {
|
|
2178
|
+
const users = await this.db.query('SELECT * FROM users');
|
|
2179
|
+
this.render('index', { users });
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
console.error('Database error:', error);
|
|
2182
|
+
|
|
2183
|
+
master.errorRenderer.send(obj, 500, {
|
|
2184
|
+
message: 'Failed to load users',
|
|
2185
|
+
code: 'DB_ERROR',
|
|
2186
|
+
stack: error.stack
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
```
|
|
2192
|
+
|
|
2193
|
+
### Logging
|
|
2194
|
+
|
|
2195
|
+
```javascript
|
|
2196
|
+
const { logger } = require('./error/MasterErrorLogger');
|
|
2197
|
+
|
|
2198
|
+
// In controllers or middleware
|
|
2199
|
+
logger.info({
|
|
2200
|
+
code: 'USER_LOGIN',
|
|
2201
|
+
message: 'User logged in',
|
|
2202
|
+
userId: user.id
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
logger.warn({
|
|
2206
|
+
code: 'INVALID_INPUT',
|
|
2207
|
+
message: 'Invalid email format',
|
|
2208
|
+
email: input
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
logger.error({
|
|
2212
|
+
code: 'DB_ERROR',
|
|
2213
|
+
message: 'Database query failed',
|
|
2214
|
+
error: error.message,
|
|
2215
|
+
stack: error.stack
|
|
2216
|
+
});
|
|
2217
|
+
```
|
|
2218
|
+
|
|
2219
|
+
### Common Use Cases
|
|
2220
|
+
|
|
2221
|
+
**Rate Limiting with Custom 429 Page:**
|
|
2222
|
+
```javascript
|
|
2223
|
+
const rateLimit = new Map();
|
|
2224
|
+
|
|
2225
|
+
master.pipeline.map('/api/*', (api) => {
|
|
2226
|
+
api.use(async (ctx, next) => {
|
|
2227
|
+
const clientId = ctx.request.connection.remoteAddress;
|
|
2228
|
+
const requests = rateLimit.get(clientId) || [];
|
|
2229
|
+
const now = Date.now();
|
|
2230
|
+
|
|
2231
|
+
// Remove requests older than 1 minute
|
|
2232
|
+
const recent = requests.filter(time => now - time < 60000);
|
|
2233
|
+
|
|
2234
|
+
if (recent.length >= 100) {
|
|
2235
|
+
master.errorRenderer.send(ctx, 429, {
|
|
2236
|
+
message: 'Rate limit exceeded (100 requests per minute)',
|
|
2237
|
+
suggestions: [
|
|
2238
|
+
'Wait 60 seconds and try again',
|
|
2239
|
+
'Upgrade to a higher tier plan',
|
|
2240
|
+
'Contact support for increased limits'
|
|
2241
|
+
]
|
|
2242
|
+
});
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
recent.push(now);
|
|
2247
|
+
rateLimit.set(clientId, recent);
|
|
2248
|
+
await next();
|
|
2249
|
+
});
|
|
2250
|
+
});
|
|
2251
|
+
```
|
|
2252
|
+
|
|
2253
|
+
**Protected Admin Section:**
|
|
2254
|
+
```javascript
|
|
2255
|
+
master.pipeline.map('/admin/*', (admin) => {
|
|
2256
|
+
admin.use(async (ctx, next) => {
|
|
2257
|
+
if (!ctx.state.user || !ctx.state.user.isAdmin) {
|
|
2258
|
+
master.errorRenderer.send(ctx, 403, {
|
|
2259
|
+
message: 'Admin access required',
|
|
2260
|
+
suggestions: [
|
|
2261
|
+
'Sign in with an admin account',
|
|
2262
|
+
'Contact an administrator for access'
|
|
2263
|
+
]
|
|
2264
|
+
});
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
await next();
|
|
2268
|
+
});
|
|
2269
|
+
});
|
|
2270
|
+
```
|
|
2271
|
+
|
|
2272
|
+
**Maintenance Mode:**
|
|
2273
|
+
```javascript
|
|
2274
|
+
const maintenanceMode = process.env.MAINTENANCE === 'true';
|
|
2275
|
+
|
|
2276
|
+
if (maintenanceMode) {
|
|
2277
|
+
master.pipeline.use(async (ctx, next) => {
|
|
2278
|
+
master.errorRenderer.send(ctx, 503, {
|
|
2279
|
+
message: 'Service temporarily unavailable'
|
|
2280
|
+
});
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
```
|
|
2284
|
+
|
|
2285
|
+
### Complete Setup Example
|
|
2286
|
+
|
|
2287
|
+
```javascript
|
|
2288
|
+
// config/initializers/config.js
|
|
2289
|
+
const master = require('mastercontroller');
|
|
2290
|
+
|
|
2291
|
+
// Initialize error renderer
|
|
2292
|
+
master.errorRenderer.init({
|
|
2293
|
+
templateDir: 'public/errors',
|
|
2294
|
+
environment: master.environmentType,
|
|
2295
|
+
showStackTrace: master.environmentType === 'development'
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
// Register custom handlers
|
|
2299
|
+
master.errorRenderer.registerHandler(404, (ctx, errorData) => {
|
|
2300
|
+
return `
|
|
2301
|
+
<!DOCTYPE html>
|
|
2302
|
+
<html>
|
|
2303
|
+
<body>
|
|
2304
|
+
<h1>404 - Page Not Found</h1>
|
|
2305
|
+
<p>${errorData.message}</p>
|
|
2306
|
+
<a href="/">Go Home</a>
|
|
2307
|
+
</body>
|
|
2308
|
+
</html>
|
|
2309
|
+
`;
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
// Global error handler
|
|
2313
|
+
master.pipeline.useError(async (error, ctx, next) => {
|
|
2314
|
+
console.error('Pipeline error:', error);
|
|
2315
|
+
|
|
2316
|
+
master.errorRenderer.send(ctx, 500, {
|
|
2317
|
+
message: error.message,
|
|
2318
|
+
code: error.code,
|
|
2319
|
+
stack: error.stack
|
|
2320
|
+
});
|
|
2321
|
+
});
|
|
2322
|
+
```
|
|
2323
|
+
|
|
2324
|
+
### Best Practices
|
|
2325
|
+
|
|
2326
|
+
1. **Keep error messages user-friendly**: Don't expose technical details in production
|
|
2327
|
+
2. **Show stack traces in development only**: Use `showStackTrace` conditional
|
|
2328
|
+
3. **Provide actionable suggestions**: Help users resolve the issue
|
|
2329
|
+
4. **Consistent design**: Match your application's design
|
|
2330
|
+
5. **Test all error codes**: Ensure templates render correctly
|
|
2331
|
+
6. **Log errors**: Use `logger` for error tracking
|
|
2332
|
+
7. **Monitor errors**: Track error rates and patterns
|
|
2333
|
+
|
|
2334
|
+
---
|
|
2335
|
+
|
|
2336
|
+
## HTTPS Setup
|
|
2337
|
+
|
|
2338
|
+
MasterController v1.3.2 includes **production-grade HTTPS/TLS security** with automatic secure defaults.
|
|
2339
|
+
|
|
2340
|
+
### 🔒 Security Features (Automatic)
|
|
2341
|
+
|
|
2342
|
+
When you setup HTTPS, MasterController automatically configures:
|
|
2343
|
+
- ✅ **TLS 1.3** by default (2026 security standard)
|
|
2344
|
+
- ✅ **Secure cipher suites** (Mozilla Intermediate configuration)
|
|
2345
|
+
- ✅ **Path traversal protection** for static files
|
|
2346
|
+
- ✅ **Open redirect protection** for HTTP→HTTPS redirects
|
|
2347
|
+
- ✅ **SNI support** for multiple domains
|
|
2348
|
+
- ✅ **Certificate live reload** (zero-downtime updates)
|
|
2349
|
+
- ✅ **HSTS support** with preload option
|
|
2350
|
+
|
|
2351
|
+
---
|
|
2352
|
+
|
|
2353
|
+
## Quick Start: HTTPS in 5 Minutes
|
|
2354
|
+
|
|
2355
|
+
### Development (Self-Signed Certificate)
|
|
2356
|
+
|
|
2357
|
+
**Step 1: Generate Self-Signed Certificate**
|
|
2358
|
+
```bash
|
|
2359
|
+
# Create certificates directory
|
|
2360
|
+
mkdir -p certs
|
|
2361
|
+
cd certs
|
|
2362
|
+
|
|
2363
|
+
# Generate self-signed certificate (valid for 365 days)
|
|
2364
|
+
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
|
|
2365
|
+
-days 365 -nodes -subj "/CN=localhost"
|
|
2366
|
+
|
|
2367
|
+
# Combine for convenience
|
|
2368
|
+
cat key.pem > localhost.pem
|
|
2369
|
+
cat cert.pem >> localhost.pem
|
|
2370
|
+
|
|
2371
|
+
cd ..
|
|
2372
|
+
```
|
|
2373
|
+
|
|
2374
|
+
**Step 2: Update `server.js`**
|
|
2375
|
+
```javascript
|
|
2376
|
+
const fs = require('fs');
|
|
2377
|
+
const master = require('mastercontroller');
|
|
2378
|
+
|
|
2379
|
+
master.environmentType = process.env.NODE_ENV || 'development';
|
|
2380
|
+
master.root = __dirname;
|
|
2381
|
+
|
|
2382
|
+
// Setup HTTPS for development
|
|
2383
|
+
const server = master.setupServer('https', {
|
|
2384
|
+
key: fs.readFileSync('./certs/key.pem'),
|
|
2385
|
+
cert: fs.readFileSync('./certs/cert.pem')
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
require('./config/initializers/config');
|
|
2389
|
+
|
|
2390
|
+
master.start(server);
|
|
2391
|
+
master.serverSettings({ httpPort: 3000 }); // Use 3000 for development
|
|
2392
|
+
|
|
2393
|
+
console.log('✅ HTTPS server running on https://localhost:3000');
|
|
2394
|
+
console.log('⚠️ Self-signed certificate - browser will show warning (this is normal)');
|
|
2395
|
+
```
|
|
2396
|
+
|
|
2397
|
+
**Step 3: Visit `https://localhost:3000`**
|
|
2398
|
+
- Browser will show "Not Secure" warning
|
|
2399
|
+
- Click "Advanced" → "Proceed to localhost" (safe for development)
|
|
2400
|
+
|
|
2401
|
+
---
|
|
2402
|
+
|
|
2403
|
+
### Production (Let's Encrypt - FREE)
|
|
2404
|
+
|
|
2405
|
+
**Step 1: Install Certbot**
|
|
2406
|
+
```bash
|
|
2407
|
+
# Ubuntu/Debian
|
|
2408
|
+
sudo apt update
|
|
2409
|
+
sudo apt install certbot
|
|
2410
|
+
|
|
2411
|
+
# CentOS/RHEL
|
|
2412
|
+
sudo yum install certbot
|
|
2413
|
+
|
|
2414
|
+
# macOS
|
|
2415
|
+
brew install certbot
|
|
2416
|
+
```
|
|
2417
|
+
|
|
2418
|
+
**Step 2: Get FREE SSL Certificate**
|
|
2419
|
+
```bash
|
|
2420
|
+
# Stop any web server on port 80
|
|
2421
|
+
sudo systemctl stop nginx
|
|
2422
|
+
|
|
2423
|
+
# Get certificate (replace with your domain)
|
|
2424
|
+
sudo certbot certonly --standalone -d yourapp.com -d www.yourapp.com
|
|
2425
|
+
|
|
2426
|
+
# Certificates will be saved to:
|
|
2427
|
+
# /etc/letsencrypt/live/yourapp.com/privkey.pem
|
|
2428
|
+
# /etc/letsencrypt/live/yourapp.com/fullchain.pem
|
|
2429
|
+
```
|
|
2430
|
+
|
|
2431
|
+
**Step 3: Update `server.js` for Production**
|
|
2432
|
+
```javascript
|
|
2433
|
+
const fs = require('fs');
|
|
2434
|
+
const master = require('mastercontroller');
|
|
2435
|
+
|
|
2436
|
+
master.environmentType = process.env.NODE_ENV || 'production';
|
|
2437
|
+
master.root = __dirname;
|
|
2438
|
+
|
|
2439
|
+
// Setup HTTPS with Let's Encrypt certificates
|
|
2440
|
+
const server = master.setupServer('https', {
|
|
2441
|
+
key: fs.readFileSync('/etc/letsencrypt/live/yourapp.com/privkey.pem'),
|
|
2442
|
+
cert: fs.readFileSync('/etc/letsencrypt/live/yourapp.com/fullchain.pem')
|
|
2443
|
+
});
|
|
2444
|
+
|
|
2445
|
+
// Enable HSTS (strongly recommended)
|
|
2446
|
+
master.enableHSTS({
|
|
2447
|
+
maxAge: 31536000, // 1 year
|
|
2448
|
+
includeSubDomains: true,
|
|
2449
|
+
preload: true
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
require('./config/initializers/config');
|
|
2453
|
+
|
|
2454
|
+
// Start HTTPS on port 443
|
|
2455
|
+
master.start(server);
|
|
2456
|
+
master.serverSettings({ httpPort: 443 });
|
|
2457
|
+
|
|
2458
|
+
// Redirect HTTP to HTTPS (port 80 → 443)
|
|
2459
|
+
const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
|
|
2460
|
+
'yourapp.com',
|
|
2461
|
+
'www.yourapp.com'
|
|
2462
|
+
]);
|
|
2463
|
+
|
|
2464
|
+
console.log('========================================');
|
|
2465
|
+
console.log('🚀 Production Server Started');
|
|
2466
|
+
console.log('========================================');
|
|
2467
|
+
console.log('✅ HTTPS: https://yourapp.com (port 443)');
|
|
2468
|
+
console.log('✅ HTTP redirect: http://yourapp.com → https://yourapp.com');
|
|
2469
|
+
console.log('✅ TLS 1.3 enabled');
|
|
2470
|
+
console.log('✅ HSTS enabled (1 year)');
|
|
2471
|
+
console.log('✅ Secure ciphers configured');
|
|
2472
|
+
console.log('========================================');
|
|
2473
|
+
```
|
|
2474
|
+
|
|
2475
|
+
**Step 4: Set Permissions (if needed)**
|
|
2476
|
+
```bash
|
|
2477
|
+
# Option 1: Allow Node.js to bind to ports 80/443 (Linux)
|
|
2478
|
+
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
|
2479
|
+
|
|
2480
|
+
# Option 2: Run with sudo (not recommended)
|
|
2481
|
+
sudo node server.js
|
|
2482
|
+
|
|
2483
|
+
# Option 3: Use reverse proxy (recommended - see below)
|
|
2484
|
+
```
|
|
2485
|
+
|
|
2486
|
+
**Step 5: Auto-Renew Certificates**
|
|
2487
|
+
```bash
|
|
2488
|
+
# Certbot automatically renews certificates
|
|
2489
|
+
# Test renewal process:
|
|
2490
|
+
sudo certbot renew --dry-run
|
|
2491
|
+
|
|
2492
|
+
# Add to crontab for auto-renewal (runs daily)
|
|
2493
|
+
sudo crontab -e
|
|
2494
|
+
# Add this line:
|
|
2495
|
+
0 0 * * * certbot renew --quiet --post-hook "systemctl restart myapp"
|
|
2496
|
+
```
|
|
2497
|
+
|
|
2498
|
+
---
|
|
2499
|
+
|
|
2500
|
+
### Production (Custom Certificate)
|
|
2501
|
+
|
|
2502
|
+
If you have a certificate from a commercial CA (GoDaddy, Namecheap, etc.):
|
|
2503
|
+
|
|
2504
|
+
```javascript
|
|
2505
|
+
const fs = require('fs');
|
|
2506
|
+
const master = require('mastercontroller');
|
|
2507
|
+
|
|
2508
|
+
master.environmentType = 'production';
|
|
2509
|
+
master.root = __dirname;
|
|
2510
|
+
|
|
2511
|
+
const server = master.setupServer('https', {
|
|
2512
|
+
key: fs.readFileSync('/path/to/your-domain.key'),
|
|
2513
|
+
cert: fs.readFileSync('/path/to/your-domain.crt'),
|
|
2514
|
+
ca: fs.readFileSync('/path/to/ca-bundle.crt') // Intermediate certificates
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
master.enableHSTS({
|
|
2518
|
+
maxAge: 31536000,
|
|
2519
|
+
includeSubDomains: true,
|
|
2520
|
+
preload: true
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
require('./config/initializers/config');
|
|
2524
|
+
master.start(server);
|
|
2525
|
+
master.serverSettings({ httpPort: 443 });
|
|
2526
|
+
|
|
2527
|
+
const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
|
|
2528
|
+
'yourapp.com',
|
|
2529
|
+
'www.yourapp.com'
|
|
2530
|
+
]);
|
|
2531
|
+
```
|
|
2532
|
+
|
|
2533
|
+
---
|
|
2534
|
+
|
|
2535
|
+
---
|
|
2536
|
+
|
|
2537
|
+
## Production Deployment Options
|
|
2538
|
+
|
|
2539
|
+
### Option 1: Direct HTTPS (Simple, Good for Small Apps)
|
|
2540
|
+
|
|
2541
|
+
Run MasterController directly on ports 80/443:
|
|
2542
|
+
|
|
2543
|
+
```javascript
|
|
2544
|
+
const fs = require('fs');
|
|
2545
|
+
const master = require('mastercontroller');
|
|
2546
|
+
|
|
2547
|
+
master.environmentType = 'production';
|
|
2548
|
+
master.root = __dirname;
|
|
2549
|
+
|
|
2550
|
+
const server = master.setupServer('https', {
|
|
2551
|
+
key: fs.readFileSync('/etc/letsencrypt/live/yourapp.com/privkey.pem'),
|
|
2552
|
+
cert: fs.readFileSync('/etc/letsencrypt/live/yourapp.com/fullchain.pem')
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
master.enableHSTS({
|
|
2556
|
+
maxAge: 31536000,
|
|
2557
|
+
includeSubDomains: true,
|
|
2558
|
+
preload: true
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
require('./config/initializers/config');
|
|
2562
|
+
master.start(server);
|
|
2563
|
+
master.serverSettings({ httpPort: 443 });
|
|
2564
|
+
|
|
2565
|
+
// HTTP redirect
|
|
2566
|
+
const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
|
|
2567
|
+
'yourapp.com',
|
|
2568
|
+
'www.yourapp.com'
|
|
2569
|
+
]);
|
|
2570
|
+
```
|
|
2571
|
+
|
|
2572
|
+
**Pros:**
|
|
2573
|
+
- ✅ Simple setup
|
|
2574
|
+
- ✅ No extra software needed
|
|
2575
|
+
- ✅ Full control over TLS
|
|
2576
|
+
|
|
2577
|
+
**Cons:**
|
|
2578
|
+
- ❌ Requires root/sudo for ports 80/443
|
|
2579
|
+
- ❌ No load balancing
|
|
2580
|
+
- ❌ No static file caching
|
|
2581
|
+
|
|
2582
|
+
---
|
|
2583
|
+
|
|
2584
|
+
### Option 2: Nginx Reverse Proxy (Recommended for Production)
|
|
2585
|
+
|
|
2586
|
+
Run MasterController on high port (3000) behind Nginx on ports 80/443:
|
|
2587
|
+
|
|
2588
|
+
**Step 1: Install Nginx**
|
|
2589
|
+
```bash
|
|
2590
|
+
# Ubuntu/Debian
|
|
2591
|
+
sudo apt update
|
|
2592
|
+
sudo apt install nginx
|
|
2593
|
+
|
|
2594
|
+
# CentOS/RHEL
|
|
2595
|
+
sudo yum install nginx
|
|
2596
|
+
|
|
2597
|
+
# macOS
|
|
2598
|
+
brew install nginx
|
|
2599
|
+
```
|
|
2600
|
+
|
|
2601
|
+
**Step 2: Configure MasterController (High Port)**
|
|
2602
|
+
```javascript
|
|
2603
|
+
// server.js - Run on port 3000
|
|
2604
|
+
const master = require('mastercontroller');
|
|
2605
|
+
|
|
2606
|
+
master.environmentType = 'production';
|
|
2607
|
+
master.root = __dirname;
|
|
2608
|
+
|
|
2609
|
+
// HTTP only (Nginx handles HTTPS)
|
|
2610
|
+
const server = master.setupServer('http');
|
|
2611
|
+
|
|
2612
|
+
require('./config/initializers/config');
|
|
2613
|
+
master.start(server);
|
|
2614
|
+
master.serverSettings({
|
|
2615
|
+
httpPort: 3000,
|
|
2616
|
+
hostname: '127.0.0.1' // Only accept local connections
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
console.log('✅ Server running on http://127.0.0.1:3000');
|
|
2620
|
+
console.log('⚠️ Behind Nginx reverse proxy');
|
|
2621
|
+
```
|
|
2622
|
+
|
|
2623
|
+
**Step 3: Configure Nginx**
|
|
2624
|
+
```nginx
|
|
2625
|
+
# /etc/nginx/sites-available/yourapp.com
|
|
2626
|
+
|
|
2627
|
+
# HTTP redirect to HTTPS
|
|
2628
|
+
server {
|
|
2629
|
+
listen 80;
|
|
2630
|
+
listen [::]:80;
|
|
2631
|
+
server_name yourapp.com www.yourapp.com;
|
|
2632
|
+
|
|
2633
|
+
return 301 https://$server_name$request_uri;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
# HTTPS server
|
|
2637
|
+
server {
|
|
2638
|
+
listen 443 ssl http2;
|
|
2639
|
+
listen [::]:443 ssl http2;
|
|
2640
|
+
server_name yourapp.com www.yourapp.com;
|
|
2641
|
+
|
|
2642
|
+
# SSL Configuration (Let's Encrypt)
|
|
2643
|
+
ssl_certificate /etc/letsencrypt/live/yourapp.com/fullchain.pem;
|
|
2644
|
+
ssl_certificate_key /etc/letsencrypt/live/yourapp.com/privkey.pem;
|
|
2645
|
+
ssl_trusted_certificate /etc/letsencrypt/live/yourapp.com/chain.pem;
|
|
2646
|
+
|
|
2647
|
+
# Modern TLS configuration (matches MasterController defaults)
|
|
2648
|
+
ssl_protocols TLSv1.3 TLSv1.2;
|
|
2649
|
+
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES256-GCM-SHA384';
|
|
2650
|
+
ssl_prefer_server_ciphers on;
|
|
2651
|
+
|
|
2652
|
+
# HSTS
|
|
2653
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|
2654
|
+
|
|
2655
|
+
# Security headers
|
|
2656
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
2657
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
2658
|
+
add_header X-XSS-Protection "1; mode=block" always;
|
|
2659
|
+
|
|
2660
|
+
# Proxy to MasterController
|
|
2661
|
+
location / {
|
|
2662
|
+
proxy_pass http://127.0.0.1:3000;
|
|
2663
|
+
proxy_http_version 1.1;
|
|
2664
|
+
|
|
2665
|
+
# WebSocket support
|
|
2666
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
2667
|
+
proxy_set_header Connection 'upgrade';
|
|
2668
|
+
|
|
2669
|
+
# Forward real client IP
|
|
2670
|
+
proxy_set_header Host $host;
|
|
2671
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
2672
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
2673
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
2674
|
+
|
|
2675
|
+
# Timeouts
|
|
2676
|
+
proxy_connect_timeout 60s;
|
|
2677
|
+
proxy_send_timeout 60s;
|
|
2678
|
+
proxy_read_timeout 60s;
|
|
2679
|
+
|
|
2680
|
+
proxy_cache_bypass $http_upgrade;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
# Static file caching (optional)
|
|
2684
|
+
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
|
2685
|
+
proxy_pass http://127.0.0.1:3000;
|
|
2686
|
+
expires 1y;
|
|
2687
|
+
add_header Cache-Control "public, immutable";
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
# Gzip compression
|
|
2691
|
+
gzip on;
|
|
2692
|
+
gzip_vary on;
|
|
2693
|
+
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;
|
|
2694
|
+
}
|
|
2695
|
+
```
|
|
2696
|
+
|
|
2697
|
+
**Step 4: Enable Nginx Configuration**
|
|
2698
|
+
```bash
|
|
2699
|
+
# Create symlink to enable site
|
|
2700
|
+
sudo ln -s /etc/nginx/sites-available/yourapp.com /etc/nginx/sites-enabled/
|
|
2701
|
+
|
|
2702
|
+
# Test configuration
|
|
2703
|
+
sudo nginx -t
|
|
2704
|
+
|
|
2705
|
+
# Reload Nginx
|
|
2706
|
+
sudo systemctl reload nginx
|
|
2707
|
+
|
|
2708
|
+
# Enable Nginx on boot
|
|
2709
|
+
sudo systemctl enable nginx
|
|
2710
|
+
```
|
|
2711
|
+
|
|
2712
|
+
**Step 5: Configure MasterController to Trust Proxy**
|
|
2713
|
+
```javascript
|
|
2714
|
+
// config/initializers/config.js
|
|
2715
|
+
const master = require('mastercontroller');
|
|
2716
|
+
|
|
2717
|
+
// Trust X-Forwarded-* headers from Nginx
|
|
2718
|
+
master.pipeline.use(async (ctx, next) => {
|
|
2719
|
+
// Get real client IP from X-Forwarded-For
|
|
2720
|
+
const forwardedFor = ctx.request.headers['x-forwarded-for'];
|
|
2721
|
+
if (forwardedFor) {
|
|
2722
|
+
ctx.request.clientIp = forwardedFor.split(',')[0].trim();
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// Trust X-Forwarded-Proto for HTTPS detection
|
|
2726
|
+
if (ctx.request.headers['x-forwarded-proto'] === 'https') {
|
|
2727
|
+
ctx.request.isHttps = true;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
await next();
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
// ... rest of config
|
|
2734
|
+
```
|
|
2735
|
+
|
|
2736
|
+
**Pros:**
|
|
2737
|
+
- ✅ No root/sudo needed for Node.js
|
|
2738
|
+
- ✅ Static file caching
|
|
2739
|
+
- ✅ Load balancing support
|
|
2740
|
+
- ✅ Better performance
|
|
2741
|
+
- ✅ Easier certificate management
|
|
2742
|
+
- ✅ Industry standard
|
|
2743
|
+
|
|
2744
|
+
**Cons:**
|
|
2745
|
+
- ❌ Extra complexity
|
|
2746
|
+
- ❌ Another service to maintain
|
|
2747
|
+
|
|
2748
|
+
---
|
|
2749
|
+
|
|
2750
|
+
### Option 3: PM2 with Nginx (Best for Production)
|
|
2751
|
+
|
|
2752
|
+
Combine PM2 process manager with Nginx:
|
|
2753
|
+
|
|
2754
|
+
**Step 1: Install PM2**
|
|
2755
|
+
```bash
|
|
2756
|
+
npm install -g pm2
|
|
2757
|
+
```
|
|
2758
|
+
|
|
2759
|
+
**Step 2: Create PM2 Ecosystem File**
|
|
2760
|
+
```javascript
|
|
2761
|
+
// ecosystem.config.js
|
|
2762
|
+
module.exports = {
|
|
2763
|
+
apps: [{
|
|
2764
|
+
name: 'myapp',
|
|
2765
|
+
script: './server.js',
|
|
2766
|
+
instances: 'max', // Use all CPU cores
|
|
2767
|
+
exec_mode: 'cluster',
|
|
2768
|
+
env: {
|
|
2769
|
+
NODE_ENV: 'production'
|
|
2770
|
+
},
|
|
2771
|
+
error_file: './logs/err.log',
|
|
2772
|
+
out_file: './logs/out.log',
|
|
2773
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
|
2774
|
+
merge_logs: true
|
|
2775
|
+
}]
|
|
2776
|
+
};
|
|
2777
|
+
```
|
|
2778
|
+
|
|
2779
|
+
**Step 3: Start with PM2**
|
|
2780
|
+
```bash
|
|
2781
|
+
# Start application
|
|
2782
|
+
pm2 start ecosystem.config.js
|
|
2783
|
+
|
|
2784
|
+
# Save PM2 configuration
|
|
2785
|
+
pm2 save
|
|
2786
|
+
|
|
2787
|
+
# Setup PM2 to start on system boot
|
|
2788
|
+
pm2 startup
|
|
2789
|
+
|
|
2790
|
+
# Monitor application
|
|
2791
|
+
pm2 monit
|
|
2792
|
+
|
|
2793
|
+
# View logs
|
|
2794
|
+
pm2 logs
|
|
2795
|
+
|
|
2796
|
+
# Restart application
|
|
2797
|
+
pm2 restart myapp
|
|
2798
|
+
|
|
2799
|
+
# Reload with zero downtime
|
|
2800
|
+
pm2 reload myapp
|
|
2801
|
+
```
|
|
2802
|
+
|
|
2803
|
+
**Step 4: Configure Nginx** (same as Option 2)
|
|
2804
|
+
|
|
2805
|
+
**Pros:**
|
|
2806
|
+
- ✅ Auto-restart on crash
|
|
2807
|
+
- ✅ Zero-downtime deployments
|
|
2808
|
+
- ✅ Cluster mode (use all CPU cores)
|
|
2809
|
+
- ✅ Log management
|
|
2810
|
+
- ✅ Monitoring
|
|
2811
|
+
- ✅ Auto-start on boot
|
|
2812
|
+
|
|
2813
|
+
---
|
|
2814
|
+
|
|
2815
|
+
---
|
|
2816
|
+
|
|
2817
|
+
## Advanced HTTPS Configuration
|
|
2818
|
+
|
|
2819
|
+
### Multiple Domains (SNI - Server Name Indication)
|
|
2820
|
+
|
|
2821
|
+
MasterController supports serving multiple domains with different certificates:
|
|
2822
|
+
|
|
2823
|
+
**Method 1: Using Environment Configuration**
|
|
2824
|
+
```json
|
|
2825
|
+
// config/environments/env.production.json
|
|
2826
|
+
{
|
|
2827
|
+
"server": {
|
|
2828
|
+
"httpPort": 443,
|
|
2829
|
+
"tls": {
|
|
2830
|
+
"default": {
|
|
2831
|
+
"keyPath": "/etc/letsencrypt/live/example.com/privkey.pem",
|
|
2832
|
+
"certPath": "/etc/letsencrypt/live/example.com/fullchain.pem"
|
|
2833
|
+
},
|
|
2834
|
+
"sni": {
|
|
2835
|
+
"api.example.com": {
|
|
2836
|
+
"keyPath": "/etc/letsencrypt/live/api.example.com/privkey.pem",
|
|
2837
|
+
"certPath": "/etc/letsencrypt/live/api.example.com/fullchain.pem"
|
|
2838
|
+
},
|
|
2839
|
+
"admin.example.com": {
|
|
2840
|
+
"keyPath": "/etc/letsencrypt/live/admin.example.com/privkey.pem",
|
|
2841
|
+
"certPath": "/etc/letsencrypt/live/admin.example.com/fullchain.pem"
|
|
2842
|
+
}
|
|
2843
|
+
},
|
|
2844
|
+
"hsts": true,
|
|
2845
|
+
"hstsMaxAge": 31536000
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
```
|
|
2850
|
+
|
|
2851
|
+
```javascript
|
|
2852
|
+
// server.js
|
|
2853
|
+
const master = require('mastercontroller');
|
|
2854
|
+
|
|
2855
|
+
master.environmentType = 'production';
|
|
2856
|
+
master.root = __dirname;
|
|
2857
|
+
|
|
2858
|
+
// Loads TLS config from environment file (including SNI)
|
|
2859
|
+
const server = master.setupServer('https');
|
|
2860
|
+
|
|
2861
|
+
require('./config/initializers/config');
|
|
2862
|
+
master.start(server);
|
|
2863
|
+
master.serverSettings(master.env.server);
|
|
2864
|
+
|
|
2865
|
+
console.log('✅ HTTPS with SNI enabled');
|
|
2866
|
+
console.log(' • example.com');
|
|
2867
|
+
console.log(' • api.example.com');
|
|
2868
|
+
console.log(' • admin.example.com');
|
|
2869
|
+
```
|
|
1352
2870
|
|
|
2871
|
+
**Method 2: Programmatic SNI**
|
|
1353
2872
|
```javascript
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
2873
|
+
const fs = require('fs');
|
|
2874
|
+
const tls = require('tls');
|
|
2875
|
+
const master = require('mastercontroller');
|
|
2876
|
+
|
|
2877
|
+
master.environmentType = 'production';
|
|
2878
|
+
master.root = __dirname;
|
|
2879
|
+
|
|
2880
|
+
// Default certificate
|
|
2881
|
+
const server = master.setupServer('https', {
|
|
2882
|
+
key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
|
|
2883
|
+
cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem'),
|
|
2884
|
+
|
|
2885
|
+
// SNI callback for different domains
|
|
2886
|
+
SNICallback: (servername, cb) => {
|
|
2887
|
+
let ctx;
|
|
2888
|
+
|
|
2889
|
+
switch(servername) {
|
|
2890
|
+
case 'api.example.com':
|
|
2891
|
+
ctx = tls.createSecureContext({
|
|
2892
|
+
key: fs.readFileSync('/etc/letsencrypt/live/api.example.com/privkey.pem'),
|
|
2893
|
+
cert: fs.readFileSync('/etc/letsencrypt/live/api.example.com/fullchain.pem')
|
|
2894
|
+
});
|
|
2895
|
+
break;
|
|
2896
|
+
|
|
2897
|
+
case 'admin.example.com':
|
|
2898
|
+
ctx = tls.createSecureContext({
|
|
2899
|
+
key: fs.readFileSync('/etc/letsencrypt/live/admin.example.com/privkey.pem'),
|
|
2900
|
+
cert: fs.readFileSync('/etc/letsencrypt/live/admin.example.com/fullchain.pem')
|
|
2901
|
+
});
|
|
2902
|
+
break;
|
|
2903
|
+
|
|
2904
|
+
default:
|
|
2905
|
+
// Use default certificate
|
|
2906
|
+
ctx = null;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
cb(null, ctx);
|
|
2910
|
+
}
|
|
1365
2911
|
});
|
|
2912
|
+
|
|
2913
|
+
require('./config/initializers/config');
|
|
2914
|
+
master.start(server);
|
|
2915
|
+
master.serverSettings({ httpPort: 443 });
|
|
1366
2916
|
```
|
|
1367
2917
|
|
|
1368
|
-
|
|
2918
|
+
---
|
|
1369
2919
|
|
|
1370
|
-
|
|
2920
|
+
### HTTP to HTTPS Redirect (Secure)
|
|
2921
|
+
|
|
2922
|
+
**⚠️ SECURITY:** Always specify allowed hosts to prevent open redirect attacks!
|
|
1371
2923
|
|
|
1372
2924
|
```javascript
|
|
1373
|
-
//
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
2925
|
+
// SECURE: Validate host header against whitelist
|
|
2926
|
+
const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
|
|
2927
|
+
'example.com',
|
|
2928
|
+
'www.example.com',
|
|
2929
|
+
'api.example.com'
|
|
2930
|
+
]);
|
|
1377
2931
|
|
|
1378
|
-
|
|
1379
|
-
GET /api/users/999
|
|
1380
|
-
Accept: application/json
|
|
1381
|
-
→ Returns JSON error response
|
|
1382
|
-
{
|
|
1383
|
-
"error": "Page Not Found",
|
|
1384
|
-
"statusCode": 404,
|
|
1385
|
-
"code": "MC_HTTP_ERROR",
|
|
1386
|
-
"message": "The user you're looking for doesn't exist."
|
|
1387
|
-
}
|
|
2932
|
+
console.log('✅ HTTP redirect server running on port 80');
|
|
1388
2933
|
```
|
|
1389
2934
|
|
|
1390
|
-
|
|
2935
|
+
**Why host validation?** Without it, attackers can redirect users to malicious domains:
|
|
2936
|
+
```bash
|
|
2937
|
+
# Attack without validation:
|
|
2938
|
+
curl -H "Host: evil.com" http://example.com
|
|
2939
|
+
# Redirects to: https://evil.com (phishing!)
|
|
2940
|
+
|
|
2941
|
+
# With validation: Returns 400 Bad Request ✅
|
|
2942
|
+
```
|
|
1391
2943
|
|
|
2944
|
+
**For Multiple Domains:**
|
|
1392
2945
|
```javascript
|
|
1393
|
-
|
|
1394
|
-
|
|
2946
|
+
// Redirect all domains
|
|
2947
|
+
const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
|
|
2948
|
+
'example.com',
|
|
2949
|
+
'www.example.com',
|
|
2950
|
+
'api.example.com',
|
|
2951
|
+
'admin.example.com',
|
|
2952
|
+
'blog.example.com'
|
|
2953
|
+
]);
|
|
2954
|
+
```
|
|
1395
2955
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
2956
|
+
---
|
|
2957
|
+
|
|
2958
|
+
### Certificate Renewal (Let's Encrypt)
|
|
2959
|
+
|
|
2960
|
+
**Automatic Renewal (Recommended)**
|
|
2961
|
+
|
|
2962
|
+
Let's Encrypt certificates expire after 90 days. Setup automatic renewal:
|
|
2963
|
+
|
|
2964
|
+
```bash
|
|
2965
|
+
# Method 1: Systemd timer (Ubuntu/Debian - already configured)
|
|
2966
|
+
sudo systemctl status certbot.timer
|
|
2967
|
+
|
|
2968
|
+
# Method 2: Crontab (manual setup)
|
|
2969
|
+
sudo crontab -e
|
|
2970
|
+
# Add this line (runs twice daily):
|
|
2971
|
+
0 0,12 * * * certbot renew --quiet --post-hook "systemctl restart myapp"
|
|
2972
|
+
|
|
2973
|
+
# Method 3: PM2 with reload hook
|
|
2974
|
+
sudo crontab -e
|
|
2975
|
+
# Add this line:
|
|
2976
|
+
0 0 * * * certbot renew --quiet --post-hook "pm2 reload myapp"
|
|
1403
2977
|
```
|
|
1404
2978
|
|
|
1405
|
-
|
|
2979
|
+
**Manual Renewal**
|
|
2980
|
+
```bash
|
|
2981
|
+
# Test renewal (dry run)
|
|
2982
|
+
sudo certbot renew --dry-run
|
|
1406
2983
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
async index(obj) {
|
|
1410
|
-
try {
|
|
1411
|
-
const users = await this.db.query('SELECT * FROM users');
|
|
1412
|
-
this.render('index', { users });
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
console.error('Database error:', error);
|
|
2984
|
+
# Actually renew certificates
|
|
2985
|
+
sudo certbot renew
|
|
1415
2986
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
2987
|
+
# Restart your application
|
|
2988
|
+
pm2 restart myapp
|
|
2989
|
+
# or
|
|
2990
|
+
sudo systemctl restart myapp
|
|
1424
2991
|
```
|
|
1425
2992
|
|
|
1426
|
-
|
|
2993
|
+
**Certificate Live Reload (Zero Downtime)**
|
|
2994
|
+
|
|
2995
|
+
MasterController supports certificate live reload with `fs.watchFile()`:
|
|
1427
2996
|
|
|
1428
2997
|
```javascript
|
|
1429
|
-
const
|
|
2998
|
+
const fs = require('fs');
|
|
2999
|
+
const master = require('mastercontroller');
|
|
1430
3000
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
code: 'USER_LOGIN',
|
|
1434
|
-
message: 'User logged in',
|
|
1435
|
-
userId: user.id
|
|
1436
|
-
});
|
|
3001
|
+
const certPath = '/etc/letsencrypt/live/example.com/fullchain.pem';
|
|
3002
|
+
const keyPath = '/etc/letsencrypt/live/example.com/privkey.pem';
|
|
1437
3003
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
email: input
|
|
3004
|
+
let server = master.setupServer('https', {
|
|
3005
|
+
key: fs.readFileSync(keyPath),
|
|
3006
|
+
cert: fs.readFileSync(certPath)
|
|
1442
3007
|
});
|
|
1443
3008
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
3009
|
+
// Watch for certificate changes
|
|
3010
|
+
fs.watchFile(certPath, (curr, prev) => {
|
|
3011
|
+
console.log('📝 Certificate changed, reloading...');
|
|
3012
|
+
|
|
3013
|
+
try {
|
|
3014
|
+
// Reload certificates without restarting server
|
|
3015
|
+
const newCert = fs.readFileSync(certPath);
|
|
3016
|
+
const newKey = fs.readFileSync(keyPath);
|
|
3017
|
+
|
|
3018
|
+
// Update server context
|
|
3019
|
+
server.setSecureContext({
|
|
3020
|
+
key: newKey,
|
|
3021
|
+
cert: newCert
|
|
3022
|
+
});
|
|
3023
|
+
|
|
3024
|
+
console.log('✅ Certificate reloaded successfully (zero downtime)');
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
console.error('❌ Failed to reload certificate:', error);
|
|
3027
|
+
}
|
|
1449
3028
|
});
|
|
3029
|
+
|
|
3030
|
+
require('./config/initializers/config');
|
|
3031
|
+
master.start(server);
|
|
3032
|
+
master.serverSettings({ httpPort: 443 });
|
|
1450
3033
|
```
|
|
1451
3034
|
|
|
1452
3035
|
---
|
|
1453
3036
|
|
|
1454
|
-
|
|
3037
|
+
### Common Errors and Solutions
|
|
3038
|
+
|
|
3039
|
+
#### Error: "EACCES: permission denied, bind 80" or "bind 443"
|
|
3040
|
+
|
|
3041
|
+
**Problem:** Node.js doesn't have permission to bind to ports 80/443.
|
|
3042
|
+
|
|
3043
|
+
**Solutions:**
|
|
3044
|
+
|
|
3045
|
+
**Option 1: Use setcap (Linux - Recommended)**
|
|
3046
|
+
```bash
|
|
3047
|
+
# Give Node.js permission to bind to privileged ports
|
|
3048
|
+
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
|
3049
|
+
|
|
3050
|
+
# Verify
|
|
3051
|
+
getcap $(which node)
|
|
3052
|
+
# Output: /usr/bin/node = cap_net_bind_service+ep
|
|
3053
|
+
```
|
|
3054
|
+
|
|
3055
|
+
**Option 2: Run as root (Not Recommended)**
|
|
3056
|
+
```bash
|
|
3057
|
+
sudo node server.js
|
|
3058
|
+
```
|
|
3059
|
+
|
|
3060
|
+
**Option 3: Use reverse proxy (Best)**
|
|
3061
|
+
```bash
|
|
3062
|
+
# Run Node.js on port 3000 (no permissions needed)
|
|
3063
|
+
# Use Nginx on ports 80/443
|
|
3064
|
+
```
|
|
3065
|
+
|
|
3066
|
+
**Option 4: Use authbind (Linux)**
|
|
3067
|
+
```bash
|
|
3068
|
+
sudo apt install authbind
|
|
3069
|
+
sudo touch /etc/authbind/byport/80
|
|
3070
|
+
sudo touch /etc/authbind/byport/443
|
|
3071
|
+
sudo chmod 500 /etc/authbind/byport/80
|
|
3072
|
+
sudo chmod 500 /etc/authbind/byport/443
|
|
3073
|
+
sudo chown $USER /etc/authbind/byport/80
|
|
3074
|
+
sudo chown $USER /etc/authbind/byport/443
|
|
3075
|
+
|
|
3076
|
+
# Run with authbind
|
|
3077
|
+
authbind --deep node server.js
|
|
3078
|
+
```
|
|
3079
|
+
|
|
3080
|
+
---
|
|
3081
|
+
|
|
3082
|
+
#### Error: "ENOENT: no such file or directory, open '/path/to/cert.pem'"
|
|
3083
|
+
|
|
3084
|
+
**Problem:** Certificate files don't exist or path is wrong.
|
|
3085
|
+
|
|
3086
|
+
**Solutions:**
|
|
3087
|
+
|
|
3088
|
+
```bash
|
|
3089
|
+
# Check if files exist
|
|
3090
|
+
ls -l /etc/letsencrypt/live/yourapp.com/
|
|
3091
|
+
|
|
3092
|
+
# Check permissions
|
|
3093
|
+
sudo ls -l /etc/letsencrypt/live/yourapp.com/
|
|
3094
|
+
# If you see permission denied, you need to run as root or copy certs
|
|
1455
3095
|
|
|
1456
|
-
|
|
3096
|
+
# Copy certificates to accessible location (if needed)
|
|
3097
|
+
sudo cp /etc/letsencrypt/live/yourapp.com/privkey.pem ~/certs/
|
|
3098
|
+
sudo cp /etc/letsencrypt/live/yourapp.com/fullchain.pem ~/certs/
|
|
3099
|
+
sudo chown $USER:$USER ~/certs/*.pem
|
|
3100
|
+
```
|
|
3101
|
+
|
|
3102
|
+
---
|
|
3103
|
+
|
|
3104
|
+
#### Error: "unable to verify the first certificate"
|
|
3105
|
+
|
|
3106
|
+
**Problem:** Missing intermediate certificates (chain).
|
|
3107
|
+
|
|
3108
|
+
**Solution:**
|
|
1457
3109
|
|
|
1458
3110
|
```javascript
|
|
1459
|
-
|
|
3111
|
+
// Use fullchain.pem instead of cert.pem
|
|
3112
|
+
const server = master.setupServer('https', {
|
|
3113
|
+
key: fs.readFileSync('/path/to/privkey.pem'),
|
|
3114
|
+
cert: fs.readFileSync('/path/to/fullchain.pem'), // NOT cert.pem!
|
|
3115
|
+
ca: fs.readFileSync('/path/to/chain.pem') // Optional: explicit chain
|
|
3116
|
+
});
|
|
3117
|
+
```
|
|
3118
|
+
|
|
3119
|
+
---
|
|
3120
|
+
|
|
3121
|
+
#### Error: "cert has expired"
|
|
3122
|
+
|
|
3123
|
+
**Problem:** SSL certificate has expired.
|
|
3124
|
+
|
|
3125
|
+
**Solutions:**
|
|
3126
|
+
|
|
3127
|
+
```bash
|
|
3128
|
+
# Check expiration date
|
|
3129
|
+
openssl x509 -in /etc/letsencrypt/live/yourapp.com/fullchain.pem -noout -dates
|
|
1460
3130
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
3131
|
+
# Renew certificate
|
|
3132
|
+
sudo certbot renew
|
|
3133
|
+
|
|
3134
|
+
# Restart application
|
|
3135
|
+
pm2 restart myapp
|
|
3136
|
+
```
|
|
3137
|
+
|
|
3138
|
+
---
|
|
3139
|
+
|
|
3140
|
+
#### Error: Browser shows "Not Secure" or "NET::ERR_CERT_AUTHORITY_INVALID"
|
|
3141
|
+
|
|
3142
|
+
**Problem:** Self-signed certificate (development) or certificate not trusted.
|
|
3143
|
+
|
|
3144
|
+
**Solutions:**
|
|
3145
|
+
|
|
3146
|
+
**For Development (Self-Signed):**
|
|
3147
|
+
1. Click "Advanced" in browser
|
|
3148
|
+
2. Click "Proceed to localhost" (safe for development)
|
|
3149
|
+
3. Or add certificate to system trust store
|
|
3150
|
+
|
|
3151
|
+
**For Production:**
|
|
3152
|
+
1. Use Let's Encrypt or commercial CA certificate
|
|
3153
|
+
2. Ensure fullchain.pem includes intermediate certificates
|
|
3154
|
+
3. Check certificate matches domain name
|
|
3155
|
+
|
|
3156
|
+
---
|
|
3157
|
+
|
|
3158
|
+
#### Browser keeps redirecting between HTTP and HTTPS (Loop)
|
|
3159
|
+
|
|
3160
|
+
**Problem:** Redirect loop, usually caused by incorrect proxy configuration.
|
|
3161
|
+
|
|
3162
|
+
**Solution:**
|
|
3163
|
+
|
|
3164
|
+
```javascript
|
|
3165
|
+
// config/initializers/config.js
|
|
3166
|
+
// Configure hostname in environment file
|
|
3167
|
+
master.env = {
|
|
3168
|
+
server: {
|
|
3169
|
+
hostname: 'yourapp.com', // Set this!
|
|
3170
|
+
httpsPort: 443
|
|
3171
|
+
}
|
|
1464
3172
|
};
|
|
1465
3173
|
|
|
1466
|
-
|
|
3174
|
+
// For Nginx proxy, make sure X-Forwarded-Proto is set correctly
|
|
3175
|
+
```
|
|
3176
|
+
|
|
3177
|
+
---
|
|
3178
|
+
|
|
3179
|
+
### HSTS (HTTP Strict Transport Security)
|
|
3180
|
+
|
|
3181
|
+
HSTS tells browsers to always use HTTPS for your domain (prevents downgrade attacks).
|
|
3182
|
+
|
|
3183
|
+
```javascript
|
|
3184
|
+
// Basic usage (1 year, includeSubDomains)
|
|
3185
|
+
master.enableHSTS();
|
|
3186
|
+
|
|
3187
|
+
// Custom configuration
|
|
3188
|
+
master.enableHSTS({
|
|
3189
|
+
maxAge: 15552000, // 180 days
|
|
3190
|
+
includeSubDomains: true, // Cover *.example.com
|
|
3191
|
+
preload: false // Don't submit to preload list yet
|
|
3192
|
+
});
|
|
1467
3193
|
```
|
|
1468
3194
|
|
|
1469
|
-
|
|
3195
|
+
**HSTS Preload List:**
|
|
3196
|
+
After running HSTS for 30+ days, submit to [hstspreload.org](https://hstspreload.org/) for browser built-in enforcement.
|
|
3197
|
+
|
|
3198
|
+
### Environment-based TLS Configuration
|
|
1470
3199
|
|
|
1471
3200
|
Configure TLS in `config/environments/env.production.json`:
|
|
1472
3201
|
|
|
@@ -1488,33 +3217,206 @@ Configure TLS in `config/environments/env.production.json`:
|
|
|
1488
3217
|
"keyPath": "/path/to/app.key",
|
|
1489
3218
|
"certPath": "/path/to/app.crt"
|
|
1490
3219
|
}
|
|
1491
|
-
}
|
|
3220
|
+
},
|
|
3221
|
+
"hsts": true,
|
|
3222
|
+
"hstsMaxAge": 31536000
|
|
1492
3223
|
}
|
|
1493
3224
|
}
|
|
1494
3225
|
}
|
|
1495
3226
|
```
|
|
1496
3227
|
|
|
1497
3228
|
```javascript
|
|
3229
|
+
// Loads TLS config from environment file
|
|
1498
3230
|
const server = master.setupServer('https');
|
|
1499
3231
|
master.serverSettings(master.env.server);
|
|
1500
3232
|
```
|
|
1501
3233
|
|
|
1502
|
-
|
|
3234
|
+
**Features:**
|
|
3235
|
+
- ✅ **SNI Support** - Different certificates for different domains
|
|
3236
|
+
- ✅ **Live Reload** - Update certificates without restarting server
|
|
3237
|
+
- ✅ **HSTS Configuration** - Automatic HSTS from config
|
|
3238
|
+
|
|
3239
|
+
### Advanced TLS Configuration
|
|
3240
|
+
|
|
3241
|
+
#### Custom TLS Version
|
|
3242
|
+
```javascript
|
|
3243
|
+
const server = master.setupServer('https', {
|
|
3244
|
+
key: fs.readFileSync('/path/to/key.pem'),
|
|
3245
|
+
cert: fs.readFileSync('/path/to/cert.pem'),
|
|
3246
|
+
minVersion: 'TLSv1.2', // Override default TLS 1.3 (for compatibility)
|
|
3247
|
+
maxVersion: 'TLSv1.3'
|
|
3248
|
+
});
|
|
3249
|
+
```
|
|
3250
|
+
|
|
3251
|
+
#### Custom Cipher Suites
|
|
3252
|
+
```javascript
|
|
3253
|
+
const server = master.setupServer('https', {
|
|
3254
|
+
key: fs.readFileSync('/path/to/key.pem'),
|
|
3255
|
+
cert: fs.readFileSync('/path/to/cert.pem'),
|
|
3256
|
+
ciphers: [
|
|
3257
|
+
'TLS_AES_256_GCM_SHA384',
|
|
3258
|
+
'TLS_CHACHA20_POLY1305_SHA256',
|
|
3259
|
+
'ECDHE-RSA-AES256-GCM-SHA384'
|
|
3260
|
+
].join(':')
|
|
3261
|
+
});
|
|
3262
|
+
```
|
|
3263
|
+
|
|
3264
|
+
**Note:** MasterController uses secure defaults. Only customize if you have specific requirements.
|
|
3265
|
+
|
|
3266
|
+
### Let's Encrypt Example
|
|
1503
3267
|
|
|
1504
3268
|
```javascript
|
|
1505
|
-
//
|
|
1506
|
-
const
|
|
1507
|
-
|
|
3269
|
+
// Let's Encrypt certificates (auto-renewed by certbot)
|
|
3270
|
+
const server = master.setupServer('https', {
|
|
3271
|
+
key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
|
|
3272
|
+
cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem')
|
|
3273
|
+
});
|
|
3274
|
+
|
|
3275
|
+
master.enableHSTS({
|
|
3276
|
+
maxAge: 31536000,
|
|
3277
|
+
includeSubDomains: true,
|
|
3278
|
+
preload: true
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3281
|
+
master.start(server);
|
|
1508
3282
|
master.serverSettings({ httpPort: 443 });
|
|
1509
3283
|
|
|
1510
|
-
//
|
|
1511
|
-
const redirectServer = master.startHttpToHttpsRedirect(80
|
|
3284
|
+
// Secure HTTP redirect
|
|
3285
|
+
const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
|
|
3286
|
+
'example.com',
|
|
3287
|
+
'www.example.com'
|
|
3288
|
+
]);
|
|
1512
3289
|
```
|
|
1513
3290
|
|
|
1514
|
-
###
|
|
3291
|
+
### Testing Your HTTPS Setup
|
|
3292
|
+
|
|
3293
|
+
#### 1. SSL Labs Test
|
|
3294
|
+
```bash
|
|
3295
|
+
# Test your HTTPS configuration (should get A or A+)
|
|
3296
|
+
https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com
|
|
3297
|
+
```
|
|
3298
|
+
|
|
3299
|
+
#### 2. Local Testing
|
|
3300
|
+
```bash
|
|
3301
|
+
# Test TLS 1.3
|
|
3302
|
+
curl -v --tlsv1.3 https://localhost
|
|
3303
|
+
|
|
3304
|
+
# Test HSTS header
|
|
3305
|
+
curl -I https://localhost | grep Strict-Transport-Security
|
|
3306
|
+
|
|
3307
|
+
# Test HTTP redirect
|
|
3308
|
+
curl -I http://localhost
|
|
3309
|
+
|
|
3310
|
+
# Test path traversal protection (should return 403)
|
|
3311
|
+
curl http://localhost/../../../etc/passwd
|
|
3312
|
+
```
|
|
3313
|
+
|
|
3314
|
+
#### 3. Cipher Suite Testing
|
|
3315
|
+
```bash
|
|
3316
|
+
# Use testssl.sh for comprehensive testing
|
|
3317
|
+
./testssl.sh --full https://yourdomain.com
|
|
3318
|
+
|
|
3319
|
+
# Or nmap
|
|
3320
|
+
nmap --script ssl-enum-ciphers -p 443 yourdomain.com
|
|
3321
|
+
```
|
|
3322
|
+
|
|
3323
|
+
### Security Comparison
|
|
3324
|
+
|
|
3325
|
+
MasterController's HTTPS implementation **exceeds industry standards**:
|
|
3326
|
+
|
|
3327
|
+
| Feature | MasterController v1.3.1 | Express | ASP.NET Core | Rails |
|
|
3328
|
+
|---------|-------------------------|---------|--------------|-------|
|
|
3329
|
+
| **TLS 1.3 Default** | ✅ | ❌ | ❌ | ❌ |
|
|
3330
|
+
| **Secure Ciphers** | ✅ Auto | ❌ Manual | ⚠️ Partial | ❌ Manual |
|
|
3331
|
+
| **Path Traversal Protection** | ✅ | ✅ | ✅ | ✅ |
|
|
3332
|
+
| **Open Redirect Protection** | ✅ | ✅ | ✅ | ✅ |
|
|
3333
|
+
| **SNI Support** | ✅ Built-in | ❌ Manual | ✅ | ❌ Manual |
|
|
3334
|
+
| **Certificate Live Reload** | ✅ **Unique!** | ❌ | ❌ | ❌ |
|
|
3335
|
+
| **HSTS Built-in** | ✅ | Via helmet | ✅ | ✅ |
|
|
3336
|
+
|
|
3337
|
+
### Complete Production Example
|
|
1515
3338
|
|
|
1516
3339
|
```javascript
|
|
1517
|
-
|
|
3340
|
+
// server.js - Production HTTPS setup
|
|
3341
|
+
const master = require('mastercontroller');
|
|
3342
|
+
const fs = require('fs');
|
|
3343
|
+
|
|
3344
|
+
// Set environment
|
|
3345
|
+
master.environmentType = process.env.NODE_ENV || 'production';
|
|
3346
|
+
master.root = __dirname;
|
|
3347
|
+
|
|
3348
|
+
// Setup HTTPS with Let's Encrypt certificates
|
|
3349
|
+
const server = master.setupServer('https', {
|
|
3350
|
+
key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
|
|
3351
|
+
cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem')
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
// Enable HSTS with preload
|
|
3355
|
+
master.enableHSTS({
|
|
3356
|
+
maxAge: 31536000, // 1 year
|
|
3357
|
+
includeSubDomains: true,
|
|
3358
|
+
preload: true // Submit to hstspreload.org after 30 days
|
|
3359
|
+
});
|
|
3360
|
+
|
|
3361
|
+
// Load application configuration
|
|
3362
|
+
require('./config/initializers/config');
|
|
3363
|
+
|
|
3364
|
+
// Start HTTPS server on port 443
|
|
3365
|
+
master.start(server);
|
|
3366
|
+
master.serverSettings({ httpPort: 443 });
|
|
3367
|
+
|
|
3368
|
+
// Start HTTP to HTTPS redirect with host validation
|
|
3369
|
+
const redirectServer = master.startHttpToHttpsRedirect(80, '0.0.0.0', [
|
|
3370
|
+
'example.com',
|
|
3371
|
+
'www.example.com',
|
|
3372
|
+
'api.example.com',
|
|
3373
|
+
'admin.example.com'
|
|
3374
|
+
]);
|
|
3375
|
+
|
|
3376
|
+
console.log('========================================');
|
|
3377
|
+
console.log('🚀 MasterController Production Server');
|
|
3378
|
+
console.log('========================================');
|
|
3379
|
+
console.log('✅ HTTPS on port 443');
|
|
3380
|
+
console.log('✅ HTTP redirect on port 80');
|
|
3381
|
+
console.log('✅ TLS 1.3 enabled');
|
|
3382
|
+
console.log('✅ Secure cipher suites');
|
|
3383
|
+
console.log('✅ HSTS enabled (max-age: 1 year)');
|
|
3384
|
+
console.log('✅ Path traversal protection');
|
|
3385
|
+
console.log('✅ Open redirect protection');
|
|
3386
|
+
console.log('========================================');
|
|
3387
|
+
```
|
|
3388
|
+
|
|
3389
|
+
### Troubleshooting
|
|
3390
|
+
|
|
3391
|
+
**Certificate Errors:**
|
|
3392
|
+
```bash
|
|
3393
|
+
# Check certificate expiration
|
|
3394
|
+
openssl x509 -in /path/to/cert.pem -noout -dates
|
|
3395
|
+
|
|
3396
|
+
# Verify certificate chain
|
|
3397
|
+
openssl verify -CAfile /path/to/ca.pem /path/to/cert.pem
|
|
3398
|
+
```
|
|
3399
|
+
|
|
3400
|
+
**Port Permission Errors (ports 80/443):**
|
|
3401
|
+
```bash
|
|
3402
|
+
# Option 1: Use setcap (Linux)
|
|
3403
|
+
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
|
3404
|
+
|
|
3405
|
+
# Option 2: Run as root (not recommended)
|
|
3406
|
+
sudo node server.js
|
|
3407
|
+
|
|
3408
|
+
# Option 3: Use reverse proxy (recommended)
|
|
3409
|
+
# Run Node.js on high port (3000) behind nginx/Apache
|
|
3410
|
+
```
|
|
3411
|
+
|
|
3412
|
+
**HSTS Testing:**
|
|
3413
|
+
```bash
|
|
3414
|
+
# Check if HSTS header is present
|
|
3415
|
+
curl -I https://yourdomain.com | grep -i strict
|
|
3416
|
+
|
|
3417
|
+
# Check HSTS status in browser
|
|
3418
|
+
# Chrome: chrome://net-internals/#hsts
|
|
3419
|
+
# Firefox: about:networking#hsts
|
|
1518
3420
|
```
|
|
1519
3421
|
|
|
1520
3422
|
---
|
|
@@ -1580,16 +3482,15 @@ master.enableHSTS(); // In production HTTPS
|
|
|
1580
3482
|
|
|
1581
3483
|
### Sessions
|
|
1582
3484
|
|
|
1583
|
-
- `master.
|
|
1584
|
-
- `master.
|
|
1585
|
-
- `master.
|
|
1586
|
-
- `master.
|
|
1587
|
-
- `master.
|
|
1588
|
-
- `master.
|
|
1589
|
-
- `master.
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
- `master.sessions.middleware()` - Get pipeline middleware
|
|
3485
|
+
- `master.session.init(options)` - Initialize secure sessions
|
|
3486
|
+
- `master.session.destroy(req, res)` - Destroy session completely
|
|
3487
|
+
- `master.session.touch(sessionId)` - Extend session expiry
|
|
3488
|
+
- `master.session.getSessionCount()` - Get active session count
|
|
3489
|
+
- `master.session.clearAllSessions()` - Clear all sessions (testing only)
|
|
3490
|
+
- `master.session.getBestPractices(environment)` - Get recommended settings
|
|
3491
|
+
- `master.session.middleware()` - Get pipeline middleware
|
|
3492
|
+
|
|
3493
|
+
**Session Data Access:** Use `obj.request.session` object directly (Rails/Express style)
|
|
1593
3494
|
|
|
1594
3495
|
### Request
|
|
1595
3496
|
|
|
@@ -1602,11 +3503,26 @@ master.enableHSTS(); // In production HTTPS
|
|
|
1602
3503
|
|
|
1603
3504
|
### Tools
|
|
1604
3505
|
|
|
1605
|
-
|
|
1606
|
-
- `master.tools.
|
|
3506
|
+
**Encryption:**
|
|
3507
|
+
- `master.tools.encrypt(data, secret)` - Encrypt data with AES-256-CBC
|
|
3508
|
+
- `master.tools.decrypt(data, secret)` - Decrypt data with AES-256-CBC
|
|
3509
|
+
|
|
3510
|
+
**File Conversion (NEW in v1.3.1):**
|
|
3511
|
+
- `master.tools.fileToBase64(filePathOrFile, options)` - Convert file to base64 (binary-safe)
|
|
3512
|
+
- `master.tools.base64ToFile(base64String, outputPath, options)` - Convert base64 to file
|
|
3513
|
+
- `master.tools.fileToBuffer(filePathOrFile, options)` - Convert file to Node.js Buffer
|
|
3514
|
+
- `master.tools.fileToBytes(filePathOrFile, options)` - Convert file to Uint8Array
|
|
3515
|
+
- `master.tools.bytesToBase64(bufferOrBytes, options)` - Convert Buffer/Uint8Array to base64
|
|
3516
|
+
- `master.tools.base64ToBytes(base64String)` - Convert base64 to Buffer
|
|
3517
|
+
- `master.tools.streamFileToBase64(filePathOrFile, options)` - Stream large files to base64 (async)
|
|
3518
|
+
|
|
3519
|
+
**Utilities:**
|
|
1607
3520
|
- `master.tools.combineObjects(target, source)` - Merge objects
|
|
1608
3521
|
- `master.tools.makeWordId(length)` - Generate random ID
|
|
1609
3522
|
|
|
3523
|
+
**Deprecated:**
|
|
3524
|
+
- `master.tools.base64(path)` - ⚠️ DEPRECATED - Broken for binary files, use `fileToBase64()` instead
|
|
3525
|
+
|
|
1610
3526
|
---
|
|
1611
3527
|
|
|
1612
3528
|
## Production Tips
|
|
@@ -1626,14 +3542,17 @@ master.enableHSTS(); // In production HTTPS
|
|
|
1626
3542
|
|
|
1627
3543
|
## Documentation
|
|
1628
3544
|
|
|
1629
|
-
|
|
3545
|
+
### Security Documentation
|
|
3546
|
+
|
|
3547
|
+
- [Security Fixes v1.3.2](SECURITY-FIXES-v1.3.2.md) - All security fixes and migration guide
|
|
3548
|
+
- [Security Quick Start](docs/SECURITY-QUICKSTART.md) - 5-minute security setup guide
|
|
3549
|
+
- [Security Audit - Action System](docs/SECURITY-AUDIT-ACTION-SYSTEM.md) - Complete security audit of controllers and filters
|
|
3550
|
+
- [Security Audit - HTTPS](docs/SECURITY-AUDIT-HTTPS.md) - HTTPS/TLS security audit
|
|
3551
|
+
|
|
3552
|
+
### Feature Documentation
|
|
1630
3553
|
|
|
1631
|
-
- [
|
|
1632
|
-
- [
|
|
1633
|
-
- [HTTPS with Environment TLS & SNI](docs/server-setup-https-env-tls-sni.md)
|
|
1634
|
-
- [Hostname Binding](docs/server-setup-hostname-binding.md)
|
|
1635
|
-
- [Nginx Reverse Proxy](docs/server-setup-nginx-reverse-proxy.md)
|
|
1636
|
-
- [Environment TLS Reference](docs/environment-tls-reference.md)
|
|
3554
|
+
- [Timeout and Error Handling](docs/timeout-and-error-handling.md) - Professional timeout tracking and error rendering
|
|
3555
|
+
- [Environment TLS Reference](docs/environment-tls-reference.md) - TLS/SNI configuration reference
|
|
1637
3556
|
|
|
1638
3557
|
---
|
|
1639
3558
|
|