Path: blob/master/src/packages/hub/servers/express-app.ts
5843 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, root } from "@cocalc/backend/data";3334const PYTHON_API_PATH = join(root, "python", "cocalc-api", "site");3536// NOTE: we are not using compression because that interferes with streaming file download,37// and could be generally confusing.3839// Used for longterm caching of files. This should be in units of seconds.40const MAX_AGE = Math.round(ms("10 days") / 1000);41const SHORT_AGE = Math.round(ms("10 seconds") / 1000);4243interface Options {44projectControl;45isPersonal: boolean;46nextServer: boolean;47proxyServer: boolean;48conatServer: boolean;49cert?: string;50key?: string;51}5253export default async function init(opts: Options): Promise<{54httpServer;55router: express.Router;56}> {57const winston = getLogger("express-app");58winston.info("creating express app");5960// Create an express application61const app = express();62app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/61016364// makes JSON (e.g. the /customize endpoint) pretty-printed65app.set("json spaces", 2);6667// healthchecks are for internal use, no basePath prefix68// they also have to come first, since e.g. the vhost depends69// on the DB, which could be down70const basicEndpoints = express.Router();71await setupHealthChecks({ router: basicEndpoints, db: database });72app.use(basicEndpoints);7374// also, for the same reasons as above, setup the /metrics endpoint75initMetricsEndpoint(basicEndpoints);7677// now, we build the router for some other endpoints78const router = express.Router();7980// This must go very early - we handle virtual hosts, like wstein.org81// before any other routes or middleware interfere.82if (opts.nextServer) {83app.use(vhostShare());84}8586app.use(cookieParser());8788// Install custom middleware to track response time metrics via prometheus89setupInstrumentation(router);9091// see http://stackoverflow.com/questions/10849687/express-js-how-to-get-remote-client-address92app.enable("trust proxy");9394router.use("/robots.txt", initRobots());9596// setup the analytics.js endpoint97await initAnalytics(router, database);9899// The /static content, used by docker, development, etc.100// This is the stuff that's packaged up via webpack in packages/static.101await initStatic(router);102103// Static assets that are used by the webapp, the landing page, etc.104router.use(105"/webapp",106express.static(WEBAPP_PATH, { setHeaders: cacheLongTerm }),107);108109// This is @cocalc/cdn – cocalc serves everything it might get from a CDN on its own.110// This is defined in the @cocalc/cdn package. See the comments in packages/cdn.111router.use("/cdn", express.static(CDN_PATH, { setHeaders: cacheLongTerm }));112113// Redirect requests to /app to /static/app.html.114// TODO: this will likely go away when rewrite the landing pages to not115// redirect users to /app in the first place.116router.get("/app", (req, res) => {117// query is exactly "?key=value,key=..."118const query = parseURL(req.url, true).search || "";119res.redirect(join(basePath, "static/app.html") + query);120});121122router.use("/api/python", express.static(PYTHON_API_PATH));123124initBlobs(router);125initBlobUpload(router);126initUpload(router);127initCustomize(router, opts.isPersonal);128initStats(router);129initAppRedirect(router);130131if (basePath !== "/") {132app.use(basePath, router);133} else {134app.use(router);135}136137const httpServer = initHttpServer({138cert: opts.cert,139key: opts.key,140app,141});142143if (opts.conatServer) {144winston.info(`initializing the Conat Server`);145initConatServer({146httpServer,147ssl: !!opts.cert,148});149}150151// This must be second to the last, since it will prevent any152// other upgrade handlers from being added to httpServer.153if (opts.proxyServer) {154winston.info(`initializing the http proxy server`, {155conatSocketioCount,156conatServer: !!opts.conatServer,157isPersonal: opts.isPersonal,158});159initProxy({160projectControl: opts.projectControl,161isPersonal: opts.isPersonal,162httpServer,163app,164// enable proxy server for /conat if:165// (1) we are not running conat at all from here, or166// (2) we are running socketio in cluster mode, hence167// on a different port168proxyConat: !opts.conatServer || (conatSocketioCount ?? 1) >= 2,169});170}171172// IMPORTANT:173// The nextjs server must be **LAST** (!), since it takes174// all routes not otherwise handled above.175if (opts.nextServer) {176// The Next.js server177await initNext(app);178}179return { httpServer, router };180}181182function cacheShortTerm(res) {183res.setHeader(184"Cache-Control",185`public, max-age=${SHORT_AGE}, must-revalidate`,186);187res.setHeader(188"Expires",189new Date(Date.now().valueOf() + SHORT_AGE).toUTCString(),190);191}192193// Various files such as the webpack static content should be cached long-term,194// and we use this function to set appropriate headers at various points below.195function cacheLongTerm(res) {196res.setHeader(197"Cache-Control",198`public, max-age=${MAX_AGE}, must-revalidate'`,199);200res.setHeader(201"Expires",202new Date(Date.now().valueOf() + MAX_AGE).toUTCString(),203);204}205206async function initStatic(router) {207let compiler: any = null;208if (209process.env.NODE_ENV != "production" &&210!process.env.NO_RSPACK_DEV_SERVER211) {212// Try to use the integrated rspack dev server, if it is installed.213// It might not be installed at all, e.g., in production, and there214// @cocalc/static can't even be imported.215try {216const { rspackCompiler } = require("@cocalc/static/rspack-compiler");217compiler = rspackCompiler();218} catch (err) {219console.warn("rspack is not available", err);220}221}222223if (compiler != null) {224console.warn(225"\n-----------\n| RSPACK: Running rspack dev server for frontend /static app.\n| Set env variable NO_RSPACK_DEV_SERVER to disable.\n-----------\n",226);227router.use("/static", webpackDevMiddleware(compiler, {}));228router.use("/static", webpackHotMiddleware(compiler, {}));229} else {230router.use(231join("/static", STATIC_PATH, "app.html"),232express.static(join(STATIC_PATH, "app.html"), {233setHeaders: cacheShortTerm,234}),235);236router.use(237"/static",238express.static(STATIC_PATH, { setHeaders: cacheLongTerm }),239);240}241242// Also, immediately 404 if anything else under static is requested243// which isn't handled above, rather than passing this on to the next app244router.use("/static", (_, res) => res.status(404).end());245}246247248