Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/hub/proxy/handle-request.ts
5714 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 hasAccess from "./check-for-access-to-project";
14
import { handleFileDownload } from "./file-download";
15
16
const logger = getLogger("proxy:handle-request");
17
18
interface Options {
19
projectControl: ProjectControlFunction;
20
isPersonal: boolean;
21
}
22
23
export default function init({ projectControl, isPersonal }: Options) {
24
/* Cache at most 5000 proxies, each for up to 3 minutes.
25
Throwing away proxies at any time from the cache is fine since
26
the proxy is just used to handle *individual* http requests,
27
and the cache is entirely for speed. Also, invalidating cache entries
28
works around weird cases, where maybe error/close don't get
29
properly called, but the proxy is not working due to network
30
issues. Invalidating cache entries quickly is also good from
31
a permissions and security point of view.
32
*/
33
34
const cache = new LRU<string, ProxyServer>({
35
max: 5000,
36
ttl: 1000 * 60 * 3,
37
});
38
39
async function handleProxyRequest(req, res): Promise<void> {
40
const dbg = (...args) => {
41
// for low level debugging -- silly isn't logged by default
42
logger.silly(req.url, ...args);
43
};
44
dbg("got request");
45
// dangerous/verbose to log...?
46
// dbg("headers = ", req.headers);
47
48
if (!isPersonal && versionCheckFails(req, res)) {
49
dbg("version check failed");
50
// note that the versionCheckFails function already sent back an error response.
51
throw Error("version check failed");
52
}
53
54
// Before doing anything further with the request on to the proxy, we remove **all** cookies whose
55
// name contains "remember_me", to prevent the project backend from getting at
56
// the user's session cookie, since one project shouldn't be able to get
57
// access to any user's account.
58
let remember_me, api_key;
59
if (req.headers["cookie"] != null) {
60
let cookie;
61
({ cookie, remember_me, api_key } = stripRememberMeCookie(
62
req.headers["cookie"],
63
));
64
req.headers["cookie"] = cookie;
65
}
66
67
if (!isPersonal && !remember_me && !api_key) {
68
dbg("no rememember me set, so blocking");
69
// Not in personal mode and there is no remember_me or api_key set all, so
70
// definitely block access. 4xx since this is a *client* problem.
71
const url = await siteUrl();
72
throw Error(
73
`Please login to <a target='_blank' href='${url}'>${url}</a> with cookies enabled, then refresh this page.`,
74
);
75
}
76
77
const url = stripBasePath(req.url);
78
const parsed = parseReq(url, remember_me, api_key);
79
// TODO: parseReq is called again in getTarget so need to refactor...
80
const { type, project_id } = parsed;
81
if (type == "files") {
82
if (
83
!(await hasAccess({
84
project_id,
85
remember_me,
86
api_key,
87
type: "read",
88
isPersonal,
89
}))
90
) {
91
throw Error(`user does not have read access to project`);
92
}
93
await handleFileDownload(req, res, url, project_id);
94
return;
95
}
96
97
const { host, port, internal_url } = await getTarget({
98
remember_me,
99
api_key,
100
url,
101
isPersonal,
102
projectControl,
103
parsed,
104
});
105
106
// It's http here because we've already got past the ssl layer. This is all internal.
107
const target = `http://${host}:${port}`;
108
dbg("target resolves to", target);
109
110
let proxy;
111
if (cache.has(target)) {
112
// we already have the proxy for this target in the cache
113
dbg("using cached proxy");
114
proxy = cache.get(target);
115
} else {
116
logger.debug("make a new proxy server to", target);
117
proxy = createProxyServer({
118
ws: false,
119
target,
120
});
121
// and cache it.
122
cache.set(target, proxy);
123
logger.debug("created new proxy");
124
125
proxy.on("error", (err) => {
126
logger.debug(`http proxy error -- ${err}`);
127
});
128
}
129
130
if (internal_url != null) {
131
dbg("changing req url from ", req.url, " to ", internal_url);
132
req.url = internal_url;
133
}
134
dbg("handling the request using the proxy");
135
proxy.web(req, res);
136
}
137
138
return async (req, res) => {
139
try {
140
await handleProxyRequest(req, res);
141
} catch (err) {
142
const msg = `WARNING: error proxying request ${req.url} -- ${err}`;
143
try {
144
// this will fail if handleProxyRequest already wrote a header, so we
145
// try/catch it.
146
res.writeHead(500, { "Content-Type": "text/html" });
147
} catch {}
148
try {
149
res.end(msg);
150
} catch {}
151
// Not something to log as an error -- just debug; it's normal for it to happen, e.g., when
152
// a project isn't running.
153
logger.debug(msg);
154
}
155
};
156
}
157
158