Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/hub/proxy/target.ts
Views: 687
/*1Given a URL that we need to proxy, determine the target (host and port)2that is being proxied.34Throws an error if anything goes wrong, e.g., user doesn't have access5to this target or the target project isn't running.6*/78import LRU from "lru-cache";910import getLogger from "@cocalc/hub/logger";11import { database } from "@cocalc/hub/servers/database";12import { ProjectControlFunction } from "@cocalc/server/projects/control";13import { reuseInFlight } from "@cocalc/util/reuse-in-flight";14import { NamedServerName } from "@cocalc/util/types/servers";15import hasAccess from "./check-for-access-to-project";16import { parseReq } from "./parse";1718const hub_projects = require("../projects");1920const logger = getLogger("proxy:target");2122// The cached entries expire after 30 seconds. Caching the target23// helps enormously when there is a burst of requests.24// Also if a project restarts, the browser port might change and we25// don't want to have to fix this via getting an error.2627// Also, if the project stops and starts, the host=ip address could28// change, so we need to timeout so we see that thange.2930const cache = new LRU({ max: 20000, ttl: 1000 * 30 });3132// This gets explicitly called from outside when certain errors occur.33export function invalidateTargetCache(remember_me: string, url: string): void {34const { key } = parseReq(url, remember_me);35logger.debug("invalidateCache:", url);36cache.delete(key);37}3839interface Options {40remember_me?: string; // undefined = allow; only used for websocket upgrade.41api_key?: string;42url: string;43isPersonal: boolean;44projectControl: ProjectControlFunction;45}4647export async function getTarget({48remember_me,49api_key,50url,51isPersonal,52projectControl,53}: Options): Promise<{54host: string;55port: number;56internal_url: string | undefined;57}> {58const { key, type, project_id, port_desc, internal_url } = parseReq(59url,60remember_me,61api_key,62);6364if (cache.has(key)) {65return cache.get(key) as any;66}67// NOTE: do not log the key, since then logs leak way for68// an attacker to get in.69const dbg = logger.debug;70dbg("url", url);7172// For now, we always require write access to proxy.73// We no longer have a notion of "read access" to projects,74// instead focusing on public sharing, cloning, etc.75if (76!(await hasAccess({77project_id,78remember_me,79api_key,80type: "write",81isPersonal,82}))83) {84throw Error(`user does not have write access to project`);85}8687const project = projectControl(project_id);88let state = await project.state();89let host = state.ip;90dbg("host", host);91if (92port_desc === "jupyter" || // Jupyter Classic93port_desc === "jupyterlab" || // JupyterLab94port_desc === "code" || // VSCode = "code-server"95port_desc === "rserver"96) {97if (host == null || state.state !== "running") {98// We just start the project.99// This is used specifically by Juno, but also makes it100// easier to continually use Jupyter/Lab without having101// to worry about the cocalc project.102dbg(103"project not running and jupyter requested, so starting to run",104port_desc,105);106await project.start();107state = await project.state();108host = state.ip;109} else {110// Touch project so it doesn't idle timeout111database.touch_project({ project_id });112}113}114115// https://github.com/sagemathinc/cocalc/issues/7009#issuecomment-1781950765116if (host === "localhost") {117if (118port_desc === "jupyter" || // Jupyter Classic119port_desc === "jupyterlab" || // JupyterLab120port_desc === "code" || // VSCode = "code-server"121port_desc === "rstudio" // RStudio Server122) {123host = "127.0.0.1";124}125}126127if (host == null) {128throw Error("host is undefined -- project not running");129}130131if (state.state !== "running") {132throw Error("project is not running");133}134135let port: number;136if (type === "port" || type === "server") {137port = parseInt(port_desc);138if (!Number.isInteger(port)) {139dbg("determining name=", port_desc, "server port...");140port = await namedServerPort(project_id, port_desc, projectControl);141dbg("got named server name=", port_desc, " port=", port);142}143} else if (type === "raw") {144const status = await project.status();145// connection to the HTTP server in the project that serves web browsers146if (status["browser-server.port"]) {147port = status["browser-server.port"];148} else {149throw Error(150"project browser server port not available -- project might not be opened or running",151);152}153} else {154throw Error(`unknown url type -- ${type}`);155}156157dbg("finished: ", { host, port, type });158const target = { host, port, internal_url };159cache.set(key, target);160return target;161}162163// cache the chosen port for up to 30 seconds, since getting it164// from the project can be expensive.165const namedServerPortCache = new LRU<string, number>({166max: 10000,167ttl: 1000 * 20,168});169170async function _namedServerPort(171project_id: string,172name: NamedServerName,173projectControl,174): Promise<number> {175const key = project_id + name;176const p = namedServerPortCache.get(key);177if (p) {178return p;179}180const project = hub_projects.new_project(181// NOT @cocalc/server/projects/control like above...182project_id,183database,184projectControl,185);186const port = await project.named_server_port(name);187namedServerPortCache.set(key, port);188return port;189}190191const namedServerPort = reuseInFlight(_namedServerPort, {192createKey: (args) => args[0] + args[1],193});194195196