Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/hub/servers/express-app.ts
Views: 687
/*1The main hub express app.2*/34import compression from "compression";5import cookieParser from "cookie-parser";6import express from "express";7import ms from "ms";8import { join } from "path";9import { parse as parseURL } from "url";10import webpackDevMiddleware from "webpack-dev-middleware";11import webpackHotMiddleware from "webpack-hot-middleware";12import { path as WEBAPP_PATH } from "@cocalc/assets";13import basePath from "@cocalc/backend/base-path";14import { path as CDN_PATH } from "@cocalc/cdn";15import vhostShare from "@cocalc/next/lib/share/virtual-hosts";16import { path as STATIC_PATH } from "@cocalc/static";17import { initAnalytics } from "../analytics";18import { setup_health_checks as setupHealthChecks } from "../health-checks";19import { getLogger } from "../logger";20import initProxy from "../proxy";21import initAPI from "./app/api";22import initAppRedirect from "./app/app-redirect";23import initBlobUpload from "./app/blob-upload";24import initBlobs from "./app/blobs";25import initCustomize from "./app/customize";26import { initMetricsEndpoint, setupInstrumentation } from "./app/metrics";27import initNext from "./app/next";28import initSetCookies from "./app/set-cookies";29import initStats from "./app/stats";30import initStripeWebhook from "./app/webhooks/stripe";31import { database } from "./database";32import initHttpServer from "./http";33import initRobots from "./robots";3435// Used for longterm caching of files. This should be in units of seconds.36const MAX_AGE = Math.round(ms("10 days") / 1000);37const SHORT_AGE = Math.round(ms("10 seconds") / 1000);3839interface Options {40projectControl;41isPersonal: boolean;42nextServer: boolean;43proxyServer: boolean;44cert?: string;45key?: string;46listenersHack: boolean;47}4849export default async function init(opts: Options): Promise<{50httpServer;51router: express.Router;52}> {53const winston = getLogger("express-app");54winston.info("creating express app");5556// Create an express application57const app = express();58app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/61015960// makes JSON (e.g. the /customize endpoint) pretty-printed61app.set("json spaces", 2);6263// healthchecks are for internal use, no basePath prefix64// they also have to come first, since e.g. the vhost depends65// on the DB, which could be down66const basicEndpoints = express.Router();67await setupHealthChecks({ router: basicEndpoints, db: database });68app.use(basicEndpoints);6970// also, for the same reasons as above, setup the /metrics endpoint71initMetricsEndpoint(basicEndpoints);7273// now, we build the router for some other endpoints74const router = express.Router();7576// This must go very early - we handle virtual hosts, like wstein.org77// before any other routes or middleware interfere.78if (opts.nextServer) {79app.use(vhostShare());80}8182// Enable compression, as suggested by83// http://expressjs.com/en/advanced/best-practice-performance.html#use-gzip-compression84// NOTE "Express runs everything in order" --85// https://github.com/expressjs/compression/issues/35#issuecomment-7707617086app.use(compression());8788app.use(cookieParser());8990// Install custom middleware to track response time metrics via prometheus91setupInstrumentation(router);9293// see http://stackoverflow.com/questions/10849687/express-js-how-to-get-remote-client-address94app.enable("trust proxy");9596router.use("/robots.txt", initRobots());9798// setup the analytics.js endpoint99await initAnalytics(router, database);100101initAPI(router, opts.projectControl);102103// The /static content, used by docker, development, etc.104// This is the stuff that's packaged up via webpack in packages/static.105await initStatic(router);106107// Static assets that are used by the webapp, the landing page, etc.108router.use(109"/webapp",110express.static(WEBAPP_PATH, { setHeaders: cacheLongTerm }),111);112113// This is @cocalc/cdn – cocalc serves everything it might get from a CDN on its own.114// This is defined in the @cocalc/cdn package. See the comments in packages/cdn.115router.use("/cdn", express.static(CDN_PATH, { setHeaders: cacheLongTerm }));116117// Redirect requests to /app to /static/app.html.118// TODO: this will likely go away when rewrite the landing pages to not119// redirect users to /app in the first place.120router.get("/app", (req, res) => {121// query is exactly "?key=value,key=..."122const query = parseURL(req.url, true).search || "";123res.redirect(join(basePath, "static/app.html") + query);124});125126initBlobs(router);127initBlobUpload(router);128initStripeWebhook(router);129initSetCookies(router);130initCustomize(router, opts.isPersonal);131initStats(router);132initAppRedirect(router);133134if (basePath !== "/") {135app.use(basePath, router);136} else {137app.use(router);138}139140const httpServer = initHttpServer({141cert: opts.cert,142key: opts.key,143app,144});145146if (opts.proxyServer) {147winston.info(`initializing the http proxy server`);148initProxy({149projectControl: opts.projectControl,150isPersonal: opts.isPersonal,151httpServer,152app,153listenersHack: opts.listenersHack,154});155}156157// IMPORTANT:158// The nextjs server must be **LAST** (!), since it takes159// all routes not otherwise handled above.160if (opts.nextServer) {161// The Next.js server162await initNext(app);163}164165return { httpServer, router };166}167168function cacheShortTerm(res) {169res.setHeader(170"Cache-Control",171`public, max-age=${SHORT_AGE}, must-revalidate`,172);173res.setHeader(174"Expires",175new Date(Date.now().valueOf() + SHORT_AGE).toUTCString(),176);177}178179// Various files such as the webpack static content should be cached long-term,180// and we use this function to set appropriate headers at various points below.181function cacheLongTerm(res) {182res.setHeader(183"Cache-Control",184`public, max-age=${MAX_AGE}, must-revalidate'`,185);186res.setHeader(187"Expires",188new Date(Date.now().valueOf() + MAX_AGE).toUTCString(),189);190}191192async function initStatic(router) {193let compiler: any = null;194if (195process.env.NODE_ENV != "production" &&196!process.env.NO_RSPACK_DEV_SERVER197) {198// Try to use the integrated rspack dev server, if it is installed.199// It might not be installed at all, e.g., in production, and there200// @cocalc/static can't even be imported.201try {202const { rspackCompiler } = require("@cocalc/static/rspack-compiler");203compiler = rspackCompiler();204} catch (err) {205console.warn("rspack is not available", err);206}207}208209if (compiler != null) {210console.warn(211"\n-----------\n| RSPACK: Running rspack dev server for frontend /static app.\n| Set env variable NO_RSPACK_DEV_SERVER to disable.\n-----------\n",212);213router.use("/static", webpackDevMiddleware(compiler, {}));214router.use("/static", webpackHotMiddleware(compiler, {}));215} else {216router.use(217join("/static", STATIC_PATH, "app.html"),218express.static(join(STATIC_PATH, "app.html"), {219setHeaders: cacheShortTerm,220}),221);222router.use(223"/static",224express.static(STATIC_PATH, { setHeaders: cacheLongTerm }),225);226}227228// Also, immediately 404 if anything else under static is requested229// which isn't handled above, rather than passing this on to the next app230router.use("/static", (_, res) => res.status(404).end());231}232233234