mastercontroller 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +4 -1
- package/MasterAction.js +137 -23
- package/MasterActionFilters.js +197 -92
- package/MasterControl.js +265 -44
- package/MasterHtml.js +226 -143
- package/MasterPipeline.js +1 -1
- package/MasterRequest.js +202 -24
- package/MasterSocket.js +6 -1
- package/MasterTools.js +428 -13
- package/README.md +2364 -309
- package/SECURITY-FIXES-v1.3.2.md +614 -0
- package/docs/SECURITY-AUDIT-ACTION-SYSTEM.md +1374 -0
- package/docs/SECURITY-AUDIT-HTTPS.md +1056 -0
- package/docs/SECURITY-QUICKSTART.md +375 -0
- package/docs/timeout-and-error-handling.md +8 -6
- package/package.json +1 -1
- package/security/SecurityEnforcement.js +241 -0
- package/security/SessionSecurity.js +100 -2
- package/test/security/filters.test.js +276 -0
- package/test/security/https.test.js +214 -0
- package/test/security/path-traversal.test.js +222 -0
- package/test/security/xss.test.js +190 -0
- package/MasterSession.js +0 -208
- package/docs/server-setup-hostname-binding.md +0 -24
- package/docs/server-setup-http.md +0 -32
- package/docs/server-setup-https-credentials.md +0 -32
- package/docs/server-setup-https-env-tls-sni.md +0 -62
- package/docs/server-setup-nginx-reverse-proxy.md +0 -46
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,94 +797,129 @@ master.cors.init({
|
|
|
790
797
|
|
|
791
798
|
## Sessions
|
|
792
799
|
|
|
793
|
-
|
|
800
|
+
MasterController provides secure, Rails/Django-style sessions with automatic regeneration and protection.
|
|
801
|
+
|
|
802
|
+
### Secure Sessions
|
|
803
|
+
|
|
804
|
+
#### `master.session.init(options)`
|
|
794
805
|
|
|
795
|
-
Initialize sessions (auto-registers with middleware pipeline).
|
|
806
|
+
Initialize secure sessions with Rails/Django-style `req.session` object (auto-registers with middleware pipeline).
|
|
796
807
|
|
|
797
808
|
```javascript
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
809
|
+
// Environment-specific configuration
|
|
810
|
+
const isProduction = master.environmentType === 'production';
|
|
811
|
+
|
|
812
|
+
master.session.init({
|
|
813
|
+
cookieName: 'mc_session',
|
|
814
|
+
maxAge: isProduction ? 3600000 : 86400000, // Production: 1 hour, Dev: 24 hours
|
|
815
|
+
httpOnly: true, // Prevent JavaScript access (XSS protection)
|
|
816
|
+
secure: isProduction, // HTTPS only in production
|
|
817
|
+
sameSite: isProduction ? 'strict' : 'lax', // CSRF protection
|
|
818
|
+
rolling: true, // Extend session on each request
|
|
819
|
+
regenerateInterval: 900000, // Regenerate session ID every 15 minutes
|
|
820
|
+
useFingerprint: false // Session hijacking detection (opt-in)
|
|
805
821
|
});
|
|
806
822
|
```
|
|
807
823
|
|
|
808
|
-
|
|
824
|
+
**Security Features:**
|
|
825
|
+
- ✅ 32-byte (256-bit) session IDs (cryptographically secure)
|
|
826
|
+
- ✅ Automatic session regeneration (prevents fixation attacks)
|
|
827
|
+
- ✅ HttpOnly cookies (prevents XSS cookie theft)
|
|
828
|
+
- ✅ Secure flag for HTTPS (prevents MITM attacks)
|
|
829
|
+
- ✅ SameSite CSRF protection
|
|
830
|
+
- ✅ Rolling sessions (extends expiry on activity)
|
|
831
|
+
- ✅ Automatic cleanup of expired sessions
|
|
832
|
+
- ✅ Optional fingerprinting (detects hijacking)
|
|
833
|
+
|
|
834
|
+
#### Using Sessions in Controllers
|
|
809
835
|
|
|
810
|
-
|
|
811
|
-
Create a session.
|
|
836
|
+
Sessions are accessed via `obj.request.session` object:
|
|
812
837
|
|
|
813
838
|
```javascript
|
|
814
839
|
class AuthController {
|
|
815
840
|
login(obj) {
|
|
816
841
|
const user = authenticateUser(obj.params.formData);
|
|
817
842
|
|
|
818
|
-
|
|
843
|
+
// Set session data (Rails/Express style)
|
|
844
|
+
obj.request.session.userId = user.id;
|
|
845
|
+
obj.request.session.username = user.name;
|
|
846
|
+
obj.request.session.loggedInAt = Date.now();
|
|
819
847
|
|
|
820
848
|
this.redirect('/dashboard');
|
|
821
849
|
}
|
|
850
|
+
|
|
851
|
+
logout(obj) {
|
|
852
|
+
// Destroy entire session
|
|
853
|
+
master.session.destroy(obj.request, obj.response);
|
|
854
|
+
this.redirect('/');
|
|
855
|
+
}
|
|
822
856
|
}
|
|
823
857
|
```
|
|
824
858
|
|
|
825
|
-
#### `master.sessions.get(name, request, secret)`
|
|
826
|
-
Retrieve session data.
|
|
827
|
-
|
|
828
859
|
```javascript
|
|
829
860
|
class DashboardController {
|
|
830
861
|
index(obj) {
|
|
831
|
-
|
|
862
|
+
// Read session data
|
|
863
|
+
const userId = obj.request.session.userId;
|
|
832
864
|
|
|
833
|
-
if (!
|
|
865
|
+
if (!userId) {
|
|
834
866
|
this.redirect('/login');
|
|
835
867
|
return;
|
|
836
868
|
}
|
|
837
869
|
|
|
838
|
-
this.render('dashboard', {
|
|
870
|
+
this.render('dashboard', { userId });
|
|
839
871
|
}
|
|
840
872
|
}
|
|
841
873
|
```
|
|
842
874
|
|
|
843
|
-
####
|
|
844
|
-
|
|
875
|
+
#### Session Management API
|
|
876
|
+
|
|
877
|
+
**`master.session.destroy(req, res)`** - Destroy session completely
|
|
845
878
|
|
|
846
879
|
```javascript
|
|
847
|
-
|
|
848
|
-
logout(obj) {
|
|
849
|
-
master.sessions.delete('user', obj.response);
|
|
850
|
-
this.redirect('/');
|
|
851
|
-
}
|
|
852
|
-
}
|
|
880
|
+
master.session.destroy(obj.request, obj.response);
|
|
853
881
|
```
|
|
854
882
|
|
|
855
|
-
|
|
856
|
-
Clear all sessions (useful for testing).
|
|
883
|
+
**`master.session.touch(sessionId)`** - Extend session expiry
|
|
857
884
|
|
|
858
885
|
```javascript
|
|
859
|
-
master.
|
|
886
|
+
master.session.touch(obj.request.sessionId);
|
|
860
887
|
```
|
|
861
888
|
|
|
862
|
-
|
|
889
|
+
**`master.session.getSessionCount()`** - Get active session count (monitoring)
|
|
863
890
|
|
|
864
|
-
Direct cookie access:
|
|
865
|
-
|
|
866
|
-
#### `master.sessions.setCookie(name, value, response, options)`
|
|
867
891
|
```javascript
|
|
868
|
-
master.
|
|
892
|
+
const count = master.session.getSessionCount();
|
|
893
|
+
console.log(`Active sessions: ${count}`);
|
|
869
894
|
```
|
|
870
895
|
|
|
871
|
-
|
|
896
|
+
**`master.session.clearAllSessions()`** - Clear all sessions (testing only)
|
|
897
|
+
|
|
872
898
|
```javascript
|
|
873
|
-
|
|
899
|
+
master.session.clearAllSessions();
|
|
874
900
|
```
|
|
875
901
|
|
|
876
|
-
####
|
|
902
|
+
#### Environment-Specific Best Practices
|
|
903
|
+
|
|
877
904
|
```javascript
|
|
878
|
-
|
|
905
|
+
// Get recommended settings
|
|
906
|
+
const settings = master.session.getBestPractices('production');
|
|
907
|
+
master.session.init(settings);
|
|
879
908
|
```
|
|
880
909
|
|
|
910
|
+
**Production Settings:**
|
|
911
|
+
- Secure: true (HTTPS only)
|
|
912
|
+
- SameSite: 'strict' (maximum CSRF protection)
|
|
913
|
+
- MaxAge: 1 hour (short-lived sessions)
|
|
914
|
+
- RegenerateInterval: 15 minutes
|
|
915
|
+
|
|
916
|
+
**Development Settings:**
|
|
917
|
+
- Secure: false (allow HTTP)
|
|
918
|
+
- SameSite: 'lax' (easier testing)
|
|
919
|
+
- MaxAge: 24 hours (convenient for development)
|
|
920
|
+
- RegenerateInterval: 1 hour
|
|
921
|
+
|
|
922
|
+
|
|
881
923
|
---
|
|
882
924
|
|
|
883
925
|
## Security
|
|
@@ -987,350 +1029,2173 @@ class UsersController {
|
|
|
987
1029
|
- `detectSQLInjection(input)` - Detect SQL injection
|
|
988
1030
|
- `detectCommandInjection(input)` - Detect command injection
|
|
989
1031
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
## Components
|
|
1032
|
+
### File Upload Security
|
|
993
1033
|
|
|
994
|
-
|
|
1034
|
+
MasterController v1.3.1 includes built-in protection against file upload attacks and DoS.
|
|
995
1035
|
|
|
996
|
-
|
|
1036
|
+
#### Request Body Size Limits
|
|
997
1037
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
+
}
|
|
1013
1055
|
```
|
|
1014
1056
|
|
|
1015
|
-
|
|
1057
|
+
**DoS Protection:**
|
|
1058
|
+
- All request bodies are size-limited (prevents memory exhaustion)
|
|
1059
|
+
- Connections destroyed if limits exceeded
|
|
1060
|
+
- Configurable per content-type
|
|
1016
1061
|
|
|
1017
|
-
|
|
1018
|
-
// In config/initializers/config.js
|
|
1019
|
-
master.component('components', 'user');
|
|
1020
|
-
master.component('components', 'mail');
|
|
1021
|
-
```
|
|
1062
|
+
#### File Type Validation
|
|
1022
1063
|
|
|
1023
|
-
|
|
1064
|
+
**Always validate file types in your controllers:**
|
|
1024
1065
|
|
|
1025
1066
|
```javascript
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1067
|
+
class UploadController {
|
|
1068
|
+
uploadImage(obj) {
|
|
1069
|
+
const file = obj.params.formData.files.avatar[0];
|
|
1070
|
+
|
|
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
|
+
}
|
|
1029
1077
|
|
|
1030
|
-
|
|
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
|
+
}
|
|
1031
1084
|
|
|
1032
|
-
|
|
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
|
+
}
|
|
1033
1091
|
|
|
1034
|
-
|
|
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);
|
|
1035
1096
|
|
|
1036
|
-
|
|
1097
|
+
// 5. Move file
|
|
1098
|
+
fs.renameSync(file.filepath, uploadPath);
|
|
1037
1099
|
|
|
1038
|
-
|
|
1100
|
+
this.json({ success: true, filename: safeFilename });
|
|
1101
|
+
}
|
|
1039
1102
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1103
|
+
uploadDocument(obj) {
|
|
1104
|
+
const file = obj.params.formData.files.document[0];
|
|
1105
|
+
|
|
1106
|
+
// Allow PDF, DOC, DOCX only
|
|
1107
|
+
const allowedTypes = [
|
|
1108
|
+
'application/pdf',
|
|
1109
|
+
'application/msword',
|
|
1110
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
1111
|
+
];
|
|
1112
|
+
|
|
1113
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
1114
|
+
this.json({ error: 'Only PDF and Word documents allowed' });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Process upload...
|
|
1048
1119
|
}
|
|
1049
|
-
}
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1050
1122
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1123
|
+
#### Formidable Custom Filter
|
|
1124
|
+
|
|
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
|
+
}
|
|
1053
1133
|
```
|
|
1054
1134
|
|
|
1055
|
-
|
|
1135
|
+
**Note:** JSON doesn't support functions, so filters must be configured in code:
|
|
1056
1136
|
|
|
1057
1137
|
```javascript
|
|
1058
|
-
//
|
|
1059
|
-
master.
|
|
1138
|
+
// config/initializers/config.js
|
|
1139
|
+
const formidableOptions = master.env.request.formidable;
|
|
1060
1140
|
|
|
1061
|
-
//
|
|
1062
|
-
|
|
1141
|
+
// Add runtime filter for images only
|
|
1142
|
+
formidableOptions.filter = function({ name, originalFilename, mimetype }) {
|
|
1143
|
+
return mimetype && mimetype.startsWith('image/');
|
|
1144
|
+
};
|
|
1063
1145
|
|
|
1064
|
-
|
|
1065
|
-
master.
|
|
1146
|
+
master.request.init({
|
|
1147
|
+
...master.env.request,
|
|
1148
|
+
formidable: formidableOptions
|
|
1149
|
+
});
|
|
1066
1150
|
```
|
|
1067
1151
|
|
|
1068
|
-
|
|
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
|
|
1069
1164
|
|
|
1070
1165
|
```javascript
|
|
1071
|
-
|
|
1166
|
+
class UploadController {
|
|
1167
|
+
upload(obj) {
|
|
1168
|
+
const file = obj.params.formData.files.upload[0];
|
|
1072
1169
|
|
|
1073
|
-
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
// elapsed: 15000,
|
|
1088
|
-
// remaining: 15000
|
|
1089
|
-
// }
|
|
1090
|
-
// ]
|
|
1091
|
-
// }
|
|
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
|
+
}
|
|
1092
1184
|
```
|
|
1093
1185
|
|
|
1094
|
-
|
|
1186
|
+
---
|
|
1187
|
+
|
|
1188
|
+
## File Conversion & Binary Data
|
|
1189
|
+
|
|
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.
|
|
1191
|
+
|
|
1192
|
+
### Quick Start
|
|
1095
1193
|
|
|
1096
1194
|
```javascript
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1195
|
+
// Convert uploaded file to base64 for API response
|
|
1196
|
+
class UploadController {
|
|
1197
|
+
uploadImage(obj) {
|
|
1198
|
+
const file = obj.params.formData.files.image[0];
|
|
1199
|
+
|
|
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
|
+
});
|
|
1099
1205
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1206
|
+
this.json({
|
|
1207
|
+
success: true,
|
|
1208
|
+
imageData: base64 // Can be used directly in <img src="">
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1102
1212
|
```
|
|
1103
1213
|
|
|
1104
|
-
|
|
1214
|
+
### File to Base64
|
|
1105
1215
|
|
|
1106
|
-
|
|
1216
|
+
#### `master.tools.fileToBase64(filePathOrFile, options)`
|
|
1217
|
+
|
|
1218
|
+
Convert a file to base64 string (binary-safe for all file types).
|
|
1107
1219
|
|
|
1108
|
-
|
|
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)
|
|
1109
1225
|
|
|
1110
|
-
|
|
1226
|
+
**Returns:** Base64 string
|
|
1227
|
+
|
|
1228
|
+
**Examples:**
|
|
1111
1229
|
|
|
1112
1230
|
```javascript
|
|
1113
|
-
//
|
|
1114
|
-
master.
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
|
1118
1239
|
});
|
|
1240
|
+
|
|
1241
|
+
// Use in HTML email or response
|
|
1242
|
+
const html = `<img src="${dataURI}" alt="Avatar">`;
|
|
1243
|
+
|
|
1244
|
+
// Store in database
|
|
1245
|
+
await db.query('UPDATE users SET avatar = ? WHERE id = ?', [base64, userId]);
|
|
1119
1246
|
```
|
|
1120
1247
|
|
|
1121
|
-
|
|
1248
|
+
**Error Handling:**
|
|
1122
1249
|
|
|
1123
1250
|
```javascript
|
|
1124
|
-
|
|
1125
|
-
master.
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
});
|
|
1134
|
-
return;
|
|
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');
|
|
1135
1260
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1261
|
+
}
|
|
1262
|
+
```
|
|
1138
1263
|
|
|
1139
|
-
|
|
1140
|
-
class UsersController {
|
|
1141
|
-
async show(obj) {
|
|
1142
|
-
const userId = obj.params.userId;
|
|
1143
|
-
const user = await this.db.query('SELECT * FROM users WHERE id = ?', [userId]);
|
|
1264
|
+
---
|
|
1144
1265
|
|
|
1145
|
-
|
|
1146
|
-
master.errorRenderer.send(obj, 404, {
|
|
1147
|
-
message: `User #${userId} not found`,
|
|
1148
|
-
suggestions: [
|
|
1149
|
-
'Check the user ID',
|
|
1150
|
-
'Browse all users',
|
|
1151
|
-
'Search for the user by name'
|
|
1152
|
-
]
|
|
1153
|
-
});
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1266
|
+
### Base64 to File
|
|
1156
1267
|
|
|
1157
|
-
|
|
1268
|
+
#### `master.tools.base64ToFile(base64String, outputPath, options)`
|
|
1269
|
+
|
|
1270
|
+
Convert base64 string to a file on disk (binary-safe).
|
|
1271
|
+
|
|
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)
|
|
1278
|
+
|
|
1279
|
+
**Returns:** `{ success: true, filePath: outputPath, size: number }`
|
|
1280
|
+
|
|
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
|
+
});
|
|
1158
1301
|
}
|
|
1159
1302
|
}
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
### Error Templates
|
|
1163
1303
|
|
|
1164
|
-
|
|
1304
|
+
// Data URI with prefix (automatically handled)
|
|
1305
|
+
const dataURI = 'data:image/png;base64,iVBORw0KGgoAAAANS...';
|
|
1306
|
+
master.tools.base64ToFile(dataURI, './output.png');
|
|
1165
1307
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
├── 401.html # Unauthorized
|
|
1170
|
-
├── 403.html # Forbidden
|
|
1171
|
-
├── 404.html # Not Found
|
|
1172
|
-
├── 405.html # Method Not Allowed
|
|
1173
|
-
├── 422.html # Unprocessable Entity
|
|
1174
|
-
├── 429.html # Too Many Requests
|
|
1175
|
-
├── 500.html # Internal Server Error
|
|
1176
|
-
├── 502.html # Bad Gateway
|
|
1177
|
-
├── 503.html # Service Unavailable
|
|
1178
|
-
└── 504.html # Gateway Timeout
|
|
1308
|
+
// Pure base64 without prefix
|
|
1309
|
+
const pureBase64 = 'iVBORw0KGgoAAAANS...';
|
|
1310
|
+
master.tools.base64ToFile(pureBase64, './output.png');
|
|
1179
1311
|
```
|
|
1180
1312
|
|
|
1181
|
-
|
|
1313
|
+
---
|
|
1182
1314
|
|
|
1183
|
-
|
|
1184
|
-
<!DOCTYPE html>
|
|
1185
|
-
<html>
|
|
1186
|
-
<head>
|
|
1187
|
-
<title>{{title}} ({{statusCode}})</title>
|
|
1188
|
-
</head>
|
|
1189
|
-
<body>
|
|
1190
|
-
<h1>{{statusCode}} - {{title}}</h1>
|
|
1191
|
-
<p>{{message}}</p>
|
|
1315
|
+
### Buffer Operations
|
|
1192
1316
|
|
|
1193
|
-
|
|
1194
|
-
{{#if showStackTrace}}
|
|
1195
|
-
<pre>{{stack}}</pre>
|
|
1196
|
-
{{/if}}
|
|
1317
|
+
#### `master.tools.fileToBuffer(filePathOrFile, options)`
|
|
1197
1318
|
|
|
1198
|
-
|
|
1199
|
-
{{#each suggestions}}
|
|
1200
|
-
<li>{{this}}</li>
|
|
1201
|
-
{{/each}}
|
|
1202
|
-
</body>
|
|
1203
|
-
</html>
|
|
1204
|
-
```
|
|
1319
|
+
Convert file to Node.js Buffer (for in-memory processing).
|
|
1205
1320
|
|
|
1206
|
-
**
|
|
1207
|
-
- `
|
|
1208
|
-
- `
|
|
1209
|
-
- `
|
|
1210
|
-
- `{{code}}` - Error code
|
|
1211
|
-
- `{{stack}}` - Stack trace (development only)
|
|
1212
|
-
- `{{suggestions}}` - Array of suggestions
|
|
1213
|
-
- `{{environment}}` - Current environment
|
|
1321
|
+
**Parameters:**
|
|
1322
|
+
- `filePathOrFile`: File path string OR formidable file object
|
|
1323
|
+
- `options`:
|
|
1324
|
+
- `maxSize` (number) - Maximum file size (default: 10MB)
|
|
1214
1325
|
|
|
1215
|
-
|
|
1326
|
+
**Returns:** Node.js Buffer
|
|
1327
|
+
|
|
1328
|
+
**Examples:**
|
|
1216
1329
|
|
|
1217
1330
|
```javascript
|
|
1218
|
-
//
|
|
1219
|
-
master.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
});
|
|
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);
|
|
1230
1342
|
```
|
|
1231
1343
|
|
|
1232
|
-
|
|
1344
|
+
---
|
|
1233
1345
|
|
|
1234
|
-
|
|
1346
|
+
#### `master.tools.fileToBytes(filePathOrFile, options)`
|
|
1235
1347
|
|
|
1236
|
-
|
|
1237
|
-
// Browser request → HTML
|
|
1238
|
-
GET /users/999
|
|
1239
|
-
Accept: text/html
|
|
1240
|
-
→ Returns beautiful HTML error page
|
|
1348
|
+
Convert file to Uint8Array (for Web APIs and TypedArrays).
|
|
1241
1349
|
|
|
1242
|
-
|
|
1243
|
-
|
|
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>
|
|
2051
|
+
```
|
|
2052
|
+
|
|
2053
|
+
**Available Variables:**
|
|
2054
|
+
- `{{statusCode}}` - HTTP status code (404, 500, etc.)
|
|
2055
|
+
- `{{title}}` - Error title
|
|
2056
|
+
- `{{message}}` - Error message
|
|
2057
|
+
- `{{code}}` - Error code
|
|
2058
|
+
- `{{stack}}` - Stack trace (development only)
|
|
2059
|
+
- `{{suggestions}}` - Array of suggestions
|
|
2060
|
+
- `{{environment}}` - Current environment
|
|
2061
|
+
|
|
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
|
|
1244
2147
|
Accept: application/json
|
|
1245
2148
|
→ Returns JSON error response
|
|
1246
2149
|
{
|
|
1247
|
-
"error": "Page Not Found",
|
|
1248
|
-
"statusCode": 404,
|
|
1249
|
-
"code": "MC_HTTP_ERROR",
|
|
1250
|
-
"message": "The user you're looking for doesn't exist."
|
|
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
|
+
}
|
|
1251
2848
|
}
|
|
1252
2849
|
```
|
|
1253
2850
|
|
|
1254
|
-
|
|
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');
|
|
1255
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
|
+
```
|
|
2870
|
+
|
|
2871
|
+
**Method 2: Programmatic SNI**
|
|
1256
2872
|
```javascript
|
|
1257
|
-
|
|
1258
|
-
|
|
2873
|
+
const fs = require('fs');
|
|
2874
|
+
const tls = require('tls');
|
|
2875
|
+
const master = require('mastercontroller');
|
|
1259
2876
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
+
}
|
|
1266
2911
|
});
|
|
2912
|
+
|
|
2913
|
+
require('./config/initializers/config');
|
|
2914
|
+
master.start(server);
|
|
2915
|
+
master.serverSettings({ httpPort: 443 });
|
|
1267
2916
|
```
|
|
1268
2917
|
|
|
1269
|
-
|
|
2918
|
+
---
|
|
2919
|
+
|
|
2920
|
+
### HTTP to HTTPS Redirect (Secure)
|
|
2921
|
+
|
|
2922
|
+
**⚠️ SECURITY:** Always specify allowed hosts to prevent open redirect attacks!
|
|
1270
2923
|
|
|
1271
2924
|
```javascript
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
console.error('Database error:', error);
|
|
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
|
+
]);
|
|
1279
2931
|
|
|
1280
|
-
|
|
1281
|
-
message: 'Failed to load users',
|
|
1282
|
-
code: 'DB_ERROR',
|
|
1283
|
-
stack: error.stack
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
2932
|
+
console.log('✅ HTTP redirect server running on port 80');
|
|
1288
2933
|
```
|
|
1289
2934
|
|
|
1290
|
-
|
|
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
|
+
```
|
|
1291
2943
|
|
|
2944
|
+
**For Multiple Domains:**
|
|
1292
2945
|
```javascript
|
|
1293
|
-
|
|
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
|
+
```
|
|
1294
2955
|
|
|
1295
|
-
|
|
1296
|
-
logger.info({
|
|
1297
|
-
code: 'USER_LOGIN',
|
|
1298
|
-
message: 'User logged in',
|
|
1299
|
-
userId: user.id
|
|
1300
|
-
});
|
|
2956
|
+
---
|
|
1301
2957
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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"
|
|
2977
|
+
```
|
|
2978
|
+
|
|
2979
|
+
**Manual Renewal**
|
|
2980
|
+
```bash
|
|
2981
|
+
# Test renewal (dry run)
|
|
2982
|
+
sudo certbot renew --dry-run
|
|
2983
|
+
|
|
2984
|
+
# Actually renew certificates
|
|
2985
|
+
sudo certbot renew
|
|
2986
|
+
|
|
2987
|
+
# Restart your application
|
|
2988
|
+
pm2 restart myapp
|
|
2989
|
+
# or
|
|
2990
|
+
sudo systemctl restart myapp
|
|
2991
|
+
```
|
|
2992
|
+
|
|
2993
|
+
**Certificate Live Reload (Zero Downtime)**
|
|
2994
|
+
|
|
2995
|
+
MasterController supports certificate live reload with `fs.watchFile()`:
|
|
2996
|
+
|
|
2997
|
+
```javascript
|
|
2998
|
+
const fs = require('fs');
|
|
2999
|
+
const master = require('mastercontroller');
|
|
3000
|
+
|
|
3001
|
+
const certPath = '/etc/letsencrypt/live/example.com/fullchain.pem';
|
|
3002
|
+
const keyPath = '/etc/letsencrypt/live/example.com/privkey.pem';
|
|
3003
|
+
|
|
3004
|
+
let server = master.setupServer('https', {
|
|
3005
|
+
key: fs.readFileSync(keyPath),
|
|
3006
|
+
cert: fs.readFileSync(certPath)
|
|
1306
3007
|
});
|
|
1307
3008
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
+
}
|
|
1313
3028
|
});
|
|
3029
|
+
|
|
3030
|
+
require('./config/initializers/config');
|
|
3031
|
+
master.start(server);
|
|
3032
|
+
master.serverSettings({ httpPort: 443 });
|
|
1314
3033
|
```
|
|
1315
3034
|
|
|
1316
3035
|
---
|
|
1317
3036
|
|
|
1318
|
-
|
|
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/
|
|
1319
3091
|
|
|
1320
|
-
|
|
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
|
|
3095
|
+
|
|
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:**
|
|
1321
3109
|
|
|
1322
3110
|
```javascript
|
|
1323
|
-
|
|
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
|
|
3130
|
+
|
|
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:**
|
|
1324
3163
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
+
}
|
|
1328
3172
|
};
|
|
1329
3173
|
|
|
1330
|
-
|
|
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
|
+
});
|
|
1331
3193
|
```
|
|
1332
3194
|
|
|
1333
|
-
|
|
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
|
|
1334
3199
|
|
|
1335
3200
|
Configure TLS in `config/environments/env.production.json`:
|
|
1336
3201
|
|
|
@@ -1352,33 +3217,206 @@ Configure TLS in `config/environments/env.production.json`:
|
|
|
1352
3217
|
"keyPath": "/path/to/app.key",
|
|
1353
3218
|
"certPath": "/path/to/app.crt"
|
|
1354
3219
|
}
|
|
1355
|
-
}
|
|
3220
|
+
},
|
|
3221
|
+
"hsts": true,
|
|
3222
|
+
"hstsMaxAge": 31536000
|
|
1356
3223
|
}
|
|
1357
3224
|
}
|
|
1358
3225
|
}
|
|
1359
3226
|
```
|
|
1360
3227
|
|
|
1361
3228
|
```javascript
|
|
3229
|
+
// Loads TLS config from environment file
|
|
1362
3230
|
const server = master.setupServer('https');
|
|
1363
3231
|
master.serverSettings(master.env.server);
|
|
1364
3232
|
```
|
|
1365
3233
|
|
|
1366
|
-
|
|
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
|
|
1367
3267
|
|
|
1368
3268
|
```javascript
|
|
1369
|
-
//
|
|
1370
|
-
const
|
|
1371
|
-
|
|
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);
|
|
1372
3282
|
master.serverSettings({ httpPort: 443 });
|
|
1373
3283
|
|
|
1374
|
-
//
|
|
1375
|
-
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
|
+
]);
|
|
1376
3289
|
```
|
|
1377
3290
|
|
|
1378
|
-
###
|
|
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
|
|
1379
3338
|
|
|
1380
3339
|
```javascript
|
|
1381
|
-
|
|
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
|
|
1382
3420
|
```
|
|
1383
3421
|
|
|
1384
3422
|
---
|
|
@@ -1444,16 +3482,15 @@ master.enableHSTS(); // In production HTTPS
|
|
|
1444
3482
|
|
|
1445
3483
|
### Sessions
|
|
1446
3484
|
|
|
1447
|
-
- `master.
|
|
1448
|
-
- `master.
|
|
1449
|
-
- `master.
|
|
1450
|
-
- `master.
|
|
1451
|
-
- `master.
|
|
1452
|
-
- `master.
|
|
1453
|
-
- `master.
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
- `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)
|
|
1457
3494
|
|
|
1458
3495
|
### Request
|
|
1459
3496
|
|
|
@@ -1466,11 +3503,26 @@ master.enableHSTS(); // In production HTTPS
|
|
|
1466
3503
|
|
|
1467
3504
|
### Tools
|
|
1468
3505
|
|
|
1469
|
-
|
|
1470
|
-
- `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:**
|
|
1471
3520
|
- `master.tools.combineObjects(target, source)` - Merge objects
|
|
1472
3521
|
- `master.tools.makeWordId(length)` - Generate random ID
|
|
1473
3522
|
|
|
3523
|
+
**Deprecated:**
|
|
3524
|
+
- `master.tools.base64(path)` - ⚠️ DEPRECATED - Broken for binary files, use `fileToBase64()` instead
|
|
3525
|
+
|
|
1474
3526
|
---
|
|
1475
3527
|
|
|
1476
3528
|
## Production Tips
|
|
@@ -1490,14 +3542,17 @@ master.enableHSTS(); // In production HTTPS
|
|
|
1490
3542
|
|
|
1491
3543
|
## Documentation
|
|
1492
3544
|
|
|
1493
|
-
|
|
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
|
|
1494
3553
|
|
|
1495
|
-
- [
|
|
1496
|
-
- [
|
|
1497
|
-
- [HTTPS with Environment TLS & SNI](docs/server-setup-https-env-tls-sni.md)
|
|
1498
|
-
- [Hostname Binding](docs/server-setup-hostname-binding.md)
|
|
1499
|
-
- [Nginx Reverse Proxy](docs/server-setup-nginx-reverse-proxy.md)
|
|
1500
|
-
- [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
|
|
1501
3556
|
|
|
1502
3557
|
---
|
|
1503
3558
|
|