UNPKG

expresser

Version:

A ready to use Node.js web app wrapper, built on top of Express.

455 lines (454 loc) 17.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.App = void 0; const utils_1 = require("./utils"); const eventemitter3_1 = __importDefault(require("eventemitter3")); const express = require("express"); const fs = require("fs"); const http = require("http"); const https = require("https"); const http2 = require("http2"); const jaul = require("jaul"); const logger = require("anyhow"); const path = require("path"); const setmeup = require("setmeup"); let settings; class App { static _instance; static get Instance() { return this._instance || (this._instance = new this()); } newInstance() { return new App(); } constructor() { if (!logger.isReady) { logger.setup(); } setmeup.load(__dirname + "/../settings.default.json", { overwrite: false }); settings = setmeup.settings; } expressApp; server; events = new eventemitter3_1.default(); on = (eventName, callback) => { this.events.on(eventName, callback); }; once = (eventName, callback) => { this.events.on(eventName, callback); }; off = (eventName, callback) => { this.events.off(eventName, callback); }; init = (middlewares) => { let mw; if (settings.general.debug && logger.options.levels.indexOf("debug") < 0) { logger.setOptions({ levels: logger.options.levels.concat(["debug"]) }); } logger.setOptions({ preprocessors: ["cleanup", "friendlyErrors", "maskSecrets"], preprocessorOptions: { errorStack: settings.logger.errorStack } }); this.expressApp = express(); const addMiddleware = (mw) => { if (!mw) return; if (mw.path && mw.handler) { this.expressApp.use(mw.path, mw.handler); } else { this.expressApp.use(mw); } }; middlewares = middlewares || { append: [], prepend: [] }; if (middlewares.prepend && !(0, utils_1.isArray)(middlewares.prepend)) { middlewares.prepend = [middlewares.prepend]; } if (middlewares.append && !(0, utils_1.isArray)(middlewares.append)) { middlewares.append = [middlewares.append]; } if (middlewares?.prepend?.length > 0) { for (mw of middlewares.prepend) { addMiddleware(mw); } } this.expressApp.set("trust proxy", settings.app.trustProxy); this.expressApp.set("views", settings.app.viewPath); if (settings.app.viewEngine) { this.expressApp.set("view engine", settings.app.viewEngine); this.expressApp.set("view options", settings.app.viewOptions); } if (settings.app.bodyParser?.enabled) { try { const midBodyParser = require("body-parser"); if (settings.app.bodyParser.rawTypes) { this.expressApp.use(midBodyParser.raw({ limit: settings.app.bodyParser.rawLimit, type: settings.app.bodyParser.rawTypes })); } this.expressApp.use(midBodyParser.json({ limit: settings.app.bodyParser.limit })); this.expressApp.use(midBodyParser.text({ limit: settings.app.bodyParser.limit })); this.expressApp.use(midBodyParser.urlencoded({ limit: settings.app.bodyParser.limit, extended: settings.app.bodyParser.extended })); } catch (ex) { logger.warn("App.init", "Can't load 'body-parser' module"); } try { const bodyParserErrorHandler = require("express-body-parser-error-handler"); this.expressApp.use(bodyParserErrorHandler()); } catch (ex) { logger.debug("App.init", "Module 'express-body-parser-error-handler' not installed, body-parser might throw ugly exceptions"); } } if (settings.app.cookie?.enabled) { try { const midCookieParser = require("cookie-parser"); this.expressApp.use(midCookieParser(settings.app.secret)); } catch (ex) { ex.friendlyMessage = "Can't load 'cookie-parser' module"; logger.error("App.init", ex); } } if (settings.app.session?.enabled) { try { const midSession = require("express-session"); const memoryStore = require("memorystore")(midSession); this.expressApp.use(midSession({ store: new memoryStore({ checkPeriod: settings.app.session.checkPeriod }), proxy: settings.app.session.proxy, resave: settings.app.session.resave, saveUninitialized: settings.app.session.saveUninitialized, secret: settings.app.secret, ttl: settings.app.session.maxAge * 1000, cookie: { secure: settings.app.session.secure, httpOnly: settings.app.session.httpOnly, maxAge: settings.app.session.maxAge * 1000 } })); } catch (ex) { ex.friendlyMessage = "Can't load 'express-session' and 'memorystore' modules"; logger.error("App.init", ex); } } if (settings.app.compression && settings.app.compression.enabled) { try { const midCompression = require("compression"); this.expressApp.use(midCompression()); } catch (ex) { ex.friendlyMessage = "Can't load 'compression' module"; logger.error("App.init", ex); } } if (settings.app.publicPath) { this.expressApp.use(express.static(settings.app.publicPath)); } if (middlewares?.append?.length > 0) { for (mw of middlewares.append) { addMiddleware(mw); } } if (settings.general.debug) { this.expressApp.use((req, res, next) => { const { method, url } = req; const ip = jaul.network.getClientIP(req); const msg = `Request from ${ip}`; if (res) { logger.debug("App", msg, method, url); } if (next) { next(); } return url; }); } if (settings.logger.errorHandler) { this.expressApp.use((err, req, res, next) => { logger.error("App", req.method, req.url, res.headersSent ? "Headers sent" : "Headers not sent", err); if (err instanceof URIError) { res.end(); } else { next(err); } }); } this.expressApp.disable("x-powered-by"); this.events.emit("init"); this.start(); }; start = () => { if (this.server) { logger.warn("App.start", "Server is already running, abort start"); return this.server; } let serverRef; if (settings.app.ssl?.enabled && settings.app.ssl?.keyFile && settings.app.ssl?.certFile) { const sslKeyFile = jaul.io.getFilePath(settings.app.ssl.keyFile); const sslCertFile = jaul.io.getFilePath(settings.app.ssl.certFile); if (sslKeyFile && sslCertFile) { const sslKey = fs.readFileSync(sslKeyFile, { encoding: settings.general.encoding }); const sslCert = fs.readFileSync(sslCertFile, { encoding: settings.general.encoding }); const sslOptions = { key: sslKey, cert: sslCert }; if (settings.app.http2) { sslOptions.allowHTTP1 = true; serverRef = http2.createSecureServer(sslOptions, this.expressApp); } else { serverRef = https.createServer(sslOptions, this.expressApp); } } else { throw new Error("Invalid certificate filename, please check paths defined on settings.app.ssl"); } } else { serverRef = settings.app.http2 ? http2.createServer(this.expressApp) : http.createServer(this.expressApp); } this.server = serverRef; let listenCb = () => { if (settings.app.ip) { logger.info("App.start", settings.app.title, `Listening on ${settings.app.ip} port ${settings.app.port}`, `URL ${settings.app.url}`); } else { logger.info("App.start", settings.app.title, `Listening on port ${settings.app.port}`, `URL ${settings.app.url}`); } }; let listenError = (err) => { logger.error("App.start", "Can't start", err); }; if (settings.app.ip) { serverRef.listen(settings.app.port, settings.app.ip, listenCb).on("error", listenError); } else { serverRef.listen(settings.app.port, listenCb).on("error", listenError); } serverRef.setTimeout(settings.app.timeout); this.events.emit("start"); return this.server; }; kill = () => { if (!this.server) { logger.warn("App.kill", "Server was not running"); return; } try { this.server.close(); this.server = null; this.events.emit("kill"); } catch (ex) { logger.error("App.kill", ex); } }; all = (...args) => { logger.debug("App.all", args[1], args[2]); return this.expressApp.all.apply(this.expressApp, args); }; get = (...args) => { logger.debug("App.get", args[1], args[2]); return this.expressApp.get.apply(this.expressApp, args); }; post = (...args) => { logger.debug("App.post", args[1], args[2]); return this.expressApp.post.apply(this.expressApp, args); }; put = (...args) => { logger.debug("App.put", args[1], args[2]); return this.expressApp.put.apply(this.expressApp, args); }; patch = (...args) => { logger.debug("App.patch", args[1], args[2]); return this.expressApp.patch.apply(this.expressApp, args); }; delete = (...args) => { logger.debug("App.delete", args[1], args[2]); return this.expressApp.delete.apply(this.expressApp, args); }; head = (...args) => { logger.debug("App.head", args[1], args[2]); return this.expressApp.head.apply(this.expressApp, args); }; use = (...args) => { logger.debug("App.use", args[1], args[2]); return this.expressApp.use.apply(this.expressApp, args); }; set = (...args) => { logger.debug("App.set", args[1], args[2]); return this.expressApp.set.apply(this.expressApp, args); }; route = (reqPath) => { logger.debug("App.route", reqPath); return this.expressApp.route.apply(this.expressApp, reqPath); }; renderView = (req, res, view, options, status) => { logger.debug("App.renderView", req.originalUrl, view, options); try { if (!options) { options = {}; } if (options.title == null) { options.title = settings.app.title; } if (status) { res.status(parseInt(status)); } res.render(view, options); } catch (ex) { logger.error("App.renderView", view, ex); this.renderError(req, res, ex); } if (settings.app.events.render) { this.events.emit("renderView", req, res, view, options, status); } }; renderText = (req, res, text, status) => { logger.debug("App.renderText", req.originalUrl, text); try { if (text == null) { logger.debug("App.renderText", "Called with empty text parameter"); text = ""; } else if (!(0, utils_1.isString)(text)) { text = text.toString(); } if (status) { res.status(parseInt(status)); } res.setHeader("content-type", "text/plain"); res.send(text); } catch (ex) { logger.error("App.renderText", text, ex); this.renderError(req, res, ex); } if (settings.app.events.render) { this.events.emit("renderText", req, res, text, status); } }; renderJson = (req, res, data, status) => { logger.debug("App.renderJson", req.originalUrl, data); if ((0, utils_1.isString)(data)) { try { data = JSON.parse(data); } catch (ex) { logger.error("App.renderJson", ex); return this.renderError(req, res, ex, 500); } } var cleanJson = function (obj, depth) { if (depth >= settings.logger.maxDepth) { return; } if ((0, utils_1.isArray)(obj)) { return Array.from(obj).map((i) => cleanJson(i, depth + 1)); } else if ((0, utils_1.isObject)(obj)) { return (() => { const result = []; for (let k in obj) { const v = obj[k]; if ((0, utils_1.isFunction)(v)) { result.push(delete obj[k]); } else { result.push(cleanJson(v, depth + 1)); } } return result; })(); } }; cleanJson(data, 0); if (status) { res.status(parseInt(status)); } if (settings.app.allowOriginHeader) { res.setHeader("Access-Control-Allow-Origin", settings.app.allowOriginHeader); } res.json(data); if (settings.app.events.render) { this.events.emit("renderJson", req, res, data, status); } }; renderImage = (req, res, filename, options) => { logger.debug("App.renderImage", req.originalUrl, filename, options); let mimetype = options ? options.mimetype : null; if (!mimetype) { let extname = path.extname(filename).toLowerCase().replace(".", ""); if (extname == "jpg") { extname = "jpeg"; } mimetype = `image/${extname}`; } res.type(mimetype); res.sendFile(filename); if (settings.app.events.render) { this.events.emit("renderImage", req, res, filename, options); } }; renderError = (req, res, error, status) => { let message; logger.debug("App.renderError", req.originalUrl, status, error); if (typeof error == "undefined" || error == null) { error = "Unknown error"; logger.warn("App.renderError", "Called with null error"); } if (status == null) { status = error.statusCode || error.status || error.code; } if (status == "ETIMEDOUT") { status = 408; } if (isNaN(status)) { status = 500; } try { if (error.error && !error.message && !error.error_description && !error.reason) { error = error.error; } if ((0, utils_1.isString)(error)) { message = { message: error }; } else { message = {}; message.message = error.message || error.error_description || error.description; if (!message.message) { message.message = error.toString(); } if (error.friendlyMessage) { message.friendlyMessage = error.friendlyMessage; } if (error.reason) { message.reason = error.reason; } if (error.code) { message.code = error.code; } else if (error.status) { message.code = error.status; } } } catch (ex) { logger.error("App.renderError", ex); } res.status(parseInt(status)).json(message); if (settings.app.events.render) { this.events.emit("renderError", req, res, error, status); } }; } exports.App = App; exports.default = App.Instance;