@reldens/server-utils 0.24.0 → 0.26.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.
@@ -141,6 +141,20 @@ class FileHandler
141
141
  }
142
142
  }
143
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
+
144
158
  createFolder(folderPath)
145
159
  {
146
160
  if(!this.isValidPath(folderPath)){
@@ -25,38 +25,55 @@ class UploaderFactory
25
25
  this.maxFilenameLength = props.maxFilenameLength || 255;
26
26
  }
27
27
 
28
+ setError(message, additionalData = {})
29
+ {
30
+ if(!this.error.message){
31
+ this.error = {message, ...additionalData};
32
+ }
33
+ }
34
+
28
35
  createUploader(fields, buckets, allowedFileTypes)
29
36
  {
30
37
  if(!this.validateInputs(fields, buckets, allowedFileTypes)){
31
- this.error = {message: 'Invalid uploader configuration: ' + this.error.message};
38
+ this.setError('Invalid uploader configuration: ' + this.error.message);
32
39
  return false;
33
40
  }
34
- let diskStorageConfiguration = {
41
+ let storage = multer.diskStorage({
35
42
  destination: (req, file, cb) => {
36
- let dest = buckets[file.fieldname];
37
- if(!FileHandler.isValidPath(dest)){
38
- return cb(new Error('Invalid destination path'));
43
+ try{
44
+ let dest = buckets[file.fieldname];
45
+ if(!FileHandler.isValidPath(dest)){
46
+ this.setError('Invalid destination path', {dest, fieldname: file.fieldname});
47
+ return cb(new Error('Invalid destination path'));
48
+ }
49
+ let folderCreated = FileHandler.createFolder(dest);
50
+ if(!folderCreated){
51
+ this.setError('Cannot create destination folder', {dest, fileHandlerError: FileHandler.error});
52
+ return cb(new Error('Cannot create destination folder'));
53
+ }
54
+ cb(null, dest);
55
+ } catch(error){
56
+ this.setError('Cannot prepare destination.', {error: error});
57
+ cb(error);
39
58
  }
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 = {};
59
+ },
60
+ filename: (req, file, cb) => {
61
+ if(!this.validateFilenameSecurity(file.originalname)){
62
+ this.setError('Invalid filename', {originalname: file.originalname});
63
+ return cb(new Error('Invalid filename'));
64
+ }
65
+ if(!this.applySecureFileNames) {
66
+ cb(null, file.originalname);
67
+ return;
68
+ }
69
+ let secureFilename = FileHandler.generateSecureFilename(file.originalname);
70
+ if(!req.fileNameMapping){
71
+ req.fileNameMapping = {};
72
+ }
73
+ req.fileNameMapping[secureFilename] = file.originalname;
74
+ cb(null, secureFilename);
55
75
  }
56
- req.fileNameMapping[secureFilename] = file.originalname;
57
- cb(null, secureFilename);
58
- };
59
- let storage = multer.diskStorage(diskStorageConfiguration);
76
+ });
60
77
  let limits = {
61
78
  fileSize: this.maxFileSize
62
79
  };
@@ -74,39 +91,44 @@ class UploaderFactory
74
91
  upload.fields(fields)(req, res, async (multerError) => {
75
92
  if(multerError){
76
93
  if(multerError instanceof multer.MulterError){
77
- if(multerError.code === 'LIMIT_FILE_SIZE'){
94
+ if('LIMIT_FILE_SIZE' === multerError.code){
78
95
  let messageFile = 'File too large.';
96
+ this.setError(messageFile, {multerError});
79
97
  if('function' === typeof this.processErrorResponse){
80
98
  return this.processErrorResponse(413, messageFile, req, res);
81
99
  }
82
100
  return res.status(413).send(messageFile);
83
101
  }
84
- if(multerError.code === 'LIMIT_FILE_COUNT'){
102
+ if('LIMIT_FILE_COUNT' === multerError.code){
85
103
  let messageTooMany = 'Too many files.';
104
+ this.setError(messageTooMany, {multerError});
86
105
  if('function' === typeof this.processErrorResponse){
87
106
  return this.processErrorResponse(413, messageTooMany, req, res);
88
107
  }
89
108
  return res.status(413).send(messageTooMany);
90
109
  }
91
110
  let messageUpload = 'File upload error.';
111
+ this.setError(messageUpload, {multerError});
92
112
  if('function' === typeof this.processErrorResponse){
93
113
  return this.processErrorResponse(400, messageUpload, multerError, req, res);
94
114
  }
95
115
  return res.status(400).send(messageUpload);
96
116
  }
97
- let messageServer = 'Server error during file upload.';
117
+ let messageServer = this.error?.message ? this.error.message : 'Server error during file upload.';
118
+ this.setError(messageServer, {multerError});
98
119
  if('function' === typeof this.processErrorResponse){
99
- return this.processErrorResponse(500, messageServer, req, res);
120
+ return this.processErrorResponse(415, messageServer, req, res);
100
121
  }
101
- return res.status(500).send(messageServer);
122
+ return res.status(415).send(messageServer);
102
123
  }
103
124
  if(!req.files){
104
125
  return next();
105
126
  }
106
127
  let validationResult = await this.validateAllUploadedFiles(req, allowedFileTypes);
107
128
  if(!validationResult){
108
- this.cleanupFiles(req.files);
109
- let messageContents = 'File validation failed.';
129
+ let filePaths = Object.values(req.files).flat().map(file => file.path);
130
+ FileHandler.removeMultiple(filePaths);
131
+ let messageContents = this.error?.message ? this.error.message : 'File validation failed.';
110
132
  if('function' === typeof this.processErrorResponse){
111
133
  return this.processErrorResponse(415, messageContents, req, res);
112
134
  }
@@ -122,7 +144,8 @@ class UploaderFactory
122
144
  try {
123
145
  for(let fieldName in req.files){
124
146
  for(let file of req.files[fieldName]){
125
- if(!await this.validateFileContents(file, allowedFileTypes[fieldName])){
147
+ let validationResult = await this.validateFileContents(file, allowedFileTypes[fieldName]);
148
+ if(!validationResult){
126
149
  FileHandler.remove(file.path);
127
150
  return false;
128
151
  }
@@ -130,7 +153,7 @@ class UploaderFactory
130
153
  }
131
154
  return true;
132
155
  } catch(error){
133
- this.error = {message: 'Error processing uploaded files.', error};
156
+ this.setError('Error processing uploaded files.', {error});
134
157
  return false;
135
158
  }
136
159
  }
@@ -159,28 +182,28 @@ class UploaderFactory
159
182
  validateInputs(fields, buckets, allowedFileTypes)
160
183
  {
161
184
  if(!Array.isArray(fields)){
162
- this.error = {message: 'Fields must be an array'};
185
+ this.setError('Fields must be an array');
163
186
  return false;
164
187
  }
165
188
  if(!buckets || 'object' !== typeof buckets){
166
- this.error = {message: 'Buckets must be an object'};
189
+ this.setError('Buckets must be an object');
167
190
  return false;
168
191
  }
169
192
  if(!allowedFileTypes || 'object' !== typeof allowedFileTypes){
170
- this.error = {message: 'AllowedFileTypes must be an object'};
193
+ this.setError('AllowedFileTypes must be an object');
171
194
  return false;
172
195
  }
173
196
  for(let field of fields){
174
197
  if(!field.name || 'string' !== typeof field.name){
175
- this.error = {message: 'Field name is invalid'};
198
+ this.setError('Field name is invalid');
176
199
  return false;
177
200
  }
178
201
  if(!buckets[field.name]){
179
- this.error = {message: 'Missing bucket for field: ' + field.name};
202
+ this.setError('Missing bucket for field: ' + field.name);
180
203
  return false;
181
204
  }
182
205
  if(!allowedFileTypes[field.name]){
183
- this.error = {message: 'Missing allowedFileType for field: ' + field.name};
206
+ this.setError('Missing allowedFileType for field: ' + field.name);
184
207
  return false;
185
208
  }
186
209
  }
@@ -190,49 +213,77 @@ class UploaderFactory
190
213
  validateFile(file, allowedFileType, cb)
191
214
  {
192
215
  if(!allowedFileType){
193
- return cb();
216
+ return cb(null, true);
194
217
  }
195
218
  if(!this.validateFilenameSecurity(file.originalname)){
196
- this.error = {message: 'Insecure filename: ' + file.originalname};
197
- return cb(new Error('Insecure filename: ' + file.originalname));
219
+ this.setError('Insecure filename: '+file.originalname, {originalname: file.originalname});
220
+ return cb(new Error('Insecure filename: '+file.originalname));
198
221
  }
199
222
  let fileExtension = FileHandler.extension(file.originalname).toLowerCase();
200
- let allowedExtensions = this.allowedExtensions[allowedFileType];
223
+ let allowedExtensions = this.allowedExtensions && this.allowedExtensions[allowedFileType];
201
224
  if(allowedExtensions && !allowedExtensions.includes(fileExtension)){
202
- this.error = {message: 'Invalid file extension: ' + fileExtension};
203
- return cb(new Error('Invalid file extension: ' + fileExtension));
225
+ this.setError('Invalid file extension: '+fileExtension, {
226
+ extension: fileExtension,
227
+ allowedExtensions,
228
+ allowedFileType,
229
+ filename: file.originalname
230
+ });
231
+ return cb(new Error('Invalid file extension: '+fileExtension));
204
232
  }
205
233
  let allowedFileTypeRegex = this.convertToRegex(allowedFileType);
206
234
  if(!allowedFileTypeRegex){
207
- this.error = {message: 'File type could not be converted to regex.', allowedFileType};
235
+ this.setError('File type could not be converted to regex.', {allowedFileType});
208
236
  return cb(new Error('File type could not be converted to regex'));
209
237
  }
210
238
  let mimeTypeValid = allowedFileTypeRegex.test(file.mimetype);
211
239
  if(!mimeTypeValid){
212
- this.error = {message: 'Invalid MIME type: ' + file.mimetype};
213
- return cb(new Error('Invalid MIME type: ' + file.mimetype));
240
+ this.setError('Invalid MIME type: '+file.mimetype, {
241
+ mimetype: file.mimetype,
242
+ allowedFileType,
243
+ filename: file.originalname,
244
+ regex: allowedFileTypeRegex
245
+ });
246
+ return cb(new Error('Invalid MIME type: '+file.mimetype));
214
247
  }
215
- return cb();
248
+ return cb(null, true);
216
249
  }
217
250
 
218
251
  async validateFileContents(file, allowedFileType)
219
252
  {
220
253
  if(!FileHandler.isFile(file.path)){
221
- this.error = {message: 'File path must be provided.', file};
254
+ this.setError('File path must be provided.', {file});
222
255
  return false;
223
256
  }
224
257
  let detectedType = FileHandler.detectFileType(file.path);
225
258
  if(detectedType && 'application/octet-stream' !== detectedType){
226
259
  let expectedMimeTypes = this.mimeTypes[allowedFileType] || [];
227
260
  if(0 < expectedMimeTypes.length && -1 === expectedMimeTypes.indexOf(detectedType)){
228
- this.error = {
229
- message: 'File content type mismatch.',
230
- detected: detectedType, expected: expectedMimeTypes
231
- };
261
+ this.setError('File content type mismatch.', {
262
+ detected: detectedType,
263
+ expected: expectedMimeTypes,
264
+ filename: file.filename,
265
+ path: file.path,
266
+ allowedFileType
267
+ });
232
268
  return false;
233
269
  }
234
270
  }
235
- return FileHandler.validateFileType(file.path, allowedFileType, this.allowedExtensions, this.maxFileSize);
271
+ let typeValidationResult = FileHandler.validateFileType(
272
+ file.path,
273
+ allowedFileType,
274
+ this.allowedExtensions,
275
+ this.maxFileSize
276
+ );
277
+ if(!typeValidationResult){
278
+ this.setError('File type validation failed.', {
279
+ fileHandlerError: FileHandler.error,
280
+ filename: file.filename,
281
+ path: file.path,
282
+ allowedFileType
283
+ });
284
+ return false;
285
+ }
286
+ return true;
236
287
  }
237
288
 
238
289
  convertToRegex(key)
@@ -246,17 +297,6 @@ class UploaderFactory
246
297
  return new RegExp(types.join('|'));
247
298
  }
248
299
 
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
300
  }
261
301
 
262
302
  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.24.0",
4
+ "version": "0.26.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",