@reldens/server-utils 0.15.0 → 0.17.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
@@ -12,6 +12,7 @@ A set of helpers for Node.js to create a server and handle files.
12
12
  - File handler.
13
13
  - Express server factory.
14
14
  - Multer uploader factory.
15
+ - Password manager.
15
16
 
16
17
  Need something specific?
17
18
 
package/index.js CHANGED
@@ -7,9 +7,11 @@
7
7
  const { FileHandler } = require('./lib/file-handler');
8
8
  const { AppServerFactory } = require('./lib/app-server-factory');
9
9
  const { UploaderFactory } = require('./lib/uploader-factory');
10
+ const { Encryptor } = require('./lib/encryptor');
10
11
 
11
12
  module.exports = {
12
13
  FileHandler,
13
14
  AppServerFactory,
14
- UploaderFactory
15
+ UploaderFactory,
16
+ Encryptor
15
17
  };
@@ -51,6 +51,11 @@ class AppServerFactory
51
51
  this.tooManyRequestsMessage = 'Too many requests, please try again later.';
52
52
  this.error = {};
53
53
  this.processErrorResponse = false;
54
+ this.port = 3000;
55
+ this.autoListen = false;
56
+ this.domains = [];
57
+ this.useVirtualHosts = false;
58
+ this.defaultDomain = '';
54
59
  }
55
60
 
56
61
  createAppServer(appServerConfig)
@@ -61,6 +66,9 @@ class AppServerFactory
61
66
  if(this.useHelmet){
62
67
  this.app.use(this.helmetConfig ? helmet(this.helmetConfig) : helmet());
63
68
  }
69
+ if(this.useVirtualHosts){
70
+ this.setupVirtualHosts();
71
+ }
64
72
  if(this.useCors){
65
73
  let corsOptions = {
66
74
  origin: this.corsOrigin,
@@ -89,9 +97,11 @@ class AppServerFactory
89
97
  if(!req.body){
90
98
  return next();
91
99
  }
92
- if(typeof req.body === 'object'){
93
- for(let key in req.body){
94
- if(typeof req.body[key] === 'string'){
100
+ if('object' === typeof req.body){
101
+ let bodyKeys = Object.keys(req.body);
102
+ for(let i = 0; i < bodyKeys.length; i++){
103
+ let key = bodyKeys[i];
104
+ if('string' === typeof req.body[key]){
95
105
  req.body[key] = sanitizeHtml(req.body[key]);
96
106
  }
97
107
  }
@@ -102,7 +112,7 @@ class AppServerFactory
102
112
  if(this.useExpressJson){
103
113
  this.app.use(this.applicationFramework.json({
104
114
  limit: this.jsonLimit,
105
- verify: this.verifyContentTypeJson
115
+ verify: this.verifyContentTypeJson.bind(this)
106
116
  }));
107
117
  }
108
118
  if(this.useUrlencoded){
@@ -115,6 +125,13 @@ class AppServerFactory
115
125
  this.app.enable('trust proxy', this.trustedProxy);
116
126
  }
117
127
  this.appServer = this.createServer();
128
+ if(!this.appServer){
129
+ this.error = {message: 'Failed to create app server'};
130
+ return false;
131
+ }
132
+ if(this.autoListen){
133
+ this.listen();
134
+ }
118
135
  return {app: this.app, appServer: this.appServer};
119
136
  }
120
137
 
@@ -122,13 +139,57 @@ class AppServerFactory
122
139
  {
123
140
  let contentType = req.headers['content-type'] || '';
124
141
  if(
125
- req.method === 'POST'
142
+ 'POST' === req.method
126
143
  && 0 < buf.length
127
144
  && !contentType.includes('application/json')
128
145
  && !contentType.includes('multipart/form-data')
129
146
  ){
130
- throw new Error('Invalid content-type');
147
+ this.error = {message: 'Invalid content-type for JSON request'};
148
+ return false;
149
+ }
150
+ }
151
+
152
+ setupVirtualHosts()
153
+ {
154
+ if(0 === this.domains.length){
155
+ return;
131
156
  }
157
+ this.app.use((req, res, next) => {
158
+ let hostname = req.get('host');
159
+ if(!hostname){
160
+ if(this.defaultDomain){
161
+ req.domain = this.defaultDomain;
162
+ return next();
163
+ }
164
+ this.error = {message: 'No hostname provided and no default domain configured'};
165
+ return res.status(400).send('Bad Request');
166
+ }
167
+ let domain = this.findDomainConfig(hostname);
168
+ if(!domain){
169
+ if(this.defaultDomain){
170
+ req.domain = this.defaultDomain;
171
+ return next();
172
+ }
173
+ this.error = {message: 'Unknown domain: '+hostname};
174
+ return res.status(404).send('Domain not found');
175
+ }
176
+ req.domain = domain;
177
+ next();
178
+ });
179
+ }
180
+
181
+ findDomainConfig(hostname)
182
+ {
183
+ for(let i = 0; i < this.domains.length; i++){
184
+ let domain = this.domains[i];
185
+ if(domain.hostname === hostname){
186
+ return domain;
187
+ }
188
+ if(domain.aliases && domain.aliases.includes(hostname)){
189
+ return domain;
190
+ }
191
+ }
192
+ return false;
132
193
  }
133
194
 
134
195
  createServer()
@@ -136,12 +197,22 @@ class AppServerFactory
136
197
  if(!this.useHttps){
137
198
  return http.createServer(this.app);
138
199
  }
200
+ if(this.useVirtualHosts && 0 < this.domains.length){
201
+ return this.createHttpsServerWithSNI();
202
+ }
203
+ return this.createSingleHttpsServer();
204
+ }
205
+
206
+ createSingleHttpsServer()
207
+ {
139
208
  let key = FileHandler.readFile(this.keyPath, 'Key');
140
209
  if(!key){
210
+ this.error = {message: 'Could not read SSL key file: '+this.keyPath};
141
211
  return false;
142
212
  }
143
213
  let cert = FileHandler.readFile(this.certPath, 'Cert');
144
214
  if(!cert){
215
+ this.error = {message: 'Could not read SSL certificate file: '+this.certPath};
145
216
  return false;
146
217
  }
147
218
  let credentials = {
@@ -158,6 +229,67 @@ class AppServerFactory
158
229
  return https.createServer(credentials, this.app);
159
230
  }
160
231
 
232
+ createHttpsServerWithSNI()
233
+ {
234
+ let defaultCredentials = this.loadDefaultCredentials();
235
+ if(!defaultCredentials){
236
+ return false;
237
+ }
238
+ let httpsOptions = Object.assign({}, defaultCredentials);
239
+ httpsOptions.SNICallback = (hostname, callback) => {
240
+ let domain = this.findDomainConfig(hostname);
241
+ if(!domain || !domain.keyPath || !domain.certPath){
242
+ return callback(null, null);
243
+ }
244
+ let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
245
+ if(!key){
246
+ this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
247
+ return callback(null, null);
248
+ }
249
+ let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
250
+ if(!cert){
251
+ this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
252
+ return callback(null, null);
253
+ }
254
+ let ctx = require('tls').createSecureContext({
255
+ key: key.toString(),
256
+ cert: cert.toString()
257
+ });
258
+ callback(null, ctx);
259
+ };
260
+ return https.createServer(httpsOptions, this.app);
261
+ }
262
+
263
+ loadDefaultCredentials()
264
+ {
265
+ let key = FileHandler.readFile(this.keyPath, 'Default Key');
266
+ if(!key){
267
+ this.error = {message: 'Could not read default SSL key file: '+this.keyPath};
268
+ return false;
269
+ }
270
+ let cert = FileHandler.readFile(this.certPath, 'Default Cert');
271
+ if(!cert){
272
+ this.error = {message: 'Could not read default SSL certificate file: '+this.certPath};
273
+ return false;
274
+ }
275
+ return {
276
+ key: key.toString(),
277
+ cert: cert.toString(),
278
+ passphrase: this.passphrase
279
+ };
280
+ }
281
+
282
+ listen(port)
283
+ {
284
+ let listenPort = port || this.port;
285
+ if(!this.appServer){
286
+ this.error = {message: 'Cannot listen: app server not created'};
287
+ return false;
288
+ }
289
+ this.appServer.listen(listenPort);
290
+ return true;
291
+ }
292
+
161
293
  async enableServeHome(app, homePageLoadCallback)
162
294
  {
163
295
  let limiterParams = {
@@ -189,16 +321,16 @@ class AppServerFactory
189
321
  }
190
322
  return res.status(500).send(errorMessage);
191
323
  }
192
- try {
193
- return res.send(await homePageLoadCallback(req));
194
- } catch(error){
195
- let message = 'Error loading homepage.';
196
- this.error = {message, error};
324
+ let homepageContent = await homePageLoadCallback(req);
325
+ if(!homepageContent){
326
+ let message = 'Error loading homepage content';
327
+ this.error = {message};
197
328
  if('function' === typeof this.processErrorResponse){
198
329
  return this.processErrorResponse(500, message, req, res);
199
330
  }
200
331
  return res.status(500).send(message);
201
332
  }
333
+ return res.send(homepageContent);
202
334
  }
203
335
  next();
204
336
  });
@@ -207,8 +339,8 @@ class AppServerFactory
207
339
  async serveStatics(app, statics)
208
340
  {
209
341
  if(!FileHandler.isValidPath(statics)){
210
- this.error = {message: 'Invalid statics path.'};
211
- return;
342
+ this.error = {message: 'Invalid statics path: '+statics};
343
+ return false;
212
344
  }
213
345
  let staticOptions = {
214
346
  maxAge: '1d',
@@ -220,13 +352,14 @@ class AppServerFactory
220
352
  }
221
353
  };
222
354
  app.use(this.applicationFramework.static(statics, staticOptions));
355
+ return true;
223
356
  }
224
357
 
225
358
  async serveStaticsPath(app, staticsPath, statics)
226
359
  {
227
360
  if(!FileHandler.isValidPath(staticsPath) || !FileHandler.isValidPath(statics)){
228
- this.error = {message: 'Invalid statics path to be served.'};
229
- return;
361
+ this.error = {message: 'Invalid statics path to be served: '+staticsPath+' -> '+statics};
362
+ return false;
230
363
  }
231
364
  let staticOptions = {
232
365
  maxAge: '1d',
@@ -238,6 +371,25 @@ class AppServerFactory
238
371
  }
239
372
  };
240
373
  app.use(staticsPath, this.applicationFramework.static(statics, staticOptions));
374
+ return true;
375
+ }
376
+
377
+ addDomain(domainConfig)
378
+ {
379
+ if(!domainConfig.hostname){
380
+ this.error = {message: 'Domain configuration missing hostname'};
381
+ return false;
382
+ }
383
+ this.domains.push(domainConfig);
384
+ return true;
385
+ }
386
+
387
+ async close()
388
+ {
389
+ if(!this.appServer){
390
+ return true;
391
+ }
392
+ return await this.appServer.close();
241
393
  }
242
394
 
243
395
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ *
3
+ * Reldens - Encryptor
4
+ *
5
+ */
6
+
7
+ let crypto = require('crypto');
8
+
9
+ class Encryptor
10
+ {
11
+
12
+ constructor()
13
+ {
14
+ // recommended minimum for PBKDF2 with SHA-512
15
+ this.iterations = 60000;
16
+ this.keylen = 64;
17
+ this.digest = 'sha512';
18
+ }
19
+
20
+ encryptPassword(password)
21
+ {
22
+ // generate the password hash:
23
+ let salt = crypto.randomBytes(16).toString('hex');
24
+ let hash = crypto.pbkdf2Sync(password, salt, this.iterations, this.keylen, this.digest ).toString('hex');
25
+ return salt + ':' + hash;
26
+ }
27
+
28
+ validatePassword(password, storedPassword)
29
+ {
30
+ let parts = storedPassword.split(':');
31
+ if(2 !== parts.length){
32
+ return false;
33
+ }
34
+ let salt = parts[0];
35
+ let storedHash = parts[1];
36
+ let hash = crypto.pbkdf2Sync(password, salt, this.iterations, this.keylen, this.digest).toString('hex');
37
+ return storedHash === hash;
38
+ }
39
+
40
+ generateSecretKey()
41
+ {
42
+ return crypto.randomBytes(32).toString('hex');
43
+ }
44
+
45
+ }
46
+
47
+ module.exports.Encryptor = new Encryptor();
@@ -203,6 +203,53 @@ class FileHandler
203
203
  return false;
204
204
  }
205
205
 
206
+ isFolder(dirPath)
207
+ {
208
+ if(!this.isValidPath(dirPath)){
209
+ this.error = {message: 'Invalid folder path.', dirPath};
210
+ return false;
211
+ }
212
+ if(!this.exists(dirPath)){
213
+ return false;
214
+ }
215
+ try {
216
+ return fs.lstatSync(dirPath).isDirectory();
217
+ } catch (error) {
218
+ this.error = {message: 'Can not check folder.', error, dirPath};
219
+ }
220
+ return false;
221
+ }
222
+
223
+ getFilesInFolder(dirPath, extensions = [])
224
+ {
225
+ if(!this.isValidPath(dirPath)){
226
+ this.error = {message: 'Invalid folder path.', dirPath};
227
+ return [];
228
+ }
229
+ let files = this.readFolder(dirPath);
230
+ if(0 === files.length){
231
+ return [];
232
+ }
233
+ let result = [];
234
+ for(let file of files){
235
+ let filePath = path.join(dirPath, file);
236
+ if(!this.isFile(filePath)){
237
+ continue;
238
+ }
239
+ if(0 === extensions.length){
240
+ result.push(file);
241
+ continue;
242
+ }
243
+ for(let ext of extensions){
244
+ if(file.endsWith(ext)){
245
+ result.push(file);
246
+ break;
247
+ }
248
+ }
249
+ }
250
+ return result;
251
+ }
252
+
206
253
  permissionsCheck(systemPath)
207
254
  {
208
255
  if(!this.isValidPath(systemPath)){
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/server-utils",
3
3
  "scope": "@reldens",
4
- "version": "0.15.0",
4
+ "version": "0.17.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -42,6 +42,6 @@
42
42
  "express-session": "1.18.1",
43
43
  "helmet": "8.1.0",
44
44
  "multer": "^1.4.5-lts.2",
45
- "sanitize-html": "^2.16.0"
45
+ "sanitize-html": "^2.17.0"
46
46
  }
47
47
  }