Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/hub/proxy/handle-request.ts
1496 views
1
/* Handle a proxy request */
2
3
import { createProxyServer, type ProxyServer } from "http-proxy-3";
4
import LRU from "lru-cache";
5
import stripRememberMeCookie from "./strip-remember-me-cookie";
6
import { versionCheckFails } from "./version";
7
import { getTarget } from "./target";
8
import getLogger from "../logger";
9
import { stripBasePath } from "./util";
10
import { ProjectControlFunction } from "@cocalc/server/projects/control";
11
import siteUrl from "@cocalc/database/settings/site-url";
12
import { parseReq } from "./parse";
13
import { readFile as readProjectFile } from "@cocalc/conat/files/read";
14
import { path_split } from "@cocalc/util/misc";
15
import { once } from "@cocalc/util/async-utils";
16
import hasAccess from "./check-for-access-to-project";
17
import mime from "mime-types";
18
19
const DANGEROUS_CONTENT_TYPE = new Set(["image/svg+xml" /*, "text/html"*/]);
20
21
const logger = getLogger("proxy:handle-request");
22
23
interface Options {
24
projectControl: ProjectControlFunction;
25
isPersonal: boolean;
26
}
27
28
export default function init({ projectControl, isPersonal }: Options) {
29
/* Cache at most 5000 proxies, each for up to 3 minutes.
30
Throwing away proxies at any time from the cache is fine since
31
the proxy is just used to handle *individual* http requests,
32
and the cache is entirely for speed. Also, invalidating cache entries
33
works around weird cases, where maybe error/close don't get
34
properly called, but the proxy is not working due to network
35
issues. Invalidating cache entries quickly is also good from
36
a permissions and security point of view.
37
*/
38
39
const cache = new LRU<string, ProxyServer>({
40
max: 5000,
41
ttl: 1000 * 60 * 3,
42
});
43
44
async function handleProxyRequest(req, res): Promise<void> {
45
const dbg = (...args) => {
46
// for low level debugging -- silly isn't logged by default
47
logger.silly(req.url, ...args);
48
};
49
dbg("got request");
50
// dangerous/verbose to log...?
51
// dbg("headers = ", req.headers);
52
53
if (!isPersonal && versionCheckFails(req, res)) {
54
dbg("version check failed");
55
// note that the versionCheckFails function already sent back an error response.
56
throw Error("version check failed");
57
}
58
59
// Before doing anything further with the request on to the proxy, we remove **all** cookies whose
60
// name contains "remember_me", to prevent the project backend from getting at
61
// the user's session cookie, since one project shouldn't be able to get
62
// access to any user's account.
63
let remember_me, api_key;
64
if (req.headers["cookie"] != null) {
65
let cookie;
66
({ cookie, remember_me, api_key } = stripRememberMeCookie(
67
req.headers["cookie"],
68
));
69
req.headers["cookie"] = cookie;
70
}
71
72
if (!isPersonal && !remember_me && !api_key) {
73
dbg("no rememember me set, so blocking");
74
// Not in personal mode and there is no remember_me or api_key set all, so
75
// definitely block access. 4xx since this is a *client* problem.
76
const url = await siteUrl();
77
throw Error(
78
`Please login to <a target='_blank' href='${url}'>${url}</a> with cookies enabled, then refresh this page.`,
79
);
80
}
81
82
const url = stripBasePath(req.url);
83
const parsed = parseReq(url, remember_me, api_key);
84
// TODO: parseReq is called again in getTarget so need to refactor...
85
const { type, project_id } = parsed;
86
if (type == "files") {
87
dbg("handling the request via conat file streaming");
88
if (
89
!(await hasAccess({
90
project_id,
91
remember_me,
92
api_key,
93
type: "read",
94
isPersonal,
95
}))
96
) {
97
throw Error(`user does not have read access to project`);
98
}
99
const i = url.indexOf("files/");
100
const compute_server_id = req.query.id ?? 0;
101
let j = url.lastIndexOf("?");
102
if (j == -1) {
103
j = url.length;
104
}
105
const path = decodeURIComponent(url.slice(i + "files/".length, j));
106
dbg("conat: get file", { project_id, path, compute_server_id, url });
107
const fileName = path_split(path).tail;
108
const contentType = mime.lookup(fileName);
109
if (
110
req.query.download != null ||
111
DANGEROUS_CONTENT_TYPE.has(contentType)
112
) {
113
const fileNameEncoded = encodeURIComponent(fileName)
114
.replace(/['()]/g, escape)
115
.replace(/\*/g, "%2A");
116
res.setHeader(
117
"Content-disposition",
118
`attachment; filename*=UTF-8''${fileNameEncoded}`,
119
);
120
}
121
res.setHeader("Content-type", contentType);
122
for await (const chunk of await readProjectFile({
123
project_id,
124
compute_server_id,
125
path,
126
// allow a long download time (1 hour), since files can be large and
127
// networks can be slow.
128
maxWait: 1000 * 60 * 60,
129
})) {
130
if (!res.write(chunk)) {
131
// backpressure -- wait for it to resolve
132
await once(res, "drain");
133
}
134
}
135
res.end();
136
return;
137
}
138
139
const { host, port, internal_url } = await getTarget({
140
remember_me,
141
api_key,
142
url,
143
isPersonal,
144
projectControl,
145
parsed,
146
});
147
148
// It's http here because we've already got past the ssl layer. This is all internal.
149
const target = `http://${host}:${port}`;
150
dbg("target resolves to", target);
151
152
let proxy;
153
if (cache.has(target)) {
154
// we already have the proxy for this target in the cache
155
dbg("using cached proxy");
156
proxy = cache.get(target);
157
} else {
158
logger.debug("make a new proxy server to", target);
159
proxy = createProxyServer({
160
ws: false,
161
target,
162
});
163
// and cache it.
164
cache.set(target, proxy);
165
logger.debug("created new proxy");
166
167
proxy.on("error", (err) => {
168
logger.debug(`http proxy error -- ${err}`);
169
});
170
}
171
172
if (internal_url != null) {
173
dbg("changing req url from ", req.url, " to ", internal_url);
174
req.url = internal_url;
175
}
176
dbg("handling the request using the proxy");
177
proxy.web(req, res);
178
}
179
180
return async (req, res) => {
181
try {
182
await handleProxyRequest(req, res);
183
} catch (err) {
184
const msg = `WARNING: error proxying request ${req.url} -- ${err}`;
185
res.writeHead(426, { "Content-Type": "text/html" });
186
res.end(msg);
187
// Not something to log as an error -- just debug; it's normal for it to happen, e.g., when
188
// a project isn't running.
189
logger.debug(msg);
190
}
191
};
192
}
193
194