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