import * as path from "path"; import * as uuid from "uuid"; import { Express } from "express"; import * as express from "express"; import * as winston from "winston"; import * as morgan from "morgan"; import { Connection, createConnection } from "mysql2/promise"; import { engine as handlebarsEngine } from "express-handlebars"; import { Config } from "./Config"; import { DbcManager } from "./data/DbcReader"; import { CharacterCustomization } from "./data/CharacterCustomization"; import { IndexController } from "./controllers/IndexController"; import { CharacterController } from "./controllers/CharacterController"; export class Armory { public characterCustomization: CharacterCustomization; public dbc: DbcManager; public config: Config; public worldDb: Connection; public logger: winston.Logger; private charsDbs: { [key: string]: Connection }; private errorNames: { [key: number]: string }; private errorDescriptions: { [key: number]: string }; public constructor() { this.dbc = new DbcManager(); this.characterCustomization = new CharacterCustomization(); this.charsDbs = {}; this.logger = winston.createLogger({ level: "info", format: winston.format.combine( winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }), winston.format.printf((info) => `[${info.timestamp}] [${info.level.toUpperCase()}]: ${info.message}`), ), transports: [ new winston.transports.Console({ level: "debug" }), new winston.transports.File({ filename: "armory.error.log", level: "error" }), new winston.transports.File({ filename: "armory.combined.log", level: "http" }), ], }); this.errorNames = { 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 500: "Internal Server Error", }; this.errorDescriptions = { 400: "Invalid request.", 404: "Sorry, we could not find what you were looking for.", 500: "An unexpected internal error has occurred. Please contact the site owner.", }; } public async start(): Promise { const app: Express = express(); const listenPort = 48733; this.logger.info("Loading config..."); this.config = await Config.load(this.logger); this.logger.info("Loading data files..."); if (this.config.loadDbcs) { await this.dbc.loadAllFiles(); } await this.characterCustomization.loadData(); this.logger.info("Connecting to databases..."); this.worldDb = await createConnection(this.config.worldDatabase); for (const realm of this.config.realms) { this.charsDbs[realm.name.toLowerCase()] = await createConnection(realm.charactersDatabase); } this.logger.info("Starting server..."); app.locals.aowow = this.config.aowowUrl; app.locals.websiteUrl = this.config.websiteUrl; app.locals.websiteName = this.config.websiteName; app.locals.iframeMode = this.config.iframeMode; app.engine(".html", handlebarsEngine({ extname: "html", partialsDir: path.join(process.cwd(), "static", "partials"), layoutsDir: path.join(process.cwd(), "static"), defaultLayout: "layout.html", helpers: { ...require("handlebars-helpers")(), }, })); app.set("view engine", "handlebars"); app.set("views", path.join(process.cwd(), "static")); app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { req.id = uuid.v4(); next(); }); morgan.token("id", (req: express.Request) => { return req.id; }); app.use(morgan(":method :url :status - ID :id - :remote-addr - :response-time ms", { stream: { write: (msg) => this.logger.http(msg.trim()), }, })); app.use("/js", express.static(`static/js`)); app.use("/css", express.static(`static/css`)); app.use("/img", express.static(`static/img`)); app.use("/data/mo3", express.static(`data/mo3`)); app.use("/data/meta", express.static(`data/meta`)); app.use("/data/bone", express.static(`data/bone`)); app.use("/data/textures", express.static(`data/textures`)); app.use("/data/background.png", express.static(`data/background.png`)); const indexController = new IndexController(this); app.get("/", this.wrapRoute(indexController.index.bind(indexController))); app.get("/search", this.wrapRoute(indexController.search.bind(indexController))); const charsController = new CharacterController(this); await charsController.load(); app.get("/character/:realm/:name", this.wrapRoute(charsController.character.bind(charsController))); app.get("/character/:realm/:name/talents", this.wrapRoute(charsController.talents.bind(charsController))); app.get("/character/:realm/:name/achievements", this.wrapRoute(charsController.achievements.bind(charsController))); app.get("/character/:realm/:character/achievements/data", this.wrapRoute(charsController.achievementsData.bind(charsController))); app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => { // Error handler if (err instanceof Error) { const contents = err.stack ?? `${err.name}: ${err.message}`; this.logger.error(`Error on request ${req.id}. ${contents}`); } let status = 500; if (typeof err === "number") { status = err; } res.status(status).render("error.html", this.getErrorViewData(status, req)); }); app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { // 404 handler res.status(404); // Respond with html page if (req.accepts("html")) { res.render("error.html", this.getErrorViewData(404, req)); return; } // Respond with json if (req.accepts("json")) { res.json({ error: this.errorNames[404] }); return; } // Default to plain-text res.type("txt").send(this.errorNames[404]); }); this.gc(); app.listen(listenPort, "0.0.0.0", () => { this.logger.info(`Server is listening on 0.0.0.0:${listenPort}.`); }); } public getCharactersDb(realm: string): Connection { return this.charsDbs[realm.toLowerCase()]; } public gc(): void { if (this.config.loadDbcs) { return; } setTimeout(() => { if (global.gc) { global.gc(); } }, 500); } private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise) { // Adds error handling for promise-based controller methods return async (req: express.Request, res: express.Response, next: express.NextFunction) => { try { await fn(req, res, next); } catch (e) { next(e); } }; } private getErrorViewData(status: number, req: express.Request) { return { status, name: this.errorNames[status] || "An error occurred", description: this.errorDescriptions[status] || "", reqId: req.id, }; } }