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