@reldens/server-utils 0.23.0 → 0.25.0

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/README.md CHANGED
@@ -40,7 +40,7 @@ A Node.js server toolkit providing secure application server creation, file hand
40
40
  - Password hashing using PBKDF2 with configurable iterations
41
41
  - Password validation against stored hashes
42
42
  - AES-256-GCM data encryption and decryption
43
- - Secure token generation with customizable length
43
+ - Secure token generation with a customizable length
44
44
  - TOTP (Time-based One-Time Password) generation
45
45
  - Data hashing with multiple algorithms (SHA-256, SHA-512, MD5)
46
46
  - HMAC generation and verification
@@ -245,8 +245,9 @@ let serverResult = appServerFactory.createAppServer({
245
245
  ### FileHandler Methods
246
246
 
247
247
  - `exists(path)` - Checks if file or folder exists
248
- - `createFolder(path)` - Creates folder with recursive option
248
+ - `createFolder(path)` - Creates folder with a recursive option
249
249
  - `remove(path)` - Removes file or folder recursively
250
+ - `removeMultiple(filePaths)` - Removes multiple files from an array of paths
250
251
  - `copyFile(source, destination)` - Copies file to destination
251
252
  - `copyFolderSync(source, destination)` - Copies folder recursively
252
253
  - `readFile(path)` - Reads file contents as string
@@ -254,14 +255,14 @@ let serverResult = appServerFactory.createAppServer({
254
255
  - `fetchFileJson(path)` - Reads and parses JSON file
255
256
  - `fetchFileContents(path)` - Reads file with validation
256
257
  - `updateFileContents(path, content)` - Updates existing file
257
- - `isFile(path)` - Checks if path is file
258
- - `isFolder(path)` - Checks if path is folder
258
+ - `isFile(path)` - Checks if a path is a file
259
+ - `isFolder(path)` - Checks if a path is folder
259
260
  - `getFilesInFolder(path, extensions)` - Lists files with optional filtering
260
261
  - `validateFileType(path, type, allowedTypes, maxSize)` - Validates file type and size
261
262
  - `detectFileType(path)` - Detects MIME type from file signature
262
263
  - `generateSecureFilename(originalName)` - Generates cryptographically secure filename
263
264
  - `quarantineFile(path, reason)` - Moves file to quarantine folder
264
- - `createTempFile(prefix, extension)` - Creates temporary file path
265
+ - `createTempFile(prefix, extension)` - Creates a temporary file path
265
266
 
266
267
  ### Encryptor Methods
267
268
 
@@ -281,9 +282,8 @@ let serverResult = appServerFactory.createAppServer({
281
282
 
282
283
  - `createUploader(fields, buckets, allowedTypes)` - Creates multer upload middleware
283
284
  - `validateFilenameSecurity(filename)` - Validates filename for security
284
- - `validateFile(file, allowedType, callback)` - Validates file during upload
285
+ - `validateFile(file, allowedType, callback)` - Validates a file during upload
285
286
  - `validateFileContents(file, allowedType)` - Validates file content after upload
286
- - `cleanupFiles(files)` - Removes uploaded files on error
287
287
 
288
288
  ## Security Features
289
289
 
@@ -291,7 +291,7 @@ let serverResult = appServerFactory.createAppServer({
291
291
  All file operations include comprehensive path validation to prevent directory traversal attacks and access to system files.
292
292
 
293
293
  ### Secure File Upload
294
- File uploads are validated at multiple levels including filename, MIME type, file extension, file size, and content validation using magic number detection.
294
+ File uploads are validated at multiple levels including filename, MIME type, file extension, file size, and content validation using magic number detection. Failed uploads are automatically cleaned up using efficient file removal.
295
295
 
296
296
  ### Rate Limiting
297
297
  Configurable rate limiting with development mode detection for appropriate thresholds in different environments.
@@ -22,6 +22,8 @@ class FileHandler
22
22
  'C:\\Windows\\', 'C:\\System32\\',
23
23
  '%2e%2e%2f', '%2e%2e%5c'
24
24
  ];
25
+ this.nativeHandler = fs;
26
+ this.nativePaths = path;
25
27
  }
26
28
 
27
29
  joinPaths(...args)
@@ -29,6 +31,24 @@ class FileHandler
29
31
  return path.join(...args);
30
32
  }
31
33
 
34
+ getFileName(filePath)
35
+ {
36
+ if(!this.isValidPath(filePath)){
37
+ this.error = {message: 'Invalid file path.', filePath};
38
+ return false;
39
+ }
40
+ return path.basename(filePath);
41
+ }
42
+
43
+ getFolderName(filePath)
44
+ {
45
+ if(!this.isValidPath(filePath)){
46
+ this.error = {message: 'Invalid file path.', filePath};
47
+ return false;
48
+ }
49
+ return path.dirname(filePath);
50
+ }
51
+
32
52
  exists(fullPath)
33
53
  {
34
54
  if(!this.isValidPath(fullPath)){
@@ -121,6 +141,20 @@ class FileHandler
121
141
  }
122
142
  }
123
143
 
144
+ removeMultiple(filePaths)
145
+ {
146
+ if(!Array.isArray(filePaths)){
147
+ this.error = {message: 'File paths must be an array'};
148
+ return false;
149
+ }
150
+ for(let filePath of filePaths){
151
+ if(!this.remove(filePath)){
152
+ return false;
153
+ }
154
+ }
155
+ return true;
156
+ }
157
+
124
158
  createFolder(folderPath)
125
159
  {
126
160
  if(!this.isValidPath(folderPath)){
@@ -507,6 +541,230 @@ class FileHandler
507
541
  return this.joinPaths(tempDir, fileName);
508
542
  }
509
543
 
544
+ moveFile(from, to)
545
+ {
546
+ if(!this.isValidPath(from) || !this.isValidPath(to)){
547
+ this.error = {message: 'Invalid path for file move.', from, to};
548
+ return false;
549
+ }
550
+ if(!this.exists(from)){
551
+ this.error = {message: 'Source file does not exist.', from};
552
+ return false;
553
+ }
554
+ try {
555
+ fs.renameSync(from, to);
556
+ return true;
557
+ } catch (error) {
558
+ this.error = {message: 'Failed to move file.', error, from, to};
559
+ return false;
560
+ }
561
+ }
562
+
563
+ getFileSize(filePath)
564
+ {
565
+ if(!this.isValidPath(filePath)){
566
+ this.error = {message: 'Invalid file path.', filePath};
567
+ return false;
568
+ }
569
+ if(!this.exists(filePath)){
570
+ this.error = {message: 'File does not exist.', filePath};
571
+ return false;
572
+ }
573
+ try {
574
+ return fs.statSync(filePath).size;
575
+ } catch (error) {
576
+ this.error = {message: 'Failed to get file size.', error, filePath};
577
+ return false;
578
+ }
579
+ }
580
+
581
+ compareFiles(file1, file2)
582
+ {
583
+ if(!this.isValidPath(file1) || !this.isValidPath(file2)){
584
+ this.error = {message: 'Invalid file paths for comparison.', file1, file2};
585
+ return false;
586
+ }
587
+ if(!this.exists(file1) || !this.exists(file2)){
588
+ this.error = {message: 'One or both files do not exist.', file1, file2};
589
+ return false;
590
+ }
591
+ try {
592
+ let content1 = this.readFile(file1);
593
+ let content2 = this.readFile(file2);
594
+ if(!content1 || !content2){
595
+ return false;
596
+ }
597
+ return content1 === content2;
598
+ } catch (error) {
599
+ this.error = {message: 'Failed to compare files.', error, file1, file2};
600
+ return false;
601
+ }
602
+ }
603
+
604
+ getRelativePath(from, to)
605
+ {
606
+ if(!this.isValidPath(from) || !this.isValidPath(to)){
607
+ this.error = {message: 'Invalid paths for relative calculation.', from, to};
608
+ return false;
609
+ }
610
+ try {
611
+ return path.relative(from, to);
612
+ } catch (error) {
613
+ this.error = {message: 'Failed to calculate relative path.', error, from, to};
614
+ return false;
615
+ }
616
+ }
617
+
618
+ isAbsolutePath(filePath)
619
+ {
620
+ if(!this.isValidPath(filePath)){
621
+ this.error = {message: 'Invalid file path.', filePath};
622
+ return false;
623
+ }
624
+ return path.isAbsolute(filePath);
625
+ }
626
+
627
+ normalizePath(filePath)
628
+ {
629
+ if(!filePath){
630
+ this.error = {message: 'Path cannot be empty.', filePath};
631
+ return false;
632
+ }
633
+ try {
634
+ return path.normalize(filePath);
635
+ } catch (error) {
636
+ this.error = {message: 'Failed to normalize path.', error, filePath};
637
+ return false;
638
+ }
639
+ }
640
+
641
+ walkDirectory(dirPath, callback)
642
+ {
643
+ if(!this.isValidPath(dirPath)){
644
+ this.error = {message: 'Invalid directory path.', dirPath};
645
+ return false;
646
+ }
647
+ if(!this.isFolder(dirPath)){
648
+ this.error = {message: 'Path is not a directory.', dirPath};
649
+ return false;
650
+ }
651
+ if('function' !== typeof callback){
652
+ this.error = {message: 'Callback must be a function.', dirPath};
653
+ return false;
654
+ }
655
+ try {
656
+ let items = this.readFolder(dirPath);
657
+ for(let item of items){
658
+ let itemPath = this.joinPaths(dirPath, item);
659
+ callback(itemPath);
660
+ if(this.isFolder(itemPath)){
661
+ this.walkDirectory(itemPath, callback);
662
+ }
663
+ }
664
+ return true;
665
+ } catch (error) {
666
+ this.error = {message: 'Failed to walk directory.', error, dirPath};
667
+ return false;
668
+ }
669
+ }
670
+
671
+ getDirectorySize(dirPath)
672
+ {
673
+ if(!this.isValidPath(dirPath)){
674
+ this.error = {message: 'Invalid directory path.', dirPath};
675
+ return false;
676
+ }
677
+ if(!this.isFolder(dirPath)){
678
+ this.error = {message: 'Path is not a directory.', dirPath};
679
+ return false;
680
+ }
681
+ let totalSize = 0;
682
+ let calculateSize = (itemPath) => {
683
+ if(this.isFile(itemPath)){
684
+ let size = this.getFileSize(itemPath);
685
+ if(false !== size){
686
+ totalSize += size;
687
+ }
688
+ }
689
+ };
690
+ let walkResult = this.walkDirectory(dirPath, calculateSize);
691
+ if(!walkResult){
692
+ return false;
693
+ }
694
+ return totalSize;
695
+ }
696
+
697
+ emptyDirectory(dirPath)
698
+ {
699
+ if(!this.isValidPath(dirPath)){
700
+ this.error = {message: 'Invalid directory path.', dirPath};
701
+ return false;
702
+ }
703
+ if(!this.isFolder(dirPath)){
704
+ this.error = {message: 'Path is not a directory.', dirPath};
705
+ return false;
706
+ }
707
+ try {
708
+ let items = this.readFolder(dirPath);
709
+ for(let item of items){
710
+ let itemPath = this.joinPaths(dirPath, item);
711
+ this.remove(itemPath);
712
+ }
713
+ return true;
714
+ } catch (error) {
715
+ this.error = {message: 'Failed to empty directory.', error, dirPath};
716
+ return false;
717
+ }
718
+ }
719
+
720
+ appendToFile(filePath, content)
721
+ {
722
+ if(!this.isValidPath(filePath)){
723
+ this.error = {message: 'Invalid file path.', filePath};
724
+ return false;
725
+ }
726
+ try {
727
+ fs.appendFileSync(filePath, content, this.encoding);
728
+ return true;
729
+ } catch (error) {
730
+ this.error = {message: 'Failed to append to file.', error, filePath};
731
+ return false;
732
+ }
733
+ }
734
+
735
+ prependToFile(filePath, content)
736
+ {
737
+ if(!this.isValidPath(filePath)){
738
+ this.error = {message: 'Invalid file path.', filePath};
739
+ return false;
740
+ }
741
+ if(!this.exists(filePath)){
742
+ return this.writeFile(filePath, content);
743
+ }
744
+ let existingContent = this.readFile(filePath);
745
+ if(!existingContent){
746
+ return false;
747
+ }
748
+ return this.writeFile(filePath, content + existingContent);
749
+ }
750
+
751
+ replaceInFile(filePath, searchValue, replaceValue)
752
+ {
753
+ if(!this.isValidPath(filePath)){
754
+ this.error = {message: 'Invalid file path.', filePath};
755
+ return false;
756
+ }
757
+ if(!this.exists(filePath)){
758
+ this.error = {message: 'File does not exist.', filePath};
759
+ return false;
760
+ }
761
+ let content = this.readFile(filePath);
762
+ if(!content){
763
+ return false;
764
+ }
765
+ return this.writeFile(filePath, content.replace(searchValue, replaceValue));
766
+ }
767
+
510
768
  }
511
769
 
512
770
  module.exports.FileHandler = new FileHandler();
@@ -31,32 +31,36 @@ class UploaderFactory
31
31
  this.error = {message: 'Invalid uploader configuration: ' + this.error.message};
32
32
  return false;
33
33
  }
34
- let diskStorageConfiguration = {
34
+ let storage = multer.diskStorage({
35
35
  destination: (req, file, cb) => {
36
- let dest = buckets[file.fieldname];
37
- if(!FileHandler.isValidPath(dest)){
38
- return cb(new Error('Invalid destination path'));
36
+ try{
37
+ let dest = buckets[file.fieldname];
38
+ if(!FileHandler.isValidPath(dest)){
39
+ return cb(new Error('Invalid destination path'));
40
+ }
41
+ FileHandler.createFolder(dest);
42
+ cb(null, dest);
43
+ } catch(error){
44
+ this.error = {message: 'Cannot prepare destination.', error: error};
45
+ cb(error);
39
46
  }
40
- FileHandler.createFolder(dest);
41
- cb(null, dest);
42
- }
43
- };
44
- diskStorageConfiguration.filename = (req, file, cb) => {
45
- if(!this.validateFilenameSecurity(file.originalname)){
46
- return cb(new Error('Invalid filename'));
47
- }
48
- if(!this.applySecureFileNames) {
49
- cb(null, file.originalname);
50
- return;
51
- }
52
- let secureFilename = FileHandler.generateSecureFilename(file.originalname);
53
- if(!req.fileNameMapping){
54
- req.fileNameMapping = {};
47
+ },
48
+ filename: (req, file, cb) => {
49
+ if(!this.validateFilenameSecurity(file.originalname)){
50
+ return cb(new Error('Invalid filename'));
51
+ }
52
+ if(!this.applySecureFileNames) {
53
+ cb(null, file.originalname);
54
+ return;
55
+ }
56
+ let secureFilename = FileHandler.generateSecureFilename(file.originalname);
57
+ if(!req.fileNameMapping){
58
+ req.fileNameMapping = {};
59
+ }
60
+ req.fileNameMapping[secureFilename] = file.originalname;
61
+ cb(null, secureFilename);
55
62
  }
56
- req.fileNameMapping[secureFilename] = file.originalname;
57
- cb(null, secureFilename);
58
- };
59
- let storage = multer.diskStorage(diskStorageConfiguration);
63
+ });
60
64
  let limits = {
61
65
  fileSize: this.maxFileSize
62
66
  };
@@ -74,14 +78,14 @@ class UploaderFactory
74
78
  upload.fields(fields)(req, res, async (multerError) => {
75
79
  if(multerError){
76
80
  if(multerError instanceof multer.MulterError){
77
- if(multerError.code === 'LIMIT_FILE_SIZE'){
81
+ if('LIMIT_FILE_SIZE' === multerError.code){
78
82
  let messageFile = 'File too large.';
79
83
  if('function' === typeof this.processErrorResponse){
80
84
  return this.processErrorResponse(413, messageFile, req, res);
81
85
  }
82
86
  return res.status(413).send(messageFile);
83
87
  }
84
- if(multerError.code === 'LIMIT_FILE_COUNT'){
88
+ if('LIMIT_FILE_COUNT' === multerError.code){
85
89
  let messageTooMany = 'Too many files.';
86
90
  if('function' === typeof this.processErrorResponse){
87
91
  return this.processErrorResponse(413, messageTooMany, req, res);
@@ -94,19 +98,19 @@ class UploaderFactory
94
98
  }
95
99
  return res.status(400).send(messageUpload);
96
100
  }
97
- let messageServer = 'Server error during file upload.';
101
+ let messageServer = this.error && this.error.message ? this.error.message : 'Server error during file upload.';
98
102
  if('function' === typeof this.processErrorResponse){
99
- return this.processErrorResponse(500, messageServer, req, res);
103
+ return this.processErrorResponse(415, messageServer, req, res);
100
104
  }
101
- return res.status(500).send(messageServer);
105
+ return res.status(415).send(messageServer);
102
106
  }
103
107
  if(!req.files){
104
108
  return next();
105
109
  }
106
110
  let validationResult = await this.validateAllUploadedFiles(req, allowedFileTypes);
107
111
  if(!validationResult){
108
- this.cleanupFiles(req.files);
109
- let messageContents = 'File validation failed.';
112
+ FileHandler.removeMultiple(Object.values(req.files).flat().map(file => file.path));
113
+ let messageContents = this.error && this.error.message ? this.error.message : 'File validation failed.';
110
114
  if('function' === typeof this.processErrorResponse){
111
115
  return this.processErrorResponse(415, messageContents, req, res);
112
116
  }
@@ -190,17 +194,17 @@ class UploaderFactory
190
194
  validateFile(file, allowedFileType, cb)
191
195
  {
192
196
  if(!allowedFileType){
193
- return cb();
197
+ return cb(null, true);
194
198
  }
195
199
  if(!this.validateFilenameSecurity(file.originalname)){
196
- this.error = {message: 'Insecure filename: ' + file.originalname};
197
- return cb(new Error('Insecure filename: ' + file.originalname));
200
+ this.error = {message: 'Insecure filename: '+file.originalname};
201
+ return cb(new Error('Insecure filename: '+file.originalname));
198
202
  }
199
203
  let fileExtension = FileHandler.extension(file.originalname).toLowerCase();
200
- let allowedExtensions = this.allowedExtensions[allowedFileType];
204
+ let allowedExtensions = this.allowedExtensions && this.allowedExtensions[allowedFileType];
201
205
  if(allowedExtensions && !allowedExtensions.includes(fileExtension)){
202
- this.error = {message: 'Invalid file extension: ' + fileExtension};
203
- return cb(new Error('Invalid file extension: ' + fileExtension));
206
+ this.error = {message: 'Invalid file extension: '+fileExtension};
207
+ return cb(new Error('Invalid file extension: '+fileExtension));
204
208
  }
205
209
  let allowedFileTypeRegex = this.convertToRegex(allowedFileType);
206
210
  if(!allowedFileTypeRegex){
@@ -209,10 +213,10 @@ class UploaderFactory
209
213
  }
210
214
  let mimeTypeValid = allowedFileTypeRegex.test(file.mimetype);
211
215
  if(!mimeTypeValid){
212
- this.error = {message: 'Invalid MIME type: ' + file.mimetype};
213
- return cb(new Error('Invalid MIME type: ' + file.mimetype));
216
+ this.error = {message: 'Invalid MIME type: '+file.mimetype};
217
+ return cb(new Error('Invalid MIME type: '+file.mimetype));
214
218
  }
215
- return cb();
219
+ return cb(null, true);
216
220
  }
217
221
 
218
222
  async validateFileContents(file, allowedFileType)
@@ -246,17 +250,6 @@ class UploaderFactory
246
250
  return new RegExp(types.join('|'));
247
251
  }
248
252
 
249
- cleanupFiles(files)
250
- {
251
- if(!files){
252
- return;
253
- }
254
- for(let fieldName in files){
255
- for(let file of files[fieldName]){
256
- FileHandler.remove(file.path);
257
- }
258
- }
259
- }
260
253
  }
261
254
 
262
255
  module.exports.UploaderFactory = UploaderFactory;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/server-utils",
3
3
  "scope": "@reldens",
4
- "version": "0.23.0",
4
+ "version": "0.25.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",