Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/src/packages/hub/servers/express-app.ts
Views: 923
/*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 { database } from "./database";31import initHttpServer from "./http";32import initRobots from "./robots";3334// Used for longterm caching of files. This should be in units of seconds.35const MAX_AGE = Math.round(ms("10 days") / 1000);36const SHORT_AGE = Math.round(ms("10 seconds") / 1000);3738interface Options {39projectControl;40isPersonal: boolean;41nextServer: boolean;42proxyServer: boolean;43cert?: string;44key?: string;45listenersHack: boolean;46}4748export default async function init(opts: Options): Promise<{49httpServer;50router: express.Router;51}> {52const winston = getLogger("express-app");53winston.info("creating express app");5455// Create an express application56const app = express();57app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/61015859// makes JSON (e.g. the /customize endpoint) pretty-printed60app.set("json spaces", 2);6162// healthchecks are for internal use, no basePath prefix63// they also have to come first, since e.g. the vhost depends64// on the DB, which could be down65const basicEndpoints = express.Router();66await setupHealthChecks({ router: basicEndpoints, db: database });67app.use(basicEndpoints);6869// also, for the same reasons as above, setup the /metrics endpoint70initMetricsEndpoint(basicEndpoints);7172// now, we build the router for some other endpoints73const router = express.Router();7475// This must go very early - we handle virtual hosts, like wstein.org76// before any other routes or middleware interfere.77if (opts.nextServer) {78app.use(vhostShare());79}8081// Enable compression, as suggested by82// http://expressjs.com/en/advanced/best-practice-performance.html#use-gzip-compression83// NOTE "Express runs everything in order" --84// https://github.com/expressjs/compression/issues/35#issuecomment-7707617085app.use(compression());8687app.use(cookieParser());8889// Install custom middleware to track response time metrics via prometheus90setupInstrumentation(router);9192// see http://stackoverflow.com/questions/10849687/express-js-how-to-get-remote-client-address93app.enable("trust proxy");9495router.use("/robots.txt", initRobots());9697// setup the analytics.js endpoint98await initAnalytics(router, database);99100initAPI(router, opts.projectControl);101102// The /static content, used by docker, development, etc.103// This is the stuff that's packaged up via webpack in packages/static.104await initStatic(router);105106// Static assets that are used by the webapp, the landing page, etc.107router.use(108"/webapp",109express.static(WEBAPP_PATH, { setHeaders: cacheLongTerm }),110);111112// This is @cocalc/cdn – cocalc serves everything it might get from a CDN on its own.113// This is defined in the @cocalc/cdn package. See the comments in packages/cdn.114router.use("/cdn", express.static(CDN_PATH, { setHeaders: cacheLongTerm }));115116// Redirect requests to /app to /static/app.html.117// TODO: this will likely go away when rewrite the landing pages to not118// redirect users to /app in the first place.119router.get("/app", (req, res) => {120// query is exactly "?key=value,key=..."121const query = parseURL(req.url, true).search || "";122res.redirect(join(basePath, "static/app.html") + query);123});124125initBlobs(router);126initBlobUpload(router);127initSetCookies(router);128initCustomize(router, opts.isPersonal);129initStats(router);130initAppRedirect(router);131132if (basePath !== "/") {133app.use(basePath, router);134} else {135app.use(router);136}137138const httpServer = initHttpServer({139cert: opts.cert,140key: opts.key,141app,142});143144if (opts.proxyServer) {145winston.info(`initializing the http proxy server`);146initProxy({147projectControl: opts.projectControl,148isPersonal: opts.isPersonal,149httpServer,150app,151listenersHack: opts.listenersHack,152});153}154155// IMPORTANT:156// The nextjs server must be **LAST** (!), since it takes157// all routes not otherwise handled above.158if (opts.nextServer) {159// The Next.js server160await initNext(app);161}162163return { httpServer, router };164}165166function cacheShortTerm(res) {167res.setHeader(168"Cache-Control",169`public, max-age=${SHORT_AGE}, must-revalidate`,170);171res.setHeader(172"Expires",173new Date(Date.now().valueOf() + SHORT_AGE).toUTCString(),174);175}176177// Various files such as the webpack static content should be cached long-term,178// and we use this function to set appropriate headers at various points below.179function cacheLongTerm(res) {180res.setHeader(181"Cache-Control",182`public, max-age=${MAX_AGE}, must-revalidate'`,183);184res.setHeader(185"Expires",186new Date(Date.now().valueOf() + MAX_AGE).toUTCString(),187);188}189190async function initStatic(router) {191let compiler: any = null;192if (193process.env.NODE_ENV != "production" &&194!process.env.NO_RSPACK_DEV_SERVER195) {196// Try to use the integrated rspack dev server, if it is installed.197// It might not be installed at all, e.g., in production, and there198// @cocalc/static can't even be imported.199try {200const { rspackCompiler } = require("@cocalc/static/rspack-compiler");201compiler = rspackCompiler();202} catch (err) {203console.warn("rspack is not available", err);204}205}206207if (compiler != null) {208console.warn(209"\n-----------\n| RSPACK: Running rspack dev server for frontend /static app.\n| Set env variable NO_RSPACK_DEV_SERVER to disable.\n-----------\n",210);211router.use("/static", webpackDevMiddleware(compiler, {}));212router.use("/static", webpackHotMiddleware(compiler, {}));213} else {214router.use(215join("/static", STATIC_PATH, "app.html"),216express.static(join(STATIC_PATH, "app.html"), {217setHeaders: cacheShortTerm,218}),219);220router.use(221"/static",222express.static(STATIC_PATH, { setHeaders: cacheLongTerm }),223);224}225226// Also, immediately 404 if anything else under static is requested227// which isn't handled above, rather than passing this on to the next app228router.use("/static", (_, res) => res.status(404).end());229}230231232