Path: blob/master/src/packages/hub/servers/express-app.ts
1503 views
/*1The main hub express app.2*/34import cookieParser from "cookie-parser";5import express from "express";6import ms from "ms";7import { join } from "path";8import { parse as parseURL } from "url";9import webpackDevMiddleware from "webpack-dev-middleware";10import webpackHotMiddleware from "webpack-hot-middleware";11import { path as WEBAPP_PATH } from "@cocalc/assets";12import { path as CDN_PATH } from "@cocalc/cdn";13import vhostShare from "@cocalc/next/lib/share/virtual-hosts";14import { path as STATIC_PATH } from "@cocalc/static";15import { initAnalytics } from "../analytics";16import { setup_health_checks as setupHealthChecks } from "../health-checks";17import { getLogger } from "../logger";18import initProxy from "../proxy";19import initAppRedirect from "./app/app-redirect";20import initBlobUpload from "./app/blob-upload";21import initUpload from "./app/upload";22import initBlobs from "./app/blobs";23import initCustomize from "./app/customize";24import { initMetricsEndpoint, setupInstrumentation } from "./app/metrics";25import initNext from "./app/next";26import initStats from "./app/stats";27import { database } from "./database";28import initHttpServer from "./http";29import initRobots from "./robots";30import basePath from "@cocalc/backend/base-path";31import { initConatServer } from "@cocalc/server/conat/socketio";32import { conatSocketioCount } from "@cocalc/backend/data";3334// NOTE: we are not using compression because that interferes with streaming file download,35// and could be generally confusing.3637// Used for longterm caching of files. This should be in units of seconds.38const MAX_AGE = Math.round(ms("10 days") / 1000);39const SHORT_AGE = Math.round(ms("10 seconds") / 1000);4041interface Options {42projectControl;43isPersonal: boolean;44nextServer: boolean;45proxyServer: boolean;46conatServer: boolean;47cert?: string;48key?: string;49}5051export default async function init(opts: Options): Promise<{52httpServer;53router: express.Router;54}> {55const winston = getLogger("express-app");56winston.info("creating express app");5758// Create an express application59const app = express();60app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/61016162// makes JSON (e.g. the /customize endpoint) pretty-printed63app.set("json spaces", 2);6465// healthchecks are for internal use, no basePath prefix66// they also have to come first, since e.g. the vhost depends67// on the DB, which could be down68const basicEndpoints = express.Router();69await setupHealthChecks({ router: basicEndpoints, db: database });70app.use(basicEndpoints);7172// also, for the same reasons as above, setup the /metrics endpoint73initMetricsEndpoint(basicEndpoints);7475// now, we build the router for some other endpoints76const router = express.Router();7778// This must go very early - we handle virtual hosts, like wstein.org79// before any other routes or middleware interfere.80if (opts.nextServer) {81app.use(vhostShare());82}8384app.use(cookieParser());8586// Install custom middleware to track response time metrics via prometheus87setupInstrumentation(router);8889// see http://stackoverflow.com/questions/10849687/express-js-how-to-get-remote-client-address90app.enable("trust proxy");9192router.use("/robots.txt", initRobots());9394// setup the analytics.js endpoint95await initAnalytics(router, database);9697// The /static content, used by docker, development, etc.98// This is the stuff that's packaged up via webpack in packages/static.99await initStatic(router);100101// Static assets that are used by the webapp, the landing page, etc.102router.use(103"/webapp",104express.static(WEBAPP_PATH, { setHeaders: cacheLongTerm }),105);106107// This is @cocalc/cdn – cocalc serves everything it might get from a CDN on its own.108// This is defined in the @cocalc/cdn package. See the comments in packages/cdn.109router.use("/cdn", express.static(CDN_PATH, { setHeaders: cacheLongTerm }));110111// Redirect requests to /app to /static/app.html.112// TODO: this will likely go away when rewrite the landing pages to not113// redirect users to /app in the first place.114router.get("/app", (req, res) => {115// query is exactly "?key=value,key=..."116const query = parseURL(req.url, true).search || "";117res.redirect(join(basePath, "static/app.html") + query);118});119120initBlobs(router);121initBlobUpload(router);122initUpload(router);123initCustomize(router, opts.isPersonal);124initStats(router);125initAppRedirect(router);126127if (basePath !== "/") {128app.use(basePath, router);129} else {130app.use(router);131}132133const httpServer = initHttpServer({134cert: opts.cert,135key: opts.key,136app,137});138139if (opts.conatServer) {140winston.info(`initializing the Conat Server`);141initConatServer({142httpServer,143ssl: !!opts.cert,144});145}146147// This must be second to the last, since it will prevent any148// other upgrade handlers from being added to httpServer.149if (opts.proxyServer) {150winston.info(`initializing the http proxy server`, {151conatSocketioCount,152conatServer: !!opts.conatServer,153isPersonal: opts.isPersonal,154});155initProxy({156projectControl: opts.projectControl,157isPersonal: opts.isPersonal,158httpServer,159app,160// enable proxy server for /conat if:161// (1) we are not running conat at all from here, or162// (2) we are running socketio in cluster mode, hence163// on a different port164proxyConat: !opts.conatServer || (conatSocketioCount ?? 1) >= 2,165});166}167168// IMPORTANT:169// The nextjs server must be **LAST** (!), since it takes170// all routes not otherwise handled above.171if (opts.nextServer) {172// The Next.js server173await initNext(app);174}175return { httpServer, router };176}177178function cacheShortTerm(res) {179res.setHeader(180"Cache-Control",181`public, max-age=${SHORT_AGE}, must-revalidate`,182);183res.setHeader(184"Expires",185new Date(Date.now().valueOf() + SHORT_AGE).toUTCString(),186);187}188189// Various files such as the webpack static content should be cached long-term,190// and we use this function to set appropriate headers at various points below.191function cacheLongTerm(res) {192res.setHeader(193"Cache-Control",194`public, max-age=${MAX_AGE}, must-revalidate'`,195);196res.setHeader(197"Expires",198new Date(Date.now().valueOf() + MAX_AGE).toUTCString(),199);200}201202async function initStatic(router) {203let compiler: any = null;204if (205process.env.NODE_ENV != "production" &&206!process.env.NO_RSPACK_DEV_SERVER207) {208// Try to use the integrated rspack dev server, if it is installed.209// It might not be installed at all, e.g., in production, and there210// @cocalc/static can't even be imported.211try {212const { rspackCompiler } = require("@cocalc/static/rspack-compiler");213compiler = rspackCompiler();214} catch (err) {215console.warn("rspack is not available", err);216}217}218219if (compiler != null) {220console.warn(221"\n-----------\n| RSPACK: Running rspack dev server for frontend /static app.\n| Set env variable NO_RSPACK_DEV_SERVER to disable.\n-----------\n",222);223router.use("/static", webpackDevMiddleware(compiler, {}));224router.use("/static", webpackHotMiddleware(compiler, {}));225} else {226router.use(227join("/static", STATIC_PATH, "app.html"),228express.static(join(STATIC_PATH, "app.html"), {229setHeaders: cacheShortTerm,230}),231);232router.use(233"/static",234express.static(STATIC_PATH, { setHeaders: cacheLongTerm }),235);236}237238// Also, immediately 404 if anything else under static is requested239// which isn't handled above, rather than passing this on to the next app240router.use("/static", (_, res) => res.status(404).end());241}242243244