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