/// import * as bodyParser from "body-parser"; import * as cors from "cors"; import * as redisSession from "connect-redis"; import * as express from "express"; import * as expressSession from "express-session"; import * as fs from "fs-extra"; import * as http from "http"; import * as https from "https"; import * as multer from "multer"; import * as passport from "passport"; import * as eta from "@eta/eta"; import Application from "./Application"; import Authenticator from "./Authenticator"; import RepositoryManager from "@eta/db"; import RequestHandler from "./RequestHandler"; export default class WebServer { public app: Application; /** * Express application powering the web server */ public express: express.Application; /** * Main HTTP(S) web server */ public server: http.Server | https.Server; /** * Server listening on port 80, in order to redirect HTTP requests to HTTPS */ public redirectServer?: http.Server = undefined; public middleware: {[key: string]: express.Handler} = {}; private get config(): eta.Configuration { return this.app.configs.global; } public async init(): Promise { this.express = express(); this.configureExpress(); await this.app.emit("server:middleware:before"); this.setupMiddleware(); await this.app.emit("server:middleware:after"); try { Authenticator.setup(this.app); } catch (err) { eta.logger.error(err); return false; } this.express.all("/*", this.onRequest.bind(this)); this.setupHttpServer(); return true; } public async close(): Promise { await this.app.emit("server:stop"); if (this.server) { this.server.close(); } if (this.redirectServer) { this.redirectServer.close(); } } public start(): void { const onHttpServerError = (err: Error & { code: string; }) => { if (err.code === "EADDRINUSE") { eta.logger.error("Can't start server, port is already in use."); this.close().then(() => process.exit(1)).catch(err => eta.logger.error(err)); } else { eta.logger.error("Web server error occurred: " + err.message); } }; this.server.on("error", onHttpServerError); if (this.redirectServer) { this.redirectServer.on("error", onHttpServerError); } const port: number = this.config.get(`http${this.config.get("https.enable") ? "s" : ""}.port`); this.server.listen(port, "0.0.0.0", () => { eta.logger.info("Web server (main) started on port " + port); (>this.app.emit("server:start")).catch(err => { eta.logger.error(err); }); }); if (this.redirectServer !== undefined) { this.redirectServer.listen(this.config.get("http.port"), () => { eta.logger.info("Web server (redirect) started on port " + this.config.get("http.port")); }); } } private configureExpress(): void { this.express.set("view engine", "pug"); if (this.config.get("dev.enable")) { this.express.locals.pretty = true; // render Pug as readable HTML this.express.disable("view cache"); // pull Pug views from filesystem on request } } /** * Initializes ExpressJS middleware */ private setupMiddleware(): void { this.middleware = { session: expressSession({ // sets up req.session provider store: new (redisSession(expressSession))({ client: this.app.redis }), resave: true, saveUninitialized: false, secret: this.config.get("session.secret") }), multer: multer({ // sets up support for file uploads storage: multer.memoryStorage() }).any(), bodyParser: bodyParser.urlencoded({ // sets up support for standard POST bodies extended: true }), bodyParserJSON: bodyParser.json(), passport: passport.initialize(), passportSession: passport.session() }; if (this.config.get("dev.enable")) { // basically disable CORS if dev mode is enabled this.express.all("*", cors({ origin: true, optionsSuccessStatus: 200, methods: ["GET", "HEAD", "PUT", "POST", "OPTIONS", "DELETE", "PATCH"], credentials: true })); } // check for a static file before anything else this.express.use((req, res, next) => { const handler = this.createRequestHandler(req, res, next); (async () => { if (await handler.isStaticFile()) { return handler.handleRequest(); } next(); })().catch(err => eta.logger.error(err)); }); Object.keys(this.middleware).forEach(m => { this.express.use(this.middleware[m]); }); } private onRequest(req: express.Request, res: express.Response, next: express.NextFunction): void { this.createRequestHandler(req, res, next) .handleRequest().catch(err => { eta.logger.error(err); }); } private setupHttpServer(): void { if (!this.config.get("https.enable")) { // only http this.server = http.createServer(this.express); return; } // HTTPS (and redirect server) const sslOptions: any = { "key": fs.readFileSync(this.config.get("https.privateKey") || this.config.get("https.key")), "cert": fs.readFileSync(this.config.get("https.publicKey") || this.config.get("https.cert")), "secureProtocol": "TLSv1_2_method" }; const chainFilename: string = this.config.get("https.chainKey") || this.config.get("https.ca"); if (chainFilename) { sslOptions.ca = fs.readFileSync(chainFilename); } this.server = https.createServer(sslOptions, this.express); this.redirectServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { res.writeHead(301, { Location: "https://" + this.config.get("http.host") + ":" + this.config.get("https.realPort") + req.url }); res.end(); }); } private createRequestHandler(req: express.Request, res: express.Response, next: express.NextFunction) { return new RequestHandler({ req, res, next, app: this.app, config: this.app.configs[req.hostname] || this.config, db: new RepositoryManager(req.hostname) }); } }