import * as express from 'express'; import * as expressValidator from 'express-validator'; import * as session from 'express-session'; import * as passport from 'passport'; import * as bodyParser from 'body-parser'; import * as cookieParser from 'cookie-parser'; import expressFlash = require('express-flash'); import flash = require('connect-flash'); import { Database } from './database'; import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager'; const MongoStore = require('connect-mongo')(session); import RouteManager from './RouteManager'; import { WebSocket } from './websocket'; import * as webpack from 'webpack'; const config = require('../../webpack.config'); const compiler = webpack(config); import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; import * as fs from 'fs'; import * as request from 'request'; import RouteSubscriber from './RouteSubscriber'; import { publicDirectory } from '.'; import { logPort } from './ActionUtilities'; import { blue, yellow } from 'colors'; import * as cors from "cors"; import { createServer, Server as HttpsServer } from "https"; import { Server as HttpServer } from "http"; import { SSL } from './apis/google/CredentialsLoader'; /* RouteSetter is a wrapper around the server that prevents the server from being exposed. */ export type RouteSetter = (server: RouteManager) => void; export let disconnect: Function; export let resolvedPorts: { server: number, socket: number } = { server: 1050, socket: 4321 }; export let resolvedServerUrl: string; export default async function InitializeServer(routeSetter: RouteSetter) { const app = buildWithMiddleware(express()); app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*") })); app.use("/images", express.static(publicDirectory)); app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) })); app.use(wdm(compiler, { publicPath: config.output.publicPath })); app.use(whm(compiler)); registerAuthenticationRoutes(app); registerCorsProxy(app); const isRelease = determineEnvironment(); isRelease && !SSL.Loaded && SSL.exit(); routeSetter(new RouteManager(app, isRelease)); registerRelativePath(app); let server: HttpServer | HttpsServer; const { serverPort, serverName } = process.env; isRelease && serverPort && (resolvedPorts.server = Number(serverPort)); await new Promise(resolve => server = isRelease ? createServer(SSL.Credentials, app).listen(resolvedPorts.server, resolve) : app.listen(resolvedPorts.server, resolve) ); logPort("server", resolvedPorts.server); resolvedServerUrl = `${isRelease && serverName ? `https://${serverName}.com` : "http://localhost"}:${resolvedPorts.server}`; // initialize the web socket (bidirectional communication: if a user changes // a field on one client, that change must be broadcast to all other clients) await WebSocket.initialize(isRelease, app); disconnect = async () => new Promise(resolve => server.close(resolve)); return isRelease; } const week = 7 * 24 * 60 * 60 * 1000; const secret = "64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc"; function buildWithMiddleware(server: express.Express) { [ cookieParser(), session({ secret, resave: true, cookie: { maxAge: week }, saveUninitialized: true, store: process.env.DB === "MEM" ? new session.MemoryStore() : new MongoStore({ url: Database.url }) }), flash(), expressFlash(), bodyParser.json({ limit: "10mb" }), bodyParser.urlencoded({ extended: true }), expressValidator(), passport.initialize(), passport.session(), (req: express.Request, res: express.Response, next: express.NextFunction) => { res.locals.user = req.user; next(); } ].forEach(next => server.use(next)); return server; } /* Determine if the enviroment is dev mode or release mode. */ function determineEnvironment() { const isRelease = process.env.RELEASE === "true"; const color = isRelease ? blue : yellow; const label = isRelease ? "release" : "development"; console.log(`\nrunning server in ${color(label)} mode`); // swilkins: I don't think we need to read from ClientUtils.RELEASE anymore. Should be able to invoke process.env.RELEASE // on the client side, thanks to dotenv in webpack.config.js let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8"); clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(isRelease))}`; fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8"); return isRelease; } function registerAuthenticationRoutes(server: express.Express) { server.get("/signup", getSignup); server.post("/signup", postSignup); server.get("/login", getLogin); server.post("/login", postLogin); server.get("/logout", getLogout); server.get("/forgotPassword", getForgot); server.post("/forgotPassword", postForgot); const reset = new RouteSubscriber("resetPassword").add("token").build; server.get(reset, getReset); server.post(reset, postReset); } function registerCorsProxy(server: express.Express) { const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; server.use("/corsProxy", async (req, res) => { const requrl = decodeURIComponent(req.url.substring(1)); const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : ""; // cors weirdness here... // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/ and the requested url path was relative, // then we redirect again to the cors referer and just add the relative path. if (!requrl.startsWith("http") && req.originalUrl.startsWith("/corsProxy") && referer?.includes("corsProxy")) { res.redirect(referer + (referer.endsWith("/") ? "" : "/") + requrl); } else { try { await new Promise((resolve, reject) => { request(requrl).on("response", resolve).on("error", reject); }); } catch { console.log(`Malformed CORS url: ${requrl}`); return res.send(); } req.pipe(request(requrl)).on("response", res => { const headers = Object.keys(res.headers); headers.forEach(headerName => { const header = res.headers[headerName]; if (Array.isArray(header)) { res.headers[headerName] = header.filter(h => !headerCharRegex.test(h)); } else if (header) { if (headerCharRegex.test(header as any)) { delete res.headers[headerName]; } } }); }).on("error", () => console.log(`Malformed CORS url: ${requrl}`)).pipe(res); } }); } function registerRelativePath(server: express.Express) { server.use("*", (req, res) => { const relativeUrl = req.originalUrl; if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here. const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:/corsProxy/https://en.wikipedia.org/wiki/Engelbart) const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:/corsProxy/ ) const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ""); // the url of the referer without the proxy (e.g., : http:s//en.wikipedia.org/wiki/Engelbart) const absoluteTargetBaseUrl = actualReferUrl.match(/http[s]?:\/\/[^\/]*/)![0]; // the base of the original url (e.g., https://en.wikipedia.org) const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:/corsProxy/https://en.wikipedia.org/) res.redirect(redirectedProxiedUrl); } else if (relativeUrl.startsWith("/search")) { // detect search query and use default search engine res.redirect(req.headers.referer + "corsProxy/" + encodeURIComponent("http://www.google.com" + relativeUrl)); } else { res.end(); } }); }