Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/hub/proxy/target.ts
1496 views
1
/*
2
Given a URL that we need to proxy, determine the target (host and port)
3
that is being proxied.
4
5
Throws an error if anything goes wrong, e.g., user doesn't have access
6
to this target or the target project isn't running.
7
*/
8
9
import LRU from "lru-cache";
10
11
import getLogger from "@cocalc/hub/logger";
12
import { database } from "@cocalc/hub/servers/database";
13
import { ProjectControlFunction } from "@cocalc/server/projects/control";
14
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
15
import { NamedServerName } from "@cocalc/util/types/servers";
16
import hasAccess from "./check-for-access-to-project";
17
import { parseReq } from "./parse";
18
19
const hub_projects = require("../projects");
20
21
const logger = getLogger("proxy:target");
22
23
// The cached entries expire after 30 seconds. Caching the target
24
// helps enormously when there is a burst of requests.
25
// Also if a project restarts, the browser port might change and we
26
// don't want to have to fix this via getting an error.
27
28
// Also, if the project stops and starts, the host=ip address could
29
// change, so we need to timeout so we see that thange.
30
31
const cache = new LRU<
32
string,
33
{
34
host: string;
35
port: number;
36
internal_url: string | undefined;
37
}
38
>({ max: 20000, ttl: 1000 * 30 });
39
40
// This gets explicitly called from outside when certain errors occur.
41
export function invalidateTargetCache(remember_me: string, url: string): void {
42
const { key } = parseReq(url, remember_me);
43
logger.debug("invalidateCache:", url);
44
cache.delete(key);
45
}
46
47
interface Options {
48
remember_me?: string; // undefined = allow; only used for websocket upgrade.
49
api_key?: string;
50
url: string;
51
isPersonal: boolean;
52
projectControl: ProjectControlFunction;
53
parsed?: ReturnType<typeof parseReq>;
54
}
55
56
export async function getTarget({
57
remember_me,
58
api_key,
59
url,
60
isPersonal,
61
projectControl,
62
parsed,
63
}: Options): Promise<{
64
host: string;
65
port: number;
66
internal_url: string | undefined;
67
}> {
68
const { key, type, project_id, port_desc, internal_url } =
69
parsed ?? parseReq(url, remember_me, api_key);
70
71
if (cache.has(key)) {
72
return cache.get(key) as any;
73
}
74
// NOTE: do not log the key, since then logs leak way for
75
// an attacker to get in.
76
const dbg = logger.debug;
77
dbg("url", url);
78
79
// For now, we always require write access to proxy.
80
// We no longer have a notion of "read access" to projects,
81
// instead focusing on public sharing, cloning, etc.
82
if (
83
!(await hasAccess({
84
project_id,
85
remember_me,
86
api_key,
87
type: "write",
88
isPersonal,
89
}))
90
) {
91
throw Error(`user does not have write access to project`);
92
}
93
94
const project = projectControl(project_id);
95
let state = await project.state();
96
let host = state.ip;
97
dbg("host", host);
98
if (
99
port_desc === "jupyter" || // Jupyter Classic
100
port_desc === "jupyterlab" || // JupyterLab
101
port_desc === "code" || // VSCode = "code-server"
102
port_desc === "rserver"
103
) {
104
if (host == null || state.state !== "running") {
105
// We just start the project.
106
// This is used specifically by Juno, but also makes it
107
// easier to continually use Jupyter/Lab without having
108
// to worry about the cocalc project.
109
dbg(
110
"project not running and jupyter requested, so starting to run",
111
port_desc,
112
);
113
await project.start();
114
state = await project.state();
115
host = state.ip;
116
} else {
117
// Touch project so it doesn't idle timeout
118
database.touch_project({ project_id });
119
}
120
}
121
122
// https://github.com/sagemathinc/cocalc/issues/7009#issuecomment-1781950765
123
if (host === "localhost") {
124
if (
125
port_desc === "jupyter" || // Jupyter Classic
126
port_desc === "jupyterlab" || // JupyterLab
127
port_desc === "code" || // VSCode = "code-server"
128
port_desc === "rstudio" // RStudio Server
129
) {
130
host = "127.0.0.1";
131
}
132
}
133
134
if (host == null) {
135
throw Error("host is undefined -- project not running");
136
}
137
138
if (state.state !== "running") {
139
throw Error("project is not running");
140
}
141
142
let port: number;
143
if (type === "port" || type === "server") {
144
port = parseInt(port_desc);
145
if (!Number.isInteger(port)) {
146
dbg("determining name=", port_desc, "server port...");
147
port = await namedServerPort(project_id, port_desc, projectControl);
148
dbg("got named server name=", port_desc, " port=", port);
149
}
150
} else if (type === "raw") {
151
const status = await project.status();
152
// connection to the HTTP server in the project that serves web browsers
153
if (status["browser-server.port"]) {
154
port = status["browser-server.port"];
155
} else {
156
throw Error(
157
"project browser server port not available -- project might not be opened or running",
158
);
159
}
160
} else {
161
throw Error(`unknown url type -- ${type}`);
162
}
163
164
dbg("finished: ", { host, port, type });
165
const target = { host, port, internal_url };
166
cache.set(key, target);
167
return target;
168
}
169
170
// cache the chosen port for up to 30 seconds, since getting it
171
// from the project can be expensive.
172
const namedServerPortCache = new LRU<string, number>({
173
max: 10000,
174
ttl: 1000 * 20,
175
});
176
177
async function _namedServerPort(
178
project_id: string,
179
name: NamedServerName,
180
projectControl,
181
): Promise<number> {
182
const key = project_id + name;
183
const p = namedServerPortCache.get(key);
184
if (p) {
185
return p;
186
}
187
const project = hub_projects.new_project(
188
// NOT @cocalc/server/projects/control like above...
189
project_id,
190
database,
191
projectControl,
192
);
193
const port = await project.named_server_port(name);
194
namedServerPortCache.set(key, port);
195
return port;
196
}
197
198
const namedServerPort = reuseInFlight(_namedServerPort, {
199
createKey: (args) => args[0] + args[1],
200
});
201
202