mastercontroller 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +3 -1
- package/MasterAction.js +137 -23
- package/MasterActionFilters.js +197 -92
- package/MasterControl.js +264 -45
- package/MasterHtml.js +226 -143
- package/MasterRequest.js +202 -24
- package/MasterSocket.js +6 -1
- package/MasterTools.js +388 -0
- package/README.md +2288 -369
- package/SECURITY-FIXES-v1.3.2.md +614 -0
- package/docs/SECURITY-AUDIT-ACTION-SYSTEM.md +1374 -0
- package/docs/SECURITY-AUDIT-HTTPS.md +1056 -0
- package/docs/SECURITY-QUICKSTART.md +375 -0
- package/docs/timeout-and-error-handling.md +8 -6
- package/package.json +1 -1
- package/security/SecurityEnforcement.js +241 -0
- package/security/SessionSecurity.js +3 -2
- package/test/security/filters.test.js +276 -0
- package/test/security/https.test.js +214 -0
- package/test/security/path-traversal.test.js +222 -0
- package/test/security/xss.test.js +190 -0
- package/MasterSession.js +0 -208
- package/docs/server-setup-hostname-binding.md +0 -24
- package/docs/server-setup-http.md +0 -32
- package/docs/server-setup-https-credentials.md +0 -32
- package/docs/server-setup-https-env-tls-sni.md +0 -62
- package/docs/server-setup-nginx-reverse-proxy.md +0 -46
package/MasterTools.js
CHANGED
|
@@ -101,7 +101,24 @@ class MasterTools{
|
|
|
101
101
|
return sha.digest('hex');
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* @deprecated This custom base64 implementation ONLY works for TEXT strings, NOT binary files.
|
|
106
|
+
* For binary files (images, PDFs, videos), use Node.js Buffer API or the new file conversion methods below.
|
|
107
|
+
* This method will be removed in v2.0.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* // ❌ WRONG - Corrupts binary files
|
|
111
|
+
* const base64 = tools.base64().encode(binaryData);
|
|
112
|
+
*
|
|
113
|
+
* // ✅ CORRECT - Use Node.js Buffer
|
|
114
|
+
* const base64 = Buffer.from(binaryData).toString('base64');
|
|
115
|
+
*
|
|
116
|
+
* // ✅ CORRECT - Use new helper methods
|
|
117
|
+
* const base64 = tools.fileToBase64('/path/to/file.jpg');
|
|
118
|
+
*/
|
|
104
119
|
base64(){
|
|
120
|
+
console.warn('[DEPRECATED] MasterTools.base64() only works for TEXT strings, not binary files. Use Buffer.toString("base64") or tools.fileToBase64() instead. This method will be removed in v2.0.');
|
|
121
|
+
|
|
105
122
|
var $that = this;
|
|
106
123
|
return {
|
|
107
124
|
encode: function(string){
|
|
@@ -159,6 +176,377 @@ class MasterTools{
|
|
|
159
176
|
}
|
|
160
177
|
};
|
|
161
178
|
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// FILE CONVERSION UTILITIES (Production-Grade)
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Convert file to base64 string (binary-safe)
|
|
185
|
+
*
|
|
186
|
+
* @param {String|Object} filePathOrFile - File path or formidable file object
|
|
187
|
+
* @param {Object} options - Conversion options
|
|
188
|
+
* @param {Number} options.maxSize - Maximum file size in bytes (default: 10MB)
|
|
189
|
+
* @param {Boolean} options.includeDataURI - Include data URI prefix (default: false)
|
|
190
|
+
* @returns {String} Base64 encoded string
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* // Convert uploaded file to base64
|
|
194
|
+
* const file = obj.params.formData.files.image[0];
|
|
195
|
+
* const base64 = master.tools.fileToBase64(file);
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* // Convert file by path with data URI
|
|
199
|
+
* const base64 = master.tools.fileToBase64('/path/to/image.jpg', {
|
|
200
|
+
* includeDataURI: true,
|
|
201
|
+
* maxSize: 5 * 1024 * 1024 // 5MB limit
|
|
202
|
+
* });
|
|
203
|
+
* // Returns: "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
|
|
204
|
+
*/
|
|
205
|
+
fileToBase64(filePathOrFile, options = {}) {
|
|
206
|
+
const fs = require('fs');
|
|
207
|
+
const path = require('path');
|
|
208
|
+
|
|
209
|
+
// Extract file path from formidable file object or use as-is
|
|
210
|
+
const filepath = typeof filePathOrFile === 'object' && filePathOrFile.filepath
|
|
211
|
+
? filePathOrFile.filepath
|
|
212
|
+
: filePathOrFile;
|
|
213
|
+
|
|
214
|
+
// Validate file path
|
|
215
|
+
if (!filepath || typeof filepath !== 'string') {
|
|
216
|
+
throw new Error('Invalid file path provided');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!fs.existsSync(filepath)) {
|
|
220
|
+
throw new Error(`File not found: ${filepath}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get file stats
|
|
224
|
+
const stats = fs.statSync(filepath);
|
|
225
|
+
|
|
226
|
+
// Check file size
|
|
227
|
+
const maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB default
|
|
228
|
+
if (stats.size > maxSize) {
|
|
229
|
+
throw new Error(`File size (${stats.size} bytes) exceeds maximum (${maxSize} bytes). Use streaming for large files.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Security: Check if it's actually a file
|
|
233
|
+
if (!stats.isFile()) {
|
|
234
|
+
throw new Error('Path is not a file');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Read file as binary buffer
|
|
239
|
+
const buffer = fs.readFileSync(filepath);
|
|
240
|
+
|
|
241
|
+
// Convert to base64
|
|
242
|
+
const base64 = buffer.toString('base64');
|
|
243
|
+
|
|
244
|
+
// Include data URI if requested
|
|
245
|
+
if (options.includeDataURI) {
|
|
246
|
+
const mimetype = filePathOrFile.mimetype || this._getMimeTypeFromPath(filepath);
|
|
247
|
+
return `data:${mimetype};base64,${base64}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return base64;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
throw new Error(`Failed to convert file to base64: ${error.message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Convert base64 string to file (binary-safe)
|
|
258
|
+
*
|
|
259
|
+
* @param {String} base64String - Base64 encoded string (with or without data URI)
|
|
260
|
+
* @param {String} outputPath - Output file path
|
|
261
|
+
* @param {Object} options - Conversion options
|
|
262
|
+
* @param {Boolean} options.overwrite - Overwrite existing file (default: false)
|
|
263
|
+
* @returns {Object} File information {path, size, mimetype}
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* const fileInfo = master.tools.base64ToFile(
|
|
267
|
+
* 'data:image/jpeg;base64,/9j/4AAQSkZJRg...',
|
|
268
|
+
* '/path/to/output.jpg',
|
|
269
|
+
* { overwrite: true }
|
|
270
|
+
* );
|
|
271
|
+
* console.log(fileInfo);
|
|
272
|
+
* // { path: '/path/to/output.jpg', size: 51234, mimetype: 'image/jpeg' }
|
|
273
|
+
*/
|
|
274
|
+
base64ToFile(base64String, outputPath, options = {}) {
|
|
275
|
+
const fs = require('fs');
|
|
276
|
+
const path = require('path');
|
|
277
|
+
|
|
278
|
+
// Validate inputs
|
|
279
|
+
if (!base64String || typeof base64String !== 'string') {
|
|
280
|
+
throw new Error('Invalid base64 string provided');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!outputPath || typeof outputPath !== 'string') {
|
|
284
|
+
throw new Error('Invalid output path provided');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if file exists
|
|
288
|
+
if (fs.existsSync(outputPath) && !options.overwrite) {
|
|
289
|
+
throw new Error(`File already exists: ${outputPath}. Set overwrite: true to replace.`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Extract mimetype and base64 data from data URI if present
|
|
293
|
+
let mimetype = null;
|
|
294
|
+
let base64Data = base64String;
|
|
295
|
+
|
|
296
|
+
const dataURIMatch = base64String.match(/^data:([^;]+);base64,(.+)$/);
|
|
297
|
+
if (dataURIMatch) {
|
|
298
|
+
mimetype = dataURIMatch[1];
|
|
299
|
+
base64Data = dataURIMatch[2];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate base64 format
|
|
303
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data)) {
|
|
304
|
+
throw new Error('Invalid base64 string format');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Convert base64 to buffer
|
|
309
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
310
|
+
|
|
311
|
+
// Ensure output directory exists
|
|
312
|
+
const dir = path.dirname(outputPath);
|
|
313
|
+
if (!fs.existsSync(dir)) {
|
|
314
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Write buffer to file
|
|
318
|
+
fs.writeFileSync(outputPath, buffer);
|
|
319
|
+
|
|
320
|
+
// Get file stats
|
|
321
|
+
const stats = fs.statSync(outputPath);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
path: outputPath,
|
|
325
|
+
size: stats.size,
|
|
326
|
+
mimetype: mimetype || this._getMimeTypeFromPath(outputPath)
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw new Error(`Failed to convert base64 to file: ${error.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Convert file to Node.js Buffer (binary-safe)
|
|
335
|
+
*
|
|
336
|
+
* @param {String|Object} filePathOrFile - File path or formidable file object
|
|
337
|
+
* @param {Object} options - Conversion options
|
|
338
|
+
* @param {Number} options.maxSize - Maximum file size in bytes (default: 10MB)
|
|
339
|
+
* @returns {Buffer} Node.js Buffer containing file data
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* const file = obj.params.formData.files.document[0];
|
|
343
|
+
* const buffer = master.tools.fileToBuffer(file);
|
|
344
|
+
* console.log(buffer.length); // File size in bytes
|
|
345
|
+
*/
|
|
346
|
+
fileToBuffer(filePathOrFile, options = {}) {
|
|
347
|
+
const fs = require('fs');
|
|
348
|
+
|
|
349
|
+
const filepath = typeof filePathOrFile === 'object' && filePathOrFile.filepath
|
|
350
|
+
? filePathOrFile.filepath
|
|
351
|
+
: filePathOrFile;
|
|
352
|
+
|
|
353
|
+
if (!filepath || typeof filepath !== 'string') {
|
|
354
|
+
throw new Error('Invalid file path provided');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!fs.existsSync(filepath)) {
|
|
358
|
+
throw new Error(`File not found: ${filepath}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const stats = fs.statSync(filepath);
|
|
362
|
+
const maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB default
|
|
363
|
+
|
|
364
|
+
if (stats.size > maxSize) {
|
|
365
|
+
throw new Error(`File size (${stats.size} bytes) exceeds maximum (${maxSize} bytes)`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!stats.isFile()) {
|
|
369
|
+
throw new Error('Path is not a file');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
return fs.readFileSync(filepath);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(`Failed to read file to buffer: ${error.message}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Convert file to Uint8Array byte array (binary-safe)
|
|
381
|
+
*
|
|
382
|
+
* @param {String|Object} filePathOrFile - File path or formidable file object
|
|
383
|
+
* @param {Object} options - Conversion options
|
|
384
|
+
* @param {Number} options.maxSize - Maximum file size in bytes (default: 10MB)
|
|
385
|
+
* @returns {Uint8Array} Byte array
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* const file = obj.params.formData.files.data[0];
|
|
389
|
+
* const bytes = master.tools.fileToBytes(file);
|
|
390
|
+
* console.log(bytes[0]); // First byte
|
|
391
|
+
* console.log(bytes.length); // Total bytes
|
|
392
|
+
*/
|
|
393
|
+
fileToBytes(filePathOrFile, options = {}) {
|
|
394
|
+
const buffer = this.fileToBuffer(filePathOrFile, options);
|
|
395
|
+
return new Uint8Array(buffer);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Convert Buffer or Uint8Array to base64 string
|
|
400
|
+
*
|
|
401
|
+
* @param {Buffer|Uint8Array} bytes - Binary data
|
|
402
|
+
* @returns {String} Base64 encoded string
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* const buffer = fs.readFileSync('/path/to/file.pdf');
|
|
406
|
+
* const base64 = master.tools.bytesToBase64(buffer);
|
|
407
|
+
*/
|
|
408
|
+
bytesToBase64(bytes) {
|
|
409
|
+
if (!bytes) {
|
|
410
|
+
throw new Error('Invalid bytes provided');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
// Convert Uint8Array to Buffer if needed
|
|
415
|
+
const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
416
|
+
return buffer.toString('base64');
|
|
417
|
+
} catch (error) {
|
|
418
|
+
throw new Error(`Failed to convert bytes to base64: ${error.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Convert base64 string to Buffer
|
|
424
|
+
*
|
|
425
|
+
* @param {String} base64String - Base64 encoded string (with or without data URI)
|
|
426
|
+
* @returns {Buffer} Node.js Buffer
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* const base64 = 'SGVsbG8gV29ybGQ=';
|
|
430
|
+
* const buffer = master.tools.base64ToBytes(base64);
|
|
431
|
+
* console.log(buffer.toString('utf8')); // "Hello World"
|
|
432
|
+
*/
|
|
433
|
+
base64ToBytes(base64String) {
|
|
434
|
+
if (!base64String || typeof base64String !== 'string') {
|
|
435
|
+
throw new Error('Invalid base64 string provided');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Strip data URI prefix if present
|
|
439
|
+
let base64Data = base64String;
|
|
440
|
+
const dataURIMatch = base64String.match(/^data:[^;]+;base64,(.+)$/);
|
|
441
|
+
if (dataURIMatch) {
|
|
442
|
+
base64Data = dataURIMatch[1];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate base64 format
|
|
446
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data)) {
|
|
447
|
+
throw new Error('Invalid base64 string format');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
return Buffer.from(base64Data, 'base64');
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new Error(`Failed to convert base64 to bytes: ${error.message}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Stream large file to base64 (for files > 10MB)
|
|
459
|
+
* Returns a Promise that resolves with base64 string
|
|
460
|
+
*
|
|
461
|
+
* @param {String|Object} filePathOrFile - File path or formidable file object
|
|
462
|
+
* @param {Object} options - Streaming options
|
|
463
|
+
* @param {Function} options.onProgress - Progress callback (percent: number) => void
|
|
464
|
+
* @returns {Promise<String>} Base64 encoded string
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* // Stream 500MB video file
|
|
468
|
+
* const base64 = await master.tools.streamFileToBase64('/path/to/video.mp4', {
|
|
469
|
+
* onProgress: (percent) => console.log(`${percent}% complete`)
|
|
470
|
+
* });
|
|
471
|
+
*/
|
|
472
|
+
async streamFileToBase64(filePathOrFile, options = {}) {
|
|
473
|
+
const fs = require('fs');
|
|
474
|
+
const { Transform } = require('stream');
|
|
475
|
+
|
|
476
|
+
const filepath = typeof filePathOrFile === 'object' && filePathOrFile.filepath
|
|
477
|
+
? filePathOrFile.filepath
|
|
478
|
+
: filePathOrFile;
|
|
479
|
+
|
|
480
|
+
if (!filepath || !fs.existsSync(filepath)) {
|
|
481
|
+
throw new Error(`File not found: ${filepath}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const stats = fs.statSync(filepath);
|
|
485
|
+
if (!stats.isFile()) {
|
|
486
|
+
throw new Error('Path is not a file');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return new Promise((resolve, reject) => {
|
|
490
|
+
const chunks = [];
|
|
491
|
+
let bytesRead = 0;
|
|
492
|
+
|
|
493
|
+
const readStream = fs.createReadStream(filepath);
|
|
494
|
+
|
|
495
|
+
readStream.on('data', (chunk) => {
|
|
496
|
+
chunks.push(chunk);
|
|
497
|
+
bytesRead += chunk.length;
|
|
498
|
+
|
|
499
|
+
// Progress callback
|
|
500
|
+
if (options.onProgress && typeof options.onProgress === 'function') {
|
|
501
|
+
const percent = Math.round((bytesRead / stats.size) * 100);
|
|
502
|
+
options.onProgress(percent);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
readStream.on('end', () => {
|
|
507
|
+
try {
|
|
508
|
+
const buffer = Buffer.concat(chunks);
|
|
509
|
+
const base64 = buffer.toString('base64');
|
|
510
|
+
resolve(base64);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
reject(new Error(`Failed to convert stream to base64: ${error.message}`));
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
readStream.on('error', (error) => {
|
|
517
|
+
reject(new Error(`Stream error: ${error.message}`));
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get MIME type from file path
|
|
524
|
+
* @private
|
|
525
|
+
*/
|
|
526
|
+
_getMimeTypeFromPath(filepath) {
|
|
527
|
+
const path = require('path');
|
|
528
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
529
|
+
|
|
530
|
+
const mimeTypes = {
|
|
531
|
+
'.jpg': 'image/jpeg',
|
|
532
|
+
'.jpeg': 'image/jpeg',
|
|
533
|
+
'.png': 'image/png',
|
|
534
|
+
'.gif': 'image/gif',
|
|
535
|
+
'.webp': 'image/webp',
|
|
536
|
+
'.svg': 'image/svg+xml',
|
|
537
|
+
'.pdf': 'application/pdf',
|
|
538
|
+
'.txt': 'text/plain',
|
|
539
|
+
'.json': 'application/json',
|
|
540
|
+
'.xml': 'application/xml',
|
|
541
|
+
'.zip': 'application/zip',
|
|
542
|
+
'.mp4': 'video/mp4',
|
|
543
|
+
'.mp3': 'audio/mpeg',
|
|
544
|
+
'.wav': 'audio/wav'
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
548
|
+
}
|
|
549
|
+
|
|
162
550
|
combineObjandArray(data, objParams){
|
|
163
551
|
|
|
164
552
|
if(Array.isArray(data) === false){
|