CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/hub/servers/express-app.ts
Views: 923
1
/*
2
The main hub express app.
3
*/
4
5
import compression from "compression";
6
import cookieParser from "cookie-parser";
7
import express from "express";
8
import ms from "ms";
9
import { join } from "path";
10
import { parse as parseURL } from "url";
11
import webpackDevMiddleware from "webpack-dev-middleware";
12
import webpackHotMiddleware from "webpack-hot-middleware";
13
import { path as WEBAPP_PATH } from "@cocalc/assets";
14
import basePath from "@cocalc/backend/base-path";
15
import { path as CDN_PATH } from "@cocalc/cdn";
16
import vhostShare from "@cocalc/next/lib/share/virtual-hosts";
17
import { path as STATIC_PATH } from "@cocalc/static";
18
import { initAnalytics } from "../analytics";
19
import { setup_health_checks as setupHealthChecks } from "../health-checks";
20
import { getLogger } from "../logger";
21
import initProxy from "../proxy";
22
import initAPI from "./app/api";
23
import initAppRedirect from "./app/app-redirect";
24
import initBlobUpload from "./app/blob-upload";
25
import initBlobs from "./app/blobs";
26
import initCustomize from "./app/customize";
27
import { initMetricsEndpoint, setupInstrumentation } from "./app/metrics";
28
import initNext from "./app/next";
29
import initSetCookies from "./app/set-cookies";
30
import initStats from "./app/stats";
31
import { database } from "./database";
32
import initHttpServer from "./http";
33
import initRobots from "./robots";
34
35
// Used for longterm caching of files. This should be in units of seconds.
36
const MAX_AGE = Math.round(ms("10 days") / 1000);
37
const SHORT_AGE = Math.round(ms("10 seconds") / 1000);
38
39
interface Options {
40
projectControl;
41
isPersonal: boolean;
42
nextServer: boolean;
43
proxyServer: boolean;
44
cert?: string;
45
key?: string;
46
listenersHack: boolean;
47
}
48
49
export default async function init(opts: Options): Promise<{
50
httpServer;
51
router: express.Router;
52
}> {
53
const winston = getLogger("express-app");
54
winston.info("creating express app");
55
56
// Create an express application
57
const app = express();
58
app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/6101
59
60
// makes JSON (e.g. the /customize endpoint) pretty-printed
61
app.set("json spaces", 2);
62
63
// healthchecks are for internal use, no basePath prefix
64
// they also have to come first, since e.g. the vhost depends
65
// on the DB, which could be down
66
const basicEndpoints = express.Router();
67
await setupHealthChecks({ router: basicEndpoints, db: database });
68
app.use(basicEndpoints);
69
70
// also, for the same reasons as above, setup the /metrics endpoint
71
initMetricsEndpoint(basicEndpoints);
72
73
// now, we build the router for some other endpoints
74
const router = express.Router();
75
76
// This must go very early - we handle virtual hosts, like wstein.org
77
// before any other routes or middleware interfere.
78
if (opts.nextServer) {
79
app.use(vhostShare());
80
}
81
82
// Enable compression, as suggested by
83
// http://expressjs.com/en/advanced/best-practice-performance.html#use-gzip-compression
84
// NOTE "Express runs everything in order" --
85
// https://github.com/expressjs/compression/issues/35#issuecomment-77076170
86
app.use(compression());
87
88
app.use(cookieParser());
89
90
// Install custom middleware to track response time metrics via prometheus
91
setupInstrumentation(router);
92
93
// see http://stackoverflow.com/questions/10849687/express-js-how-to-get-remote-client-address
94
app.enable("trust proxy");
95
96
router.use("/robots.txt", initRobots());
97
98
// setup the analytics.js endpoint
99
await initAnalytics(router, database);
100
101
initAPI(router, opts.projectControl);
102
103
// The /static content, used by docker, development, etc.
104
// This is the stuff that's packaged up via webpack in packages/static.
105
await initStatic(router);
106
107
// Static assets that are used by the webapp, the landing page, etc.
108
router.use(
109
"/webapp",
110
express.static(WEBAPP_PATH, { setHeaders: cacheLongTerm }),
111
);
112
113
// 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.
115
router.use("/cdn", express.static(CDN_PATH, { setHeaders: cacheLongTerm }));
116
117
// Redirect requests to /app to /static/app.html.
118
// TODO: this will likely go away when rewrite the landing pages to not
119
// redirect users to /app in the first place.
120
router.get("/app", (req, res) => {
121
// query is exactly "?key=value,key=..."
122
const query = parseURL(req.url, true).search || "";
123
res.redirect(join(basePath, "static/app.html") + query);
124
});
125
126
initBlobs(router);
127
initBlobUpload(router);
128
initSetCookies(router);
129
initCustomize(router, opts.isPersonal);
130
initStats(router);
131
initAppRedirect(router);
132
133
if (basePath !== "/") {
134
app.use(basePath, router);
135
} else {
136
app.use(router);
137
}
138
139
const httpServer = initHttpServer({
140
cert: opts.cert,
141
key: opts.key,
142
app,
143
});
144
145
if (opts.proxyServer) {
146
winston.info(`initializing the http proxy server`);
147
initProxy({
148
projectControl: opts.projectControl,
149
isPersonal: opts.isPersonal,
150
httpServer,
151
app,
152
listenersHack: opts.listenersHack,
153
});
154
}
155
156
// IMPORTANT:
157
// The nextjs server must be **LAST** (!), since it takes
158
// all routes not otherwise handled above.
159
if (opts.nextServer) {
160
// The Next.js server
161
await initNext(app);
162
}
163
164
return { httpServer, router };
165
}
166
167
function cacheShortTerm(res) {
168
res.setHeader(
169
"Cache-Control",
170
`public, max-age=${SHORT_AGE}, must-revalidate`,
171
);
172
res.setHeader(
173
"Expires",
174
new Date(Date.now().valueOf() + SHORT_AGE).toUTCString(),
175
);
176
}
177
178
// Various files such as the webpack static content should be cached long-term,
179
// and we use this function to set appropriate headers at various points below.
180
function cacheLongTerm(res) {
181
res.setHeader(
182
"Cache-Control",
183
`public, max-age=${MAX_AGE}, must-revalidate'`,
184
);
185
res.setHeader(
186
"Expires",
187
new Date(Date.now().valueOf() + MAX_AGE).toUTCString(),
188
);
189
}
190
191
async function initStatic(router) {
192
let compiler: any = null;
193
if (
194
process.env.NODE_ENV != "production" &&
195
!process.env.NO_RSPACK_DEV_SERVER
196
) {
197
// Try to use the integrated rspack dev server, if it is installed.
198
// It might not be installed at all, e.g., in production, and there
199
// @cocalc/static can't even be imported.
200
try {
201
const { rspackCompiler } = require("@cocalc/static/rspack-compiler");
202
compiler = rspackCompiler();
203
} catch (err) {
204
console.warn("rspack is not available", err);
205
}
206
}
207
208
if (compiler != null) {
209
console.warn(
210
"\n-----------\n| RSPACK: Running rspack dev server for frontend /static app.\n| Set env variable NO_RSPACK_DEV_SERVER to disable.\n-----------\n",
211
);
212
router.use("/static", webpackDevMiddleware(compiler, {}));
213
router.use("/static", webpackHotMiddleware(compiler, {}));
214
} else {
215
router.use(
216
join("/static", STATIC_PATH, "app.html"),
217
express.static(join(STATIC_PATH, "app.html"), {
218
setHeaders: cacheShortTerm,
219
}),
220
);
221
router.use(
222
"/static",
223
express.static(STATIC_PATH, { setHeaders: cacheLongTerm }),
224
);
225
}
226
227
// Also, immediately 404 if anything else under static is requested
228
// which isn't handled above, rather than passing this on to the next app
229
router.use("/static", (_, res) => res.status(404).end());
230
}
231
232