import LRU from "lru-cache";
import getLogger from "@cocalc/hub/logger";
import { database } from "@cocalc/hub/servers/database";
import { ProjectControlFunction } from "@cocalc/server/projects/control";
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
import { NamedServerName } from "@cocalc/util/types/servers";
import hasAccess from "./check-for-access-to-project";
import { parseReq } from "./parse";
const hub_projects = require("../projects");
const logger = getLogger("proxy:target");
const cache = new LRU<
string,
{
host: string;
port: number;
internal_url: string | undefined;
}
>({ max: 20000, ttl: 1000 * 30 });
export function invalidateTargetCache(remember_me: string, url: string): void {
const { key } = parseReq(url, remember_me);
logger.debug("invalidateCache:", url);
cache.delete(key);
}
interface Options {
remember_me?: string;
api_key?: string;
url: string;
isPersonal: boolean;
projectControl: ProjectControlFunction;
parsed?: ReturnType<typeof parseReq>;
}
export async function getTarget({
remember_me,
api_key,
url,
isPersonal,
projectControl,
parsed,
}: Options): Promise<{
host: string;
port: number;
internal_url: string | undefined;
}> {
const { key, type, project_id, port_desc, internal_url } =
parsed ?? parseReq(url, remember_me, api_key);
if (cache.has(key)) {
return cache.get(key) as any;
}
const dbg = logger.debug;
dbg("url", url);
if (
!(await hasAccess({
project_id,
remember_me,
api_key,
type: "write",
isPersonal,
}))
) {
throw Error(`user does not have write access to project`);
}
const project = projectControl(project_id);
let state = await project.state();
let host = state.ip;
dbg("host", host);
if (
port_desc === "jupyter" ||
port_desc === "jupyterlab" ||
port_desc === "code" ||
port_desc === "rserver"
) {
if (host == null || state.state !== "running") {
dbg(
"project not running and jupyter requested, so starting to run",
port_desc,
);
await project.start();
state = await project.state();
host = state.ip;
} else {
database.touch_project({ project_id });
}
}
if (host === "localhost") {
if (
port_desc === "jupyter" ||
port_desc === "jupyterlab" ||
port_desc === "code" ||
port_desc === "rstudio"
) {
host = "127.0.0.1";
}
}
if (host == null) {
throw Error("host is undefined -- project not running");
}
if (state.state !== "running") {
throw Error("project is not running");
}
let port: number;
if (type === "port" || type === "server") {
port = parseInt(port_desc);
if (!Number.isInteger(port)) {
dbg("determining name=", port_desc, "server port...");
port = await namedServerPort(project_id, port_desc, projectControl);
dbg("got named server name=", port_desc, " port=", port);
}
} else if (type === "raw") {
const status = await project.status();
if (status["browser-server.port"]) {
port = status["browser-server.port"];
} else {
throw Error(
"project browser server port not available -- project might not be opened or running",
);
}
} else {
throw Error(`unknown url type -- ${type}`);
}
dbg("finished: ", { host, port, type });
const target = { host, port, internal_url };
cache.set(key, target);
return target;
}
const namedServerPortCache = new LRU<string, number>({
max: 10000,
ttl: 1000 * 20,
});
async function _namedServerPort(
project_id: string,
name: NamedServerName,
projectControl,
): Promise<number> {
const key = project_id + name;
const p = namedServerPortCache.get(key);
if (p) {
return p;
}
const project = hub_projects.new_project(
project_id,
database,
projectControl,
);
const port = await project.named_server_port(name);
namedServerPortCache.set(key, port);
return port;
}
const namedServerPort = reuseInFlight(_namedServerPort, {
createKey: (args) => args[0] + args[1],
});