@reldens/server-utils 0.5.0 → 0.7.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.
@@ -12,6 +12,8 @@ const bodyParser = require('body-parser');
12
12
  const session = require('express-session');
13
13
  const rateLimit = require('express-rate-limit');
14
14
  const cors = require('cors');
15
+ const helmet = require('helmet');
16
+ const xss = require('xss-clean');
15
17
 
16
18
  class AppServerFactory
17
19
  {
@@ -23,6 +25,7 @@ class AppServerFactory
23
25
  this.session = session;
24
26
  this.appServer = false;
25
27
  this.app = express();
28
+ this.rateLimit = rateLimit;
26
29
  this.useCors = 1 === Number(process.env.RELDENS_USE_CORS || 1);
27
30
  this.useExpressJson = 1 === Number(process.env.RELDENS_USE_EXPRESS_JSON || 1);
28
31
  this.useUrlencoded = 1 === Number(process.env.RELDENS_USE_URLENCODED || 1);
@@ -36,6 +39,14 @@ class AppServerFactory
36
39
  this.windowMs = Number(process.env.RELDENS_EXPRESS_RATE_LIMIT_MS || 60000);
37
40
  this.maxRequests = Number(process.env.RELDENS_EXPRESS_RATE_LIMIT_MAX_REQUESTS || 30);
38
41
  this.applyKeyGenerator = 1 === Number(process.env.RELDENS_EXPRESS_RATE_LIMIT_APPLY_KEY_GENERATOR || 0);
42
+ this.jsonLimit = String(process.env.RELDENS_EXPRESS_JSON_LIMIT || '1mb');
43
+ this.urlencodedLimit = String(process.env.RELDENS_EXPRESS_URLENCODED_LIMIT || '1mb');
44
+ this.useHelmet = 1 === Number(process.env.RELDENS_USE_HELMET || 1);
45
+ this.useXssProtection = 1 === Number(process.env.RELDENS_USE_XSS_PROTECTION || 1);
46
+ this.globalRateLimit = 1 === Number(process.env.RELDENS_GLOBAL_RATE_LIMIT || 0);
47
+ this.corsOrigin = String(process.env.RELDENS_CORS_ORIGIN || '*');
48
+ this.corsMethods = String(process.env.RELDENS_CORS_METHODS || 'GET,POST').split(',');
49
+ this.corsHeaders = String(process.env.RELDENS_CORS_HEADERS || 'Content-Type,Authorization').split(',');
39
50
  this.errorMessage = '';
40
51
  }
41
52
 
@@ -44,14 +55,46 @@ class AppServerFactory
44
55
  if(appServerConfig){
45
56
  Object.assign(this, appServerConfig);
46
57
  }
58
+ if(this.useHelmet){
59
+ this.app.use(helmet());
60
+ }
47
61
  if(this.useCors){
48
- this.app.use(cors());
62
+ let corsOptions = {
63
+ origin: this.corsOrigin,
64
+ methods: this.corsMethods,
65
+ allowedHeaders: this.corsHeaders
66
+ };
67
+ this.app.use(cors(corsOptions));
68
+ }
69
+ if(this.globalRateLimit){
70
+ let limiterParams = {
71
+ windowMs: this.windowMs,
72
+ max: this.maxRequests,
73
+ standardHeaders: true,
74
+ legacyHeaders: false,
75
+ message: 'Too many requests from this IP, please try again later'
76
+ };
77
+ if(this.applyKeyGenerator){
78
+ limiterParams.keyGenerator = function(req){
79
+ return req.ip;
80
+ };
81
+ }
82
+ this.app.use(this.rateLimit(limiterParams));
83
+ }
84
+ if(this.useXssProtection){
85
+ this.app.use(xss());
49
86
  }
50
87
  if(this.useExpressJson){
51
- this.app.use(this.applicationFramework.json());
88
+ this.app.use(this.applicationFramework.json({
89
+ limit: this.jsonLimit,
90
+ verify: this.verifyContentTypeJson
91
+ }));
52
92
  }
53
93
  if(this.useUrlencoded){
54
- this.app.use(this.bodyParser.urlencoded({extended: true}));
94
+ this.app.use(this.bodyParser.urlencoded({
95
+ extended: true,
96
+ limit: this.urlencodedLimit
97
+ }));
55
98
  }
56
99
  if('' !== this.trustedProxy){
57
100
  this.app.enable('trust proxy', this.trustedProxy);
@@ -60,19 +103,30 @@ class AppServerFactory
60
103
  return {app: this.app, appServer: this.appServer};
61
104
  }
62
105
 
106
+ verifyContentTypeJson(req, res, buf)
107
+ {
108
+ let contentType = req.headers['content-type'] || '';
109
+ if(
110
+ req.method === 'POST'
111
+ && 0 < buf.length
112
+ && !contentType.includes('application/json')
113
+ && !contentType.includes('multipart/form-data')
114
+ ){
115
+ throw new Error('Invalid content-type');
116
+ }
117
+ }
118
+
63
119
  createServer()
64
120
  {
65
121
  if(!this.useHttps){
66
122
  return http.createServer(this.app);
67
123
  }
68
- let key = FileHandler.readFile(this.keyPath);
124
+ let key = FileHandler.readFile(this.keyPath, 'Key');
69
125
  if(!key){
70
- this.errorMessage = 'Key file not found: ' + this.keyPath;
71
126
  return false;
72
127
  }
73
- let cert = FileHandler.readFile(this.certPath);
128
+ let cert = FileHandler.readFile(this.certPath, 'Cert');
74
129
  if(!cert){
75
- this.errorMessage = 'Cert file not found: ' + this.certPath;
76
130
  return false;
77
131
  }
78
132
  let credentials = {
@@ -81,7 +135,7 @@ class AppServerFactory
81
135
  passphrase: this.passphrase
82
136
  };
83
137
  if('' !== this.httpsChain){
84
- let ca = FileHandler.readFile(this.httpsChain);
138
+ let ca = FileHandler.readFile(this.httpsChain, 'Certificate Authority');
85
139
  if(ca){
86
140
  credentials.ca = ca;
87
141
  }
@@ -92,17 +146,17 @@ class AppServerFactory
92
146
  async enableServeHome(app, homePageLoadCallback)
93
147
  {
94
148
  let limiterParams = {
95
- // default 60000 = 1 minute:
96
149
  windowMs: this.windowMs,
97
- // limit each IP to 30 requests per windowMs:
98
150
  max: this.maxRequests,
151
+ standardHeaders: true,
152
+ legacyHeaders: false
99
153
  };
100
154
  if(this.applyKeyGenerator){
101
- limiterParams.keyGenerator = function (req) {
155
+ limiterParams.keyGenerator = function(req){
102
156
  return req.ip;
103
157
  };
104
158
  }
105
- let limiter = rateLimit(limiterParams);
159
+ let limiter = this.rateLimit(limiterParams);
106
160
  app.post('/', limiter);
107
161
  app.post('/', async (req, res, next) => {
108
162
  if('/' === req._parsedUrl.pathname){
@@ -116,7 +170,12 @@ class AppServerFactory
116
170
  if('function' !== typeof homePageLoadCallback){
117
171
  return res.send('Homepage contents could not be loaded.');
118
172
  }
119
- return res.send(await homePageLoadCallback(req));
173
+ try {
174
+ return res.send(await homePageLoadCallback(req));
175
+ } catch(error){
176
+ this.errorMessage = 'Error loading homepage.';
177
+ return res.status(500).send('Error loading homepage.');
178
+ }
120
179
  }
121
180
  next();
122
181
  });
@@ -124,12 +183,38 @@ class AppServerFactory
124
183
 
125
184
  async serveStatics(app, statics)
126
185
  {
127
- app.use(this.applicationFramework.static(statics));
186
+ if(!FileHandler.isValidPath(statics)){
187
+ this.errorMessage = 'Invalid statics path.';
188
+ return;
189
+ }
190
+ let staticOptions = {
191
+ maxAge: '1d',
192
+ etag: true,
193
+ lastModified: true,
194
+ index: false,
195
+ setHeaders: function(res){
196
+ res.set('X-Content-Type-Options', 'nosniff');
197
+ }
198
+ };
199
+ app.use(this.applicationFramework.static(statics, staticOptions));
128
200
  }
129
201
 
130
202
  async serveStaticsPath(app, staticsPath, statics)
131
203
  {
132
- app.use(staticsPath, this.applicationFramework.static(statics));
204
+ if(!FileHandler.isValidPath(staticsPath) || !FileHandler.isValidPath(statics)){
205
+ this.errorMessage = 'Invalid statics path to be served.';
206
+ return;
207
+ }
208
+ let staticOptions = {
209
+ maxAge: '1d',
210
+ etag: true,
211
+ lastModified: true,
212
+ index: false,
213
+ setHeaders: function(res){
214
+ res.set('X-Content-Type-Options', 'nosniff');
215
+ }
216
+ };
217
+ app.use(staticsPath, this.applicationFramework.static(statics, staticOptions));
133
218
  }
134
219
 
135
220
  }
@@ -24,13 +24,57 @@ class FileHandler
24
24
 
25
25
  exists(fullPath)
26
26
  {
27
+ if(!this.isValidPath(fullPath)){
28
+ this.error = {message: 'Invalid path.', fullPath};
29
+ return false;
30
+ }
27
31
  return fs.existsSync(fullPath);
28
32
  }
29
33
 
34
+ isValidPath(filePath)
35
+ {
36
+ if(!filePath){
37
+ return false;
38
+ }
39
+ let pathStr = String(filePath);
40
+ if(pathStr.includes('../') || pathStr.includes('..\\')){
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ sanitizePath(filePath)
47
+ {
48
+ if(!filePath){
49
+ return '';
50
+ }
51
+ let sanitized = String(filePath)
52
+ .replace(/\.\./g, '')
53
+ .replace(/[:*?"<>|]/g, '')
54
+ .substring(0, 255);
55
+ return sanitized;
56
+ }
57
+
58
+ generateSecureFilename(originalName)
59
+ {
60
+ let ext = path.extname(originalName).toLowerCase();
61
+ let randomStr = '';
62
+ let chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
63
+ let charLength = chars.length;
64
+ for(let i = 0; i < 32; i++){
65
+ randomStr += chars.charAt(Math.floor(Math.random() * charLength));
66
+ }
67
+ return randomStr + ext;
68
+ }
69
+
30
70
  remove(fullPath)
31
71
  {
32
72
  try {
33
- let deletePath = Array.isArray(fullPath) ? this.joinPaths(...fullPath) : fullPath
73
+ if(!this.isValidPath(fullPath)){
74
+ this.error = {message: 'Invalid path for removal.', fullPath};
75
+ return false;
76
+ }
77
+ let deletePath = Array.isArray(fullPath) ? this.joinPaths(...fullPath) : fullPath;
34
78
  if(fs.existsSync(deletePath)){
35
79
  fs.rmSync(deletePath, {recursive: true, force: true});
36
80
  return true;
@@ -44,6 +88,10 @@ class FileHandler
44
88
  createFolder(folderPath)
45
89
  {
46
90
  try {
91
+ if(!this.isValidPath(folderPath)){
92
+ this.error = {message: 'Invalid folder path.', folderPath};
93
+ return false;
94
+ }
47
95
  if(fs.existsSync(folderPath)){
48
96
  return true;
49
97
  }
@@ -58,6 +106,10 @@ class FileHandler
58
106
  copyFolderSync(from, to)
59
107
  {
60
108
  try {
109
+ if(!this.isValidPath(from) || !this.isValidPath(to)){
110
+ this.error = {message: 'Invalid path for folder copy.', from, to};
111
+ return false;
112
+ }
61
113
  fs.mkdirSync(to, {recursive: true});
62
114
  let folders = fs.readdirSync(from);
63
115
  for(let element of folders){
@@ -80,6 +132,10 @@ class FileHandler
80
132
 
81
133
  copyFileSyncIfDoesNotExist(from, to)
82
134
  {
135
+ if(!this.isValidPath(from) || !this.isValidPath(to)){
136
+ this.error = {message: 'Invalid path for file copy.', from, to};
137
+ return false;
138
+ }
83
139
  if(!fs.existsSync(to)){
84
140
  return fs.copyFileSync(from, to);
85
141
  }
@@ -87,9 +143,14 @@ class FileHandler
87
143
 
88
144
  copyFile(from, to)
89
145
  {
146
+ if(!this.isValidPath(from) || !this.isValidPath(to)){
147
+ this.error = {message: 'Invalid path for file copy.', from, to};
148
+ return false;
149
+ }
90
150
  let origin = Array.isArray(from) ? this.joinPaths(...from) : from;
91
151
  let dest = Array.isArray(to) ? this.joinPaths(...to) : to;
92
152
  if(!this.exists(origin)){
153
+ this.error = {message: 'Failed to copy file, origin does not exists.', from, to, origin, dest};
93
154
  return false;
94
155
  }
95
156
  try {
@@ -108,11 +169,19 @@ class FileHandler
108
169
 
109
170
  readFolder(folder, options)
110
171
  {
172
+ if(!this.isValidPath(folder)){
173
+ this.error = {message: 'Invalid folder path.', folder};
174
+ return [];
175
+ }
111
176
  return fs.readdirSync(folder, options);
112
177
  }
113
178
 
114
179
  fetchSubFoldersList(folder, options)
115
180
  {
181
+ if(!this.isValidPath(folder)){
182
+ this.error = {message: 'Invalid folder path.', folder};
183
+ return [];
184
+ }
116
185
  let files = fs.readdirSync(folder, options);
117
186
  let subFolders = [];
118
187
  for(let file of files){
@@ -126,6 +195,10 @@ class FileHandler
126
195
 
127
196
  isFile(filePath)
128
197
  {
198
+ if(!this.isValidPath(filePath)){
199
+ this.error = {message: 'Invalid file path.', filePath};
200
+ return false;
201
+ }
129
202
  try {
130
203
  return fs.lstatSync(filePath).isFile();
131
204
  } catch (error) {
@@ -136,59 +209,101 @@ class FileHandler
136
209
 
137
210
  permissionsCheck(systemPath)
138
211
  {
212
+ if(!this.isValidPath(systemPath)){
213
+ this.error = {message: 'Invalid system path.', systemPath};
214
+ return false;
215
+ }
139
216
  try {
140
217
  let crudTestPath = path.join(systemPath, 'crud-test');
141
218
  fs.mkdirSync(crudTestPath, {recursive: true});
142
219
  fs.rmSync(crudTestPath);
143
220
  return true;
144
221
  } catch (error) {
222
+ this.error = {message: 'Failed to check permissions.', error, systemPath};
145
223
  return false;
146
224
  }
147
225
  }
148
226
 
149
227
  fetchFileJson(filePath)
150
228
  {
229
+ if(!this.isValidPath(filePath)){
230
+ this.error = {message: 'Invalid file path.', filePath};
231
+ return false;
232
+ }
151
233
  let fileContents = this.fetchFileContents(filePath);
152
234
  if(!fileContents){
235
+ this.error = {message: 'Failed to fetch file contents.', filePath};
153
236
  return false;
154
237
  }
155
- let importedJson = JSON.parse(fileContents);
156
- if(!importedJson){
157
- this.error = {message: 'Can not parse data file.', filePath};
238
+ try {
239
+ return JSON.parse(fileContents);
240
+ } catch(error){
241
+ this.error = {message: 'Can not parse data file.', filePath, error};
158
242
  return false;
159
243
  }
160
- return importedJson;
161
244
  }
162
245
 
163
246
  fetchFileContents(filePath)
164
247
  {
248
+ if(!this.isValidPath(filePath)){
249
+ this.error = {message: 'Invalid file path.', filePath};
250
+ return false;
251
+ }
165
252
  if(!this.isFile(filePath)){
253
+ this.error = {message: 'File check failed to fetch file contents.', filePath};
166
254
  return false;
167
255
  }
168
- let fileContent = this.readFile(filePath);
169
- if(!fileContent){
170
- this.error = {message: 'Can not read data or empty file.', filePath};
256
+ try {
257
+ let fileContent = this.readFile(filePath);
258
+ if(!fileContent){
259
+ this.error = {message: 'Can not read data or empty file.', filePath};
260
+ return false;
261
+ }
262
+ return fileContent;
263
+ } catch(error){
264
+ this.error = {message: 'Error reading file.', filePath, error};
171
265
  return false;
172
266
  }
173
- return fileContent;
174
267
  }
175
268
 
176
269
  readFile(filePath)
177
270
  {
271
+ if(!this.isValidPath(filePath)){
272
+ this.error = {message: 'Invalid file path.', filePath};
273
+ return false;
274
+ }
178
275
  if(!filePath){
179
276
  this.error = {message: 'Missing data file.', filePath};
180
277
  return false;
181
278
  }
182
- return fs.readFileSync(filePath, {encoding: this.encoding, flag: 'r'});
279
+ try {
280
+ return fs.readFileSync(filePath, {encoding: this.encoding, flag: 'r'});
281
+ } catch(error){
282
+ this.error = {message: 'Error reading file.', filePath, error};
283
+ return false;
284
+ }
183
285
  }
184
286
 
185
287
  async updateFileContents(filePath, contents)
186
288
  {
187
- return fs.writeFileSync(fs.openSync(filePath, 'w+'), contents);
289
+ if(!this.isValidPath(filePath)){
290
+ this.error = {message: 'Invalid file path.', filePath};
291
+ return false;
292
+ }
293
+ try {
294
+ return fs.writeFileSync(fs.openSync(filePath, 'w+'), contents);
295
+ } catch(error){
296
+ this.error = {message: 'Error updating file.', filePath, error};
297
+ return false;
298
+ }
188
299
  }
189
300
 
190
301
  writeFile(fileName, content)
191
302
  {
303
+ if(!this.isValidPath(fileName)){
304
+ this.error = {message: 'Invalid file name.', fileName};
305
+ return false;
306
+ }
192
307
  try {
193
308
  fs.writeFileSync(fileName, content, this.encoding);
194
309
  return true;
@@ -198,6 +313,66 @@ class FileHandler
198
313
  return false;
199
314
  }
200
315
 
316
+ validateFileType(filePath, allowedType, allowedFileTypes, maxFileSize)
317
+ {
318
+ if(!this.isFile(filePath)){
319
+ return false;
320
+ }
321
+ let extension = path.extname(filePath).toLowerCase();
322
+ let allowedExtensions = allowedFileTypes[allowedType] || allowedFileTypes.any;
323
+ if(0 === allowedExtensions.length){
324
+ return true;
325
+ }
326
+ if(!allowedExtensions.includes(extension)){
327
+ this.error = {message: 'Invalid file extension.', extension, allowedType};
328
+ return false;
329
+ }
330
+ let fileSize = fs.statSync(filePath).size;
331
+ if(fileSize > maxFileSize){
332
+ this.error = {message: 'File too large.', fileSize, maxFileSize};
333
+ return false;
334
+ }
335
+ return true;
336
+ }
337
+
338
+ isValidJson(filePath)
339
+ {
340
+ if(!this.isFile(filePath)){
341
+ return false;
342
+ }
343
+ try {
344
+ JSON.parse(this.readFile(filePath));
345
+ return true;
346
+ } catch(error){
347
+ this.error = {message: 'Invalid JSON file.', filePath, error};
348
+ return false;
349
+ }
350
+ }
351
+
352
+ getFirstFileBytes(filePath, bytes = 4100)
353
+ {
354
+ if(!this.isFile(filePath)){
355
+ return null;
356
+ }
357
+ let fd;
358
+ try {
359
+ fd = fs.openSync(filePath, 'r');
360
+ let buffer = Buffer.alloc(bytes);
361
+ let bytesRead = fs.readSync(fd, buffer, 0, bytes, 0);
362
+ fs.closeSync(fd);
363
+ return buffer.slice(0, bytesRead);
364
+ } catch(err){
365
+ if(fd !== undefined){
366
+ try {
367
+ fs.closeSync(fd);
368
+ } catch(e){
369
+ }
370
+ }
371
+ this.error = {message: 'Error reading file head.', filePath, error: err};
372
+ return null;
373
+ }
374
+ }
375
+
201
376
  }
202
377
 
203
378
  module.exports.FileHandler = new FileHandler();
@@ -12,45 +12,153 @@ class UploaderFactory
12
12
 
13
13
  constructor(props)
14
14
  {
15
- this.mimeTypes = props.mimeTypes;
15
+ this.mimeTypes = props.mimeTypes || {};
16
16
  this.error = {message: ''};
17
+ this.maxFileSize = props.maxFileSize || 20 * 1024 * 1024;
18
+ this.fileLimit = props.fileLimit || 0;
19
+ this.allowedExtensions = props.allowedExtensions;
17
20
  }
18
21
 
19
22
  createUploader(fields, buckets, allowedFileTypes)
20
23
  {
24
+ if(!this.validateInputs(fields, buckets, allowedFileTypes)){
25
+ throw new Error('Invalid uploader configuration: ' + this.error.message);
26
+ }
21
27
  let storage = multer.diskStorage({
22
28
  destination: (req, file, cb) => {
23
- cb(null, buckets[file.fieldname]);
29
+ let dest = buckets[file.fieldname];
30
+ if(!FileHandler.isValidPath(dest)){
31
+ return cb(new Error('Invalid destination path'));
32
+ }
33
+ FileHandler.createFolder(dest);
34
+ cb(null, dest);
24
35
  },
25
- filename: (req,file,cb) => {
26
- cb(null, file.originalname);
36
+ filename: (req, file, cb) => {
37
+ let secureFilename = FileHandler.generateSecureFilename(file.originalname);
38
+ if(!req.fileNameMapping){
39
+ req.fileNameMapping = {};
40
+ }
41
+ req.fileNameMapping[secureFilename] = file.originalname;
42
+ cb(null, secureFilename);
27
43
  }
28
- })
29
- return multer({
44
+ });
45
+ let limits = {
46
+ fileSize: this.maxFileSize
47
+ };
48
+ if(0 < this.fileLimit){
49
+ limits['files'] = this.fileLimit;
50
+ }
51
+ let upload = multer({
30
52
  storage,
53
+ limits,
31
54
  fileFilter: (req, file, cb) => {
32
- return this.checkFileType(file, allowedFileTypes[file.fieldname], cb);
55
+ return this.validateFile(file, allowedFileTypes[file.fieldname], cb);
33
56
  }
34
- }).fields(fields);
57
+ });
58
+ return (req, res, next) => {
59
+ upload.fields(fields)(req, res, async (err) => {
60
+ if(err){
61
+ if(err instanceof multer.MulterError){
62
+ if(err.code === 'LIMIT_FILE_SIZE'){
63
+ return res.status(413).send('File too large');
64
+ }
65
+ if(err.code === 'LIMIT_FILE_COUNT'){
66
+ return res.status(413).send('Too many files');
67
+ }
68
+ return res.status(400).send('File upload error: ' + err.message);
69
+ }
70
+ return res.status(500).send('Server error during file upload');
71
+ }
72
+ if(!req.files){
73
+ return next();
74
+ }
75
+ try {
76
+ for(let fieldName in req.files){
77
+ for(let file of req.files[fieldName]){
78
+ if(!await this.validateFileContents(file, allowedFileTypes[fieldName])){
79
+ if(FileHandler.exists(file.path)){
80
+ FileHandler.remove(file.path);
81
+ }
82
+ return res.status(415).send('File contents do not match declared type');
83
+ }
84
+ }
85
+ }
86
+ next();
87
+ } catch(error){
88
+ console.error('File validation error:', error);
89
+ this.cleanupFiles(req.files);
90
+ return res.status(500).send('Error processing uploaded files');
91
+ }
92
+ });
93
+ };
94
+ }
95
+
96
+ validateInputs(fields, buckets, allowedFileTypes)
97
+ {
98
+ if(!Array.isArray(fields)){
99
+ this.error = {message: 'Fields must be an array'};
100
+ return false;
101
+ }
102
+ if(!buckets || typeof buckets !== 'object'){
103
+ this.error = {message: 'Buckets must be an object'};
104
+ return false;
105
+ }
106
+ if(!allowedFileTypes || typeof allowedFileTypes !== 'object'){
107
+ this.error = {message: 'AllowedFileTypes must be an object'};
108
+ return false;
109
+ }
110
+ for(let field of fields){
111
+ if(!field.name || typeof field.name !== 'string'){
112
+ this.error = {message: 'Field name is invalid'};
113
+ return false;
114
+ }
115
+ if(!Object.prototype.hasOwnProperty.call(buckets, field.name)){
116
+ this.error = {message: `Missing bucket for field: ${field.name}`};
117
+ return false;
118
+ }
119
+ if(!Object.prototype.hasOwnProperty.call(allowedFileTypes, field.name)){
120
+ this.error = {message: `Missing allowedFileType for field: ${field.name}`};
121
+ return false;
122
+ }
123
+ }
124
+ return true;
35
125
  }
36
126
 
37
- checkFileType(file, allowedFileTypes, cb)
127
+ validateFile(file, allowedFileType, cb)
38
128
  {
39
- if(!allowedFileTypes){
129
+ if(!allowedFileType){
40
130
  return cb(null, true);
41
131
  }
42
- let allowedFileTypeCheck = this.convertToRegex(allowedFileTypes);
43
- if(!allowedFileTypeCheck){
44
- this.error = {message: 'File type could not be converted to regex.', allowedFileTypes};
132
+ let fileExtension = FileHandler.extension(file.originalname).toLowerCase();
133
+ let allowedExtensions = this.allowedExtensions[allowedFileType];
134
+ if(allowedExtensions && !allowedExtensions.includes(fileExtension)){
135
+ this.error = {message: `Invalid file extension: ${fileExtension}`};
45
136
  return cb(null, false);
46
137
  }
47
- let extension = allowedFileTypeCheck.test(FileHandler.extension(file.originalname).toLowerCase());
48
- let mimeType = allowedFileTypeCheck.test(file.mimetype);
49
- if(mimeType && extension){
50
- return cb(null, true);
138
+ let allowedFileTypeRegex = this.convertToRegex(allowedFileType);
139
+ if(!allowedFileTypeRegex){
140
+ this.error = {message: 'File type could not be converted to regex.', allowedFileType};
141
+ return cb(null, false);
142
+ }
143
+ let mimeTypeValid = allowedFileTypeRegex.test(file.mimetype);
144
+ if(!mimeTypeValid){
145
+ this.error = {message: `Invalid MIME type: ${file.mimetype}`};
146
+ return cb(null, false);
147
+ }
148
+ return cb(null, true);
149
+ }
150
+
151
+ async validateFileContents(file, allowedFileType)
152
+ {
153
+ try {
154
+ if(!FileHandler.isFile(file.path)){
155
+ return false;
156
+ }
157
+ return FileHandler.validateFileType(file.path, allowedFileType, this.allowedExtensions, this.maxFileSize);
158
+ } catch(err){
159
+ console.error('Error validating file contents:', err);
160
+ return false;
51
161
  }
52
- this.error = {message: 'File type not supported.', extension, mimeType, allowedFileTypes};
53
- return cb(null, false);
54
162
  }
55
163
 
56
164
  convertToRegex(key)
@@ -58,10 +166,29 @@ class UploaderFactory
58
166
  if(!this.mimeTypes[key]){
59
167
  return false;
60
168
  }
61
- let types = this.mimeTypes[key].map(type => type.split('/').pop().replace('+', '\\+'));
169
+ let types = this.mimeTypes[key].map(type =>
170
+ type.split('/').pop().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
171
+ );
62
172
  return new RegExp(types.join('|'));
63
173
  }
64
174
 
175
+ cleanupFiles(files)
176
+ {
177
+ if(!files){
178
+ return;
179
+ }
180
+ for(let fieldName in files){
181
+ for(let file of files[fieldName]){
182
+ try {
183
+ if(FileHandler.exists(file.path)){
184
+ FileHandler.remove(file.path);
185
+ }
186
+ } catch(err){
187
+ console.error('Error cleaning up file:', file.path, err);
188
+ }
189
+ }
190
+ }
191
+ }
65
192
  }
66
193
 
67
194
  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.5.0",
4
+ "version": "0.7.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -40,6 +40,8 @@
40
40
  "express": "4.21.2",
41
41
  "express-rate-limit": "7.5.0",
42
42
  "express-session": "1.18.1",
43
- "multer": "^1.4.5-lts.2"
43
+ "helmet": "8.1.0",
44
+ "multer": "^1.4.5-lts.2",
45
+ "xss-clean": "^0.1.4"
44
46
  }
45
47
  }