@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 +1 -0
- package/index.js +3 -1
- package/lib/app-server-factory.js +167 -15
- package/lib/encryptor.js +47 -0
- package/lib/file-handler.js +47 -0
- package/package.json +2 -2
package/README.md
CHANGED
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
}
|
package/lib/encryptor.js
ADDED
|
@@ -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();
|
package/lib/file-handler.js
CHANGED
|
@@ -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.
|
|
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.
|
|
45
|
+
"sanitize-html": "^2.17.0"
|
|
46
46
|
}
|
|
47
47
|
}
|