1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | var path = require("path");
|
4 | var fs = require("fs");
|
5 | var Http = require("http");
|
6 | var express = require("express");
|
7 | var Utils_1 = require("./Utils");
|
8 | var winston = require("winston");
|
9 | var helmet = require("helmet");
|
10 | var morgan = require("morgan");
|
11 | var cookieParser = require("cookie-parser");
|
12 | var bodyParser = require("body-parser");
|
13 | var serveFavicon = require("serve-favicon");
|
14 | var Session = require("express-session");
|
15 | var FileStore = require("session-file-store");
|
16 | var md5 = require("md5");
|
17 | var User_1 = require("./User");
|
18 | var glob = require("glob");
|
19 | var util = require("util");
|
20 | var pgPromise = require("pg-promise");
|
21 | require("winston-daily-rotate-file");
|
22 | var HttpApplication = (function () {
|
23 | function HttpApplication(appName, rootFolder, options) {
|
24 | this.getAcceptedLangugeHeader = function (req) {
|
25 | var data = req.headers["accept-language"];
|
26 | if (data) {
|
27 | var lst = data.split(",");
|
28 | if (lst.length !== 0) {
|
29 | return lst[0];
|
30 | }
|
31 | else {
|
32 | data;
|
33 | }
|
34 | }
|
35 | else {
|
36 | return null;
|
37 | }
|
38 | };
|
39 | var me = this, viewPath = path.join(__dirname, "..", "resources", "views", "error");
|
40 | me.isTestMode = false;
|
41 | me.httpErrorViews = {};
|
42 | me.setHttpErrorView(404, path.join(viewPath, "404.pug"));
|
43 | me.setHttpErrorView(500, path.join(viewPath, "500.pug"));
|
44 | me.options = options || {};
|
45 | me.rootFolder = rootFolder;
|
46 | me.appName = appName;
|
47 | me.expressApp = express();
|
48 | me.expressApp.set(HttpApplication.KEY_APPLICATION_NAME, appName);
|
49 | me.expressApp.set(HttpApplication.KEY_ROOT_FOLDER, rootFolder);
|
50 | me.normalizeOptions();
|
51 | me.createRuntimeFolders();
|
52 | me.createLogger();
|
53 | }
|
54 | HttpApplication.prototype.enterTestMode = function () {
|
55 | var me = this;
|
56 | me.isTestMode = true;
|
57 | };
|
58 | HttpApplication.prototype.setHttpErrorView = function (httpStatus, viewFullPath) {
|
59 | var me = this;
|
60 | if (fs.existsSync(viewFullPath)) {
|
61 | me.httpErrorViews[httpStatus] = viewFullPath;
|
62 | }
|
63 | else {
|
64 | throw new Error("Could not resolve " + viewFullPath);
|
65 | }
|
66 | };
|
67 | HttpApplication.prototype.createLogger = function () {
|
68 | var me = this, logFolder = Utils_1.Utils.ensureFolder(path.join(me.varFolder, "log"));
|
69 | me.logger = new winston.Logger({ exitOnError: false });
|
70 | me.logger.configure({
|
71 | transports: [
|
72 | new (winston.transports.File)({
|
73 | name: "error-log",
|
74 | level: "error",
|
75 | maxsize: me.options.maxLogSize,
|
76 | filename: path.join(logFolder, "error.log")
|
77 | }),
|
78 | new (winston.transports.DailyRotateFile)({
|
79 | name: "daily-log",
|
80 | maxsize: me.options.maxLogSize,
|
81 | filename: path.join(logFolder, "application-"),
|
82 | datePattern: "yyyy-MM.log",
|
83 | level: me.options.appLogLevel
|
84 | })
|
85 | ]
|
86 | });
|
87 | me.logFolder = logFolder;
|
88 | me.expressApp.set("_logger", me.logger);
|
89 | me.logger.info("%s loggers initialized with [%s] level", me.appName, me.options.appLogLevel.toUpperCase());
|
90 | };
|
91 | HttpApplication.prototype.normalizeOptions = function () {
|
92 | var me = this;
|
93 | me.options.port = me.options.port || 8090;
|
94 | me.options.address = me.options.address || "127.0.0.1";
|
95 | me.options.appLogLevel = me.options.appLogLevel || (me.isDevel() ? "debug" : me.getEnv("loglevel", "info"));
|
96 | me.options.maxLogSize = me.options.maxLogSize || (10 * (1024 * 1024));
|
97 | me.options.runtimeFolder = me.options.runtimeFolder || path.join(me.rootFolder, "var");
|
98 | };
|
99 | HttpApplication.prototype.isDevel = function () {
|
100 | var me = this;
|
101 | return me.getEnv("env", "development") !== "production";
|
102 | };
|
103 | HttpApplication.prototype.getEnv = function (key, defValue) {
|
104 | var me = this, key = (me.appName + "_" + key);
|
105 | return (process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || defValue);
|
106 | };
|
107 | HttpApplication.prototype.configureHelmetSecurity = function (options) {
|
108 | var me = this;
|
109 | me.logger.info("Configuring Helmet");
|
110 | me.expressApp.use(helmet(options));
|
111 | me.expressApp.use(helmet.hidePoweredBy({ setTo: "PHP 4.2.0" }));
|
112 | return true;
|
113 | };
|
114 | HttpApplication.prototype.configureHttpLogger = function () {
|
115 | var me = this, loggerName = me.createHttpLogger(), httpLogger = winston.loggers.get(loggerName), jsonCombined = function (tokens, req, res) {
|
116 | return JSON.stringify({
|
117 | remote_user: tokens["remote-user"](req, res) || "anonymus",
|
118 | remote_addr: tokens["remote-addr"](req),
|
119 | method: tokens["method"](req, res),
|
120 | status: tokens["status"](req, res),
|
121 | timestamp: tokens["date"](req, res, "iso"),
|
122 | url: tokens["url"](req, res),
|
123 | request_headers: req.headers,
|
124 | response_headers: res._headers,
|
125 | response_time: tokens["response-time"](req, res)
|
126 | });
|
127 | };
|
128 | var args = [
|
129 | jsonCombined,
|
130 | {
|
131 | stream: {
|
132 | write: function (message) {
|
133 | httpLogger.info(message.trim());
|
134 | }
|
135 | }
|
136 | }
|
137 | ];
|
138 | me.expressApp.use(morgan.apply(morgan, args));
|
139 | return true;
|
140 | };
|
141 | HttpApplication.prototype.createHttpLogger = function () {
|
142 | var me = this, loggerName = "http-logger";
|
143 | winston.loggers.add(loggerName, {
|
144 | transports: [
|
145 | new (winston.transports.DailyRotateFile)({
|
146 | name: "http-log",
|
147 | filename: path.join(me.logFolder, "access-"),
|
148 | datePattern: "yyyy-MM.log",
|
149 | level: me.options.appLogLevel,
|
150 | json: false,
|
151 | maxsize: me.options.maxLogSize,
|
152 | formatter: function (options) {
|
153 | return options["message"];
|
154 | }
|
155 | })
|
156 | ]
|
157 | });
|
158 | return loggerName;
|
159 | };
|
160 | HttpApplication.prototype.configurePostgreSQLDatabase = function (host, database, username, password, port, options) {
|
161 | var me = this, constr = util.format("postgres://%s:%s@%s:%s/%s", username, password, host, port || 5432, database);
|
162 | me.pgp = pgPromise(options || {});
|
163 | me.database = me.pgp(constr);
|
164 | };
|
165 | HttpApplication.prototype.configureContentParsers = function () {
|
166 | var me = this;
|
167 | me.expressApp.use(cookieParser());
|
168 | me.expressApp.use(bodyParser.json());
|
169 | me.expressApp.use(bodyParser.urlencoded({ extended: false }));
|
170 | return true;
|
171 | };
|
172 | HttpApplication.prototype.configureFilebasedSessionContainer = function () {
|
173 | var me = this, sessionFolder = Utils_1.Utils.ensureFolder(path.join(me.varFolder, ".session")), fileStoreOptions = {
|
174 | path: sessionFolder,
|
175 | useAsync: true,
|
176 | reapInterval: 5000,
|
177 | maxAge: 10000,
|
178 | }, sessionOptions = {
|
179 | secret: md5(me.rootFolder),
|
180 | name: md5(me.rootFolder),
|
181 | cookie: {}
|
182 | };
|
183 | if (!me.isDevel()) {
|
184 | me.expressApp.set("trust proxy", 1);
|
185 | if (!me.isTestMode) {
|
186 | sessionOptions.cookie["secure"] = true;
|
187 | }
|
188 | }
|
189 | sessionOptions.store = new (FileStore(Session))(fileStoreOptions);
|
190 | sessionOptions.resave = true;
|
191 | sessionOptions.saveUninitialized = true;
|
192 | me.expressApp.use(Session(sessionOptions));
|
193 | return true;
|
194 | };
|
195 | HttpApplication.prototype.configureSessionContainer = function () {
|
196 | var me = this;
|
197 | return me.configureFilebasedSessionContainer();
|
198 | };
|
199 | HttpApplication.prototype.getCurrentUser = function (req) {
|
200 | return req.session[HttpApplication.KEY_AUTH_USER] || null;
|
201 | };
|
202 | HttpApplication.prototype.setCurrentUser = function (req, user) {
|
203 | req.session[HttpApplication.KEY_AUTH_USER] = user;
|
204 | };
|
205 | HttpApplication.prototype.getAvailableLocales = function (req) {
|
206 | return req.app.get(HttpApplication.KEY_AVAILABLE_LOCALES) || [];
|
207 | };
|
208 | HttpApplication.prototype.getCurrentLocale = function (req) {
|
209 | var me = this;
|
210 | return req.session[HttpApplication.KEY_LOCALE] || me.getAcceptedLangugeHeader(req) || "en";
|
211 | };
|
212 | HttpApplication.prototype.getContentType = function (req) {
|
213 | return req.headers["content-type"] || null;
|
214 | };
|
215 | HttpApplication.prototype.configureAuthenticatedUser = function () {
|
216 | var me = this;
|
217 | me.expressApp.use(function (req, res, next) {
|
218 | if (!me.getCurrentUser(req)) {
|
219 | req.session[HttpApplication.KEY_AUTH_USER] = new User_1.AnonymousUser();
|
220 | }
|
221 | next();
|
222 | });
|
223 | return true;
|
224 | };
|
225 | HttpApplication.prototype.configureFileBasedTranslationProvider = function (folders) {
|
226 | var me = this, tokens = {
|
227 | "${rootFolder}": me.rootFolder,
|
228 | "${thisFolder}": null
|
229 | }, file = null, db = {};
|
230 | var parseValue = function (value, sourceDir) {
|
231 | tokens["${thisFolder}"] = sourceDir;
|
232 | for (var k in tokens) {
|
233 | if (value.indexOf(k) === 0) {
|
234 | debugger;
|
235 | file = fs.realpathSync(value.replace(k, tokens[k]));
|
236 | value = fs.readFileSync(file).toLocaleString();
|
237 | }
|
238 | }
|
239 | return value;
|
240 | };
|
241 | var procesData = function (data, file) {
|
242 | for (var msg in data) {
|
243 | for (var loc in data[msg]) {
|
244 | if (!db[loc]) {
|
245 | db[loc] = {};
|
246 | }
|
247 | db[loc][msg] = parseValue(data[msg][loc], path.dirname(file));
|
248 | }
|
249 | }
|
250 | };
|
251 | folders.forEach(function (item) {
|
252 | glob.sync(item).forEach(function (file) {
|
253 | try {
|
254 | procesData(JSON.parse(fs.readFileSync(file).toLocaleString()), file);
|
255 | }
|
256 | catch (e) {
|
257 | me.logger.error(e.message);
|
258 | }
|
259 | });
|
260 | });
|
261 | me.expressApp.set(HttpApplication.KEY_I18N, db);
|
262 | };
|
263 | HttpApplication.prototype.translate = function (req, message) {
|
264 | var args = [];
|
265 | for (var _i = 2; _i < arguments.length; _i++) {
|
266 | args[_i - 2] = arguments[_i];
|
267 | }
|
268 | return util.format.apply(util.format, [((req.app.get(HttpApplication.KEY_I18N) || {})[this.getCurrentLocale(req)] || {})[message] || message].concat(args));
|
269 | };
|
270 | HttpApplication.prototype.configureLocaleProvider = function (parameterName, defaultLocale, availableLocales) {
|
271 | var me = this;
|
272 | me.expressApp.use(function (req, res, next) {
|
273 | var accept_language = me.getAcceptedLangugeHeader(req);
|
274 | defaultLocale = defaultLocale || accept_language || "en";
|
275 | availableLocales = availableLocales || [defaultLocale];
|
276 | if (availableLocales.indexOf(defaultLocale) === -1) {
|
277 | availableLocales.push(defaultLocale);
|
278 | }
|
279 | var get_param = req.query[parameterName] || null;
|
280 | var post_param = req.body[parameterName] || null;
|
281 | var sess_param = me.getCurrentLocale(req) || null;
|
282 | var value = get_param || post_param || sess_param || accept_language || defaultLocale;
|
283 | value = availableLocales.indexOf(value) !== -1 ? value : defaultLocale;
|
284 | req.session[HttpApplication.KEY_LOCALE] = value;
|
285 | if (!me.getAvailableLocales(req)) {
|
286 | req.app.set(HttpApplication.KEY_AVAILABLE_LOCALES, availableLocales);
|
287 | }
|
288 | next();
|
289 | });
|
290 | return true;
|
291 | };
|
292 | HttpApplication.prototype.configureStaticFolderProvider = function (folders) {
|
293 | var me = this;
|
294 | Object.keys(folders).forEach(function (mapping) {
|
295 | me.logger.info("Serving %s as %s", folders[mapping], mapping);
|
296 | if (mapping === "/") {
|
297 | me.expressApp.use(express.static(folders[mapping], {
|
298 | fallthrough: true
|
299 | }));
|
300 | }
|
301 | else {
|
302 | me.expressApp.use("/" + mapping, express.static(folders[mapping], {
|
303 | fallthrough: false
|
304 | }));
|
305 | }
|
306 | });
|
307 | return true;
|
308 | };
|
309 | HttpApplication.prototype.configureFaciconProvider = function (faviconPath) {
|
310 | var me = this;
|
311 | me.expressApp.use(serveFavicon(faviconPath));
|
312 | };
|
313 | HttpApplication.prototype.configurePugTemplatesProvider = function (pugOptions) {
|
314 | var me = this;
|
315 | me.expressApp.set("view engine", "pug");
|
316 | me.expressApp.set("view options", pugOptions || {});
|
317 | };
|
318 | HttpApplication.prototype.configureApplicationInternal = function () {
|
319 | var me = this, ok = false;
|
320 | if (me.configureHelmetSecurity()) {
|
321 | if (me.configureHttpLogger()) {
|
322 | if (me.configureContentParsers()) {
|
323 | if (me.configureSessionContainer()) {
|
324 | if (me.configureAuthenticatedUser()) {
|
325 | ok = true;
|
326 | }
|
327 | }
|
328 | }
|
329 | }
|
330 | }
|
331 | return ok;
|
332 | };
|
333 | HttpApplication.prototype.configure404StatusProvider = function () {
|
334 | var me = this;
|
335 | me.expressApp.use(function (req, res, next) {
|
336 | var err = new Error(req.originalUrl + " Not Found!");
|
337 | err.status = 404;
|
338 | next(err);
|
339 | });
|
340 | };
|
341 | HttpApplication.prototype.handleFinalJSONError = function (err, req, res, next) {
|
342 | var me = this;
|
343 | res.json({
|
344 | error: true,
|
345 | message: err.mesage,
|
346 | status: err.status,
|
347 | stack: me.isDevel() ? err.stack : null
|
348 | });
|
349 | };
|
350 | HttpApplication.prototype.handleFinalHTMLError = function (err, req, res, next) {
|
351 | var me = this, errorViewFile = me.httpErrorViews[err.status];
|
352 | fs.exists(errorViewFile, function (exists) {
|
353 | if (exists) {
|
354 | res.render(errorViewFile, {
|
355 | appName: me.appName,
|
356 | err: err,
|
357 | locale: me.getCurrentLocale(req) || "en"
|
358 | });
|
359 | }
|
360 | else {
|
361 | res.send("Error: " + err.status + "\nMessage: " + err.message + "\nStack: " + (me.isDevel() ? err.stack : ""));
|
362 | }
|
363 | });
|
364 | };
|
365 | HttpApplication.prototype.configureFinalErrorProvider = function () {
|
366 | var me = this;
|
367 | me.expressApp.use(function (err, req, res, next) {
|
368 | me.logger.error("Error", {
|
369 | status: err.status,
|
370 | message: err.message,
|
371 | url: req.url,
|
372 | stack: err.stack,
|
373 | req_headers: req.headers,
|
374 | });
|
375 | var contentType = me.getContentType(req) || false;
|
376 | err.status = err.status || 500;
|
377 | err.message = me.isDevel() ? err.message : "Error with status " + err.status + " has been set";
|
378 | err.stack = me.isDevel() ? err.stack : null;
|
379 | res.setHeader("Content-Type", contentType || "");
|
380 | res.status(err.status);
|
381 | res.setHeader("Cache-Control", "private, no-cache, no-store, max-age=0, proxy-revalidate, s-maxage=0");
|
382 | res.setHeader("Expires", "Sat, 12 Oct 1991 05:00:00 GMT");
|
383 | res.setHeader("Pragma", "no-cache");
|
384 | res.setHeader("Vary", "*");
|
385 | if (contentType && contentType.indexOf("application/json") !== -1) {
|
386 | me.handleFinalJSONError.apply(me, arguments);
|
387 | }
|
388 | else {
|
389 | me.handleFinalHTMLError.apply(me, arguments);
|
390 | }
|
391 | });
|
392 | };
|
393 | HttpApplication.prototype.createRuntimeFolders = function () {
|
394 | var me = this;
|
395 | me.varFolder = Utils_1.Utils.ensureFolder(me.options.runtimeFolder);
|
396 | me.expressApp.set(HttpApplication.KEY_RUNTIME_FOLDER, me.varFolder);
|
397 | };
|
398 | HttpApplication.prototype.start = function (done) {
|
399 | var me = this;
|
400 | if (!me.running) {
|
401 | me.httpServer = Http.createServer(me.expressApp);
|
402 | me.httpServer.on("error", function () {
|
403 | me.onError.apply(me, arguments);
|
404 | });
|
405 | me.httpServer.on("listening", function () {
|
406 | var addr = me.httpServer.address();
|
407 | me.logger.info("%s is ready for connections on address: %s port: %s", me.appName, addr.address, addr.port);
|
408 | if (me.onStart.apply(me, arguments) === true) {
|
409 | me.running = true;
|
410 | if (done) {
|
411 | done();
|
412 | }
|
413 | }
|
414 | });
|
415 | if (me.configureApplicationInternal()) {
|
416 | me.configureApplication();
|
417 | me.configure404StatusProvider();
|
418 | me.configureFinalErrorProvider();
|
419 | me.httpServer.listen(me.options.port, me.options.address);
|
420 | }
|
421 | }
|
422 | else {
|
423 | if (done) {
|
424 | done();
|
425 | }
|
426 | }
|
427 | };
|
428 | HttpApplication.prototype.stop = function (done) {
|
429 | var me = this;
|
430 | if (me.running) {
|
431 | me.httpServer.close(function () {
|
432 | me.onStop.apply(me, arguments);
|
433 | me.logger.info("%s is shutdown.", me.appName);
|
434 | me.logger.close();
|
435 | winston.loggers.close("http-logger");
|
436 | me.running = false;
|
437 | if (done) {
|
438 | done();
|
439 | }
|
440 | });
|
441 | }
|
442 | else {
|
443 | if (done) {
|
444 | done();
|
445 | }
|
446 | }
|
447 | };
|
448 | HttpApplication.prototype.onError = function (error) {
|
449 | var me = this, msg = "";
|
450 | if (error.syscall !== "listen") {
|
451 | throw error;
|
452 | }
|
453 | switch (error.code) {
|
454 | case "EACCES":
|
455 | msg = me.options.port + " requires elevated privileges!";
|
456 | me.logger.error(msg);
|
457 | console.error(msg);
|
458 | process.exit(1);
|
459 | break;
|
460 | case "EADDRINUSE":
|
461 | msg = me.options.port + " is already in use!";
|
462 | me.logger.error(msg);
|
463 | console.error(msg);
|
464 | process.exit(1);
|
465 | break;
|
466 | default:
|
467 | throw error;
|
468 | }
|
469 | };
|
470 | ;
|
471 | HttpApplication.prototype.onStart = function () {
|
472 | return true;
|
473 | };
|
474 | HttpApplication.prototype.onStop = function () {
|
475 | };
|
476 | return HttpApplication;
|
477 | }());
|
478 | HttpApplication.KEY_RUNTIME_FOLDER = "_runtime_folder";
|
479 | HttpApplication.KEY_ROOT_FOLDER = "_root_folder";
|
480 | HttpApplication.KEY_LOG_FOLDER = "_log_folder";
|
481 | HttpApplication.KEY_APPLICATION_NAME = "_application_name";
|
482 | HttpApplication.KEY_AUTH_USER = "_auth_user";
|
483 | HttpApplication.KEY_AVAILABLE_LOCALES = "_available_locales";
|
484 | HttpApplication.KEY_LOCALE = "_locale";
|
485 | HttpApplication.KEY_I18N = "_i18n";
|
486 | exports.HttpApplication = HttpApplication;
|
487 |
|
\ | No newline at end of file |