This repository has been archived by the owner on Dec 19, 2023. It is now read-only.
forked from larvit/larvitbase-www
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
438 lines (333 loc) · 12.4 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
'use strict';
const topLogPrefix = 'larvitbase-www: ./index.js: ';
const ReqParser = require('larvitreqparser');
const Router = require('larvitrouter');
const LBase = require('larvitbase');
const LUtils = require('larvitutils');
const async = require('async');
const send = require('send');
const path = require('path');
const ejs = require('ejs');
const fs = require('fs');
const _ = require('lodash');
ejs.includeFile_org = ejs.includeFile;
ejs.resolveInclude_org = ejs.resolveInclude;
function App(options) {
const that = this;
if (!options) {
options = {};
}
that.options = options;
if (!that.options.log) {
const lUtils = new LUtils();
that.options.log = new lUtils.Log();
}
that.log = that.options.log;
if (!that.options.routerOptions) that.options.routerOptions = {};
if (!that.options.routerOptions.log) that.options.routerOptions.log = that.log;
if (!that.options.baseOptions) that.options.baseOptions = {};
if (!that.options.baseOptions.log) that.options.baseOptions.log = that.log;
if (!that.options.reqParserOptions) that.options.reqParserOptions = {};
if (!that.options.reqParserOptions.log) that.options.reqParserOptions.log = that.log;
that.compiledTemplates = {};
// Instantiate the router
that.router = new Router(that.options.routerOptions);
// Instantiate the request parser
that.reqParser = new ReqParser(that.options.reqParserOptions);
// Only set middleware array if none is provided from the initiator
if (!Array.isArray(options.baseOptions.middleware)) {
options.baseOptions.middleware = [
function mwValidateRoute(req, res, cb) { that.mwValidateRoute(req, res, cb); },
function mwParse(req, res, cb) { that.mwParse(req, res, cb); },
function mwRoute(req, res, cb) { that.mwRoute(req, res, cb); },
function mwSendStatic(req, res, cb) { that.mwSendStatic(req, res, cb); },
function mwRunController(req, res, cb) { that.mwRunController(req, res, cb); },
function mwRender(req, res, cb) { that.mwRender(req, res, cb); },
function mwSendToClient(req, res, cb) { that.mwSendToClient(req, res, cb); },
function mwCleanup(req, res, cb) { that.mwCleanup(req, res, cb); }
];
}
// Expose middlewares more convenient
that.middleware = options.baseOptions.middleware;
};
// Internal server error. 500
App.prototype.internalError = function internalError(req, res) {
res.statusCode = 500;
res.end('500 Internal Server Error');
};
// No route target found. 404
App.prototype.noTargetFound = function noTargetFound(req, res, cb) {
const that = this;
res.statusCode = 404;
that.router.resolve('/404', function (err, result) {
if (!result.templateFullPath) {
res.end('404 Not Found');
req.finished = true;
} else {
req.routed.controllerPath = result.controllerPath;
req.routed.controllerFullPath = result.controllerFullPath;
req.routed.templatePath = result.templatePath;
req.routed.templateFullPath = result.templateFullPath;
}
cb();
});
};
App.prototype.mwValidateRoute = function mwValidateRoute(req, res, cb) {
const logPrefix = req.logPrefix + 'mwValidateRoute() - ';
const that = this;
// validating if the requested path contains path traversing characters to prevent directory traversal
var reqPath = decodeURI(req.url);
if (reqPath.includes('..')) {
that.log.verbose(logPrefix + 'Requested file outside the process directory (Directory Traversal Attempt).');
that.noTargetFound(req, res, cb);
}
}
// Cleanup middleware, removing tmp file storage and more
App.prototype.mwCleanup = function mwCleanup(req, res, cb) {
const that = this;
delete req.finished;
that.reqParser.clean(req, res, cb);
};
// Parsing middleware
App.prototype.mwParse = function mwParse(req, res, cb) {
const that = this;
req.logPrefix = topLogPrefix + 'req.uuid: ' + req.uuid + ' url: ' + req.url + ' - ';
if (req.finished) return cb();
that.reqParser.parse(req, res, cb);
};
// Template rendering middleware
App.prototype.mwRender = function mwRender(req, res, cb) {
const logPrefix = req.logPrefix + 'mwRender() - ';
const tasks = [];
const that = this;
if (req.finished || req.render === false) return cb();
if (!req.routed.templateFullPath) {
that.log.verbose(logPrefix + 'No template found. req.routed.templateFullPath is not set.');
return cb();
}
// Custom ejs includeFile that uses larvitfs to search through node_modules for templates
ejs.includeFile = function (filePath, options) {
const routerBasePath = path.resolve(that.router.options.basePath);
const routerLfs = that.router.options.lfs;
let filePathAbsolute;
let relativePath;
// First deduct the routers base path from the file in which this function is ran on
if (options.filename) {
relativePath = options.filename.substring(routerBasePath.length + 1);
// Remove possible node_modules/xxx
const pathParts = relativePath.split('/');
relativePath = '';
for (let i = 0; pathParts[i] !== undefined; i++) {
if (pathParts[i] === 'node_modules') {
i++;
} else if (pathParts[i + 1]) {
relativePath += pathParts[i] + '/';
}
}
} else {
relativePath = path.parse(req.routed.templateFullPath).dir.substring(routerBasePath.length + 1);
}
// If absolute path, we do not need to do anything extra, just run the default ejs.includeFile()
if (filePath.substring(0, 1) === '/') {
return ejs.includeFile_org(filePath, options);
}
filePathAbsolute = routerLfs.getPathSync(relativePath + '/' + filePath);
// Try with the extensions passed to the router
if (!filePathAbsolute && that.router && that.router.options && Array.isArray(that.router.options.paths.template.exts)) {
for (const ext of that.router.options.paths.template.exts) {
filePathAbsolute = routerLfs.getPathSync(relativePath + '/' + filePath + '.' + ext);
if (filePathAbsolute) break;
}
}
if (!filePathAbsolute) {
throw new Error('Can not find template matching "' + filePath + '"');
}
return ejs.includeFile_org(filePathAbsolute, options);
};
if (!that.compiledTemplates[req.routed.templateFullPath]) {
that.log.debug(logPrefix + 'Compiling template: ' + req.routed.templateFullPath);
// Custom ejs resolveInclude to resolve path based on larvitfs
/* This should be the winning solution, but it did not seem to work
ejs.resolveInclude = function (filePath, options) {
const parsedFilePath = path.parse(filePath);
let returnStr;
// If absolute path, we do not need to do anything extra, just run the default ejs.resolveInclude()
if (filePath.substring(0, 1) === '/') {
return ejs.resolveInclude_org(filePath, options);
}
returnStr = ejs.resolveInclude_org(filePath, options);
//ejs.resolveInclude = ejs.resolveInclude_org;
return returnStr;
};
*/
// Compile the template
tasks.push(function (cb) {
fs.readFile(req.routed.templateFullPath, function (err, str) {
const compileOpts = {outputFunctionName: 'print'};
let html;
if (err) {
that.log.error(logPrefix + 'Could not read template file. err: ' + err.message);
return cb(err);
}
html = str.toString();
try {
that.compiledTemplates[req.routed.templateFullPath] = ejs.compile(html, compileOpts);
} catch (err) {
that.log.error(logPrefix + 'Could not compile "' + req.routed.templateFullPath + '", err: ' + err.message);
return cb(err);
}
cb();
});
});
}
async.series(tasks, function (err) {
if (err) return cb(err);
try {
const renderObject = {};
renderObject._ = _;
renderObject.data = res.data;
res.renderedData = that.compiledTemplates[req.routed.templateFullPath](renderObject);
} catch (err) {
that.log.error(logPrefix + 'Could not render "' + req.routed.templateFullPath + '", err: ' + err.message);
return cb(err);
}
cb();
});
};
// Routing middleware
App.prototype.mwRoute = function mwRoute(req, res, cb) {
const logPrefix = req.logPrefix + 'mwRoute() - ';
const tasks = [];
const that = this;
let routeUrl;
if (req.finished) return cb();
if (!req.urlParsed) {
const err = new Error('req.urlParsed is not set');
that.log.error(logPrefix + err.message);
that.log.verbose(err.stack);
return cb(err);
}
routeUrl = req.urlParsed.pathname;
req.routed = {};
// Explicitly route / to default when we resolv files
if (routeUrl.split('?')[0] === '/') {
routeUrl = '/default';
} else if (routeUrl.split('?')[0] === '/.json') {
routeUrl = '/default.json';
}
// Handle URLs ending in .json
if (req.urlParsed.pathname.substring(req.urlParsed.pathname.length - 4) === 'json') {
that.log.debug(logPrefix + 'url ends in json, use some custom route options');
req.render = false;
routeUrl = req.urlParsed.pathname.substring(0, req.urlParsed.pathname.length - 5);
// Since the URL ends in .json, also check for static files
tasks.push(function (cb) {
that.router.resolve(req.urlParsed.pathname, function (err, result) {
if (err) return cb(err);
if (result.staticPath) {
req.routed.staticPath = result.staticPath;
req.routed.staticFullPath = result.staticFullPath;
}
cb();
});
});
} else {
req.render = true;
}
// Resolve stuff with the router
tasks.push(function (cb) {
that.router.resolve(routeUrl, function (err, result) {
if (err) return cb(err);
req.routed.controllerPath = result.controllerPath;
req.routed.controllerFullPath = result.controllerFullPath;
req.routed.templatePath = result.templatePath;
req.routed.templateFullPath = result.templateFullPath;
// Do not overwrite the .json file path from above with undefined here
if (result.staticPath) {
req.routed.staticPath = result.staticPath;
req.routed.staticFullPath = result.staticFullPath;
}
cb(err);
});
});
async.parallel(tasks, cb);
};
// Controller running middleware
App.prototype.mwRunController = function mwRunController(req, res, cb) {
const logPrefix = req.logPrefix + 'mwRunController() - ';
const that = this;
if (req.finished) return cb();
if (req.routed.templateFullPath && !req.routed.controllerFullPath) {
that.log.debug(logPrefix + 'Only template found');
return cb();
} else if (!req.routed.controllerFullPath && !req.routed.templateFullPath) {
that.log.debug(logPrefix + 'Either controller nor template found for given url, running that.noTargetFound()');
that.noTargetFound(req, res, cb);
} else { // Must be a controller here
that.log.debug(logPrefix + 'Controller found, running');
try {
require(req.routed.controllerFullPath)(req, res, cb);
} catch (err) {
that.log.error(logPrefix + 'Got exception when trying to run controller: ' + req.routed.controllerFullPath + ' (are you sure that module.exports is a function in the controller?), err: ' + err.message);
}
}
};
// Send static files middleware
App.prototype.mwSendStatic = function mwSendStatic(req, res, cb) {
const logPrefix = req.logPrefix + 'mwSendStatic() - ';
const that = this;
if (req.finished) return cb();
if (req.routed.staticFullPath) {
const sendStream = send(req, req.routed.staticFullPath, {index: false});
req.finished = true;
that.log.debug(logPrefix + 'Static file found, streaming');
sendStream.pipe(res);
sendStream.on('error', function (err) {
that.log.warn(logPrefix + 'error sending static file to client. err: ' + err.message);
return cb(err);
});
sendStream.on('end', cb);
} else {
return cb();
}
};
// Middleware for sending data to client
App.prototype.mwSendToClient = function mwSendToClient(req, res, cb) {
const logPrefix = req.logPrefix + 'mwSendToClient() - ';
const that = this;
let sendData = res.data;
if (req.finished) return cb();
// Rendered data means HTML, send as string to the client
if (res.renderedData) {
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end(res.renderedData);
req.finished = true;
return cb();
}
// If no rendered data exists, send res.data as stringified JSON to the client
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
try {
if (typeof sendData !== 'string' && !Buffer.isBuffer(sendData)) {
sendData = JSON.stringify(sendData);
}
} catch (err) {
that.log.warn(logPrefix + 'Could not stringify sendData. err: ' + err.message);
return cb(err);
}
res.end(sendData);
req.finished = true;
cb();
};
App.prototype.start = function start(cb) {
const that = this;
that.base = new LBase(that.options.baseOptions);
that.base.on('error', function (err, req, res) {
that.internalError(req, res);
});
that.base.start(cb);
};
App.prototype.stop = function (cb) {
const that = this;
that.base.httpServer.close(cb);
};
exports = module.exports = App;