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/target.ts
Views: 687
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({ max: 20000, ttl: 1000 * 30 });
32
33
// This gets explicitly called from outside when certain errors occur.
34
export function invalidateTargetCache(remember_me: string, url: string): void {
35
const { key } = parseReq(url, remember_me);
36
logger.debug("invalidateCache:", url);
37
cache.delete(key);
38
}
39
40
interface Options {
41
remember_me?: string; // undefined = allow; only used for websocket upgrade.
42
api_key?: string;
43
url: string;
44
isPersonal: boolean;
45
projectControl: ProjectControlFunction;
46
}
47
48
export async function getTarget({
49
remember_me,
50
api_key,
51
url,
52
isPersonal,
53
projectControl,
54
}: Options): Promise<{
55
host: string;
56
port: number;
57
internal_url: string | undefined;
58
}> {
59
const { key, type, project_id, port_desc, internal_url } = parseReq(
60
url,
61
remember_me,
62
api_key,
63
);
64
65
if (cache.has(key)) {
66
return cache.get(key) as any;
67
}
68
// NOTE: do not log the key, since then logs leak way for
69
// an attacker to get in.
70
const dbg = logger.debug;
71
dbg("url", url);
72
73
// For now, we always require write access to proxy.
74
// We no longer have a notion of "read access" to projects,
75
// instead focusing on public sharing, cloning, etc.
76
if (
77
!(await hasAccess({
78
project_id,
79
remember_me,
80
api_key,
81
type: "write",
82
isPersonal,
83
}))
84
) {
85
throw Error(`user does not have write access to project`);
86
}
87
88
const project = projectControl(project_id);
89
let state = await project.state();
90
let host = state.ip;
91
dbg("host", host);
92
if (
93
port_desc === "jupyter" || // Jupyter Classic
94
port_desc === "jupyterlab" || // JupyterLab
95
port_desc === "code" || // VSCode = "code-server"
96
port_desc === "rserver"
97
) {
98
if (host == null || state.state !== "running") {
99
// We just start the project.
100
// This is used specifically by Juno, but also makes it
101
// easier to continually use Jupyter/Lab without having
102
// to worry about the cocalc project.
103
dbg(
104
"project not running and jupyter requested, so starting to run",
105
port_desc,
106
);
107
await project.start();
108
state = await project.state();
109
host = state.ip;
110
} else {
111
// Touch project so it doesn't idle timeout
112
database.touch_project({ project_id });
113
}
114
}
115
116
// https://github.com/sagemathinc/cocalc/issues/7009#issuecomment-1781950765
117
if (host === "localhost") {
118
if (
119
port_desc === "jupyter" || // Jupyter Classic
120
port_desc === "jupyterlab" || // JupyterLab
121
port_desc === "code" || // VSCode = "code-server"
122
port_desc === "rstudio" // RStudio Server
123
) {
124
host = "127.0.0.1";
125
}
126
}
127
128
if (host == null) {
129
throw Error("host is undefined -- project not running");
130
}
131
132
if (state.state !== "running") {
133
throw Error("project is not running");
134
}
135
136
let port: number;
137
if (type === "port" || type === "server") {
138
port = parseInt(port_desc);
139
if (!Number.isInteger(port)) {
140
dbg("determining name=", port_desc, "server port...");
141
port = await namedServerPort(project_id, port_desc, projectControl);
142
dbg("got named server name=", port_desc, " port=", port);
143
}
144
} else if (type === "raw") {
145
const status = await project.status();
146
// connection to the HTTP server in the project that serves web browsers
147
if (status["browser-server.port"]) {
148
port = status["browser-server.port"];
149
} else {
150
throw Error(
151
"project browser server port not available -- project might not be opened or running",
152
);
153
}
154
} else {
155
throw Error(`unknown url type -- ${type}`);
156
}
157
158
dbg("finished: ", { host, port, type });
159
const target = { host, port, internal_url };
160
cache.set(key, target);
161
return target;
162
}
163
164
// cache the chosen port for up to 30 seconds, since getting it
165
// from the project can be expensive.
166
const namedServerPortCache = new LRU<string, number>({
167
max: 10000,
168
ttl: 1000 * 20,
169
});
170
171
async function _namedServerPort(
172
project_id: string,
173
name: NamedServerName,
174
projectControl,
175
): Promise<number> {
176
const key = project_id + name;
177
const p = namedServerPortCache.get(key);
178
if (p) {
179
return p;
180
}
181
const project = hub_projects.new_project(
182
// NOT @cocalc/server/projects/control like above...
183
project_id,
184
database,
185
projectControl,
186
);
187
const port = await project.named_server_port(name);
188
namedServerPortCache.set(key, port);
189
return port;
190
}
191
192
const namedServerPort = reuseInFlight(_namedServerPort, {
193
createKey: (args) => args[0] + args[1],
194
});
195
196