@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.
- package/lib/app-server-factory.js +100 -15
- package/lib/file-handler.js +186 -11
- package/lib/uploader-factory.js +147 -20
- package/package.json +4 -2
|
@@ -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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/file-handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/lib/uploader-factory.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
55
|
+
return this.validateFile(file, allowedFileTypes[file.fieldname], cb);
|
|
33
56
|
}
|
|
34
|
-
})
|
|
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
|
-
|
|
127
|
+
validateFile(file, allowedFileType, cb)
|
|
38
128
|
{
|
|
39
|
-
if(!
|
|
129
|
+
if(!allowedFileType){
|
|
40
130
|
return cb(null, true);
|
|
41
131
|
}
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return cb(null,
|
|
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 =>
|
|
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.
|
|
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
|
-
"
|
|
43
|
+
"helmet": "8.1.0",
|
|
44
|
+
"multer": "^1.4.5-lts.2",
|
|
45
|
+
"xss-clean": "^0.1.4"
|
|
44
46
|
}
|
|
45
47
|
}
|