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.

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