Path: blob/master/src/packages/hub/proxy/handle-request.ts
5714 views
/* Handle a proxy request */12import { createProxyServer, type ProxyServer } from "http-proxy-3";3import LRU from "lru-cache";4import stripRememberMeCookie from "./strip-remember-me-cookie";5import { versionCheckFails } from "./version";6import { getTarget } 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";11import { parseReq } from "./parse";12import hasAccess from "./check-for-access-to-project";13import { handleFileDownload } from "./file-download";1415const logger = getLogger("proxy:handle-request");1617interface Options {18projectControl: ProjectControlFunction;19isPersonal: boolean;20}2122export default function init({ projectControl, isPersonal }: Options) {23/* Cache at most 5000 proxies, each for up to 3 minutes.24Throwing away proxies at any time from the cache is fine since25the proxy is just used to handle *individual* http requests,26and the cache is entirely for speed. Also, invalidating cache entries27works around weird cases, where maybe error/close don't get28properly called, but the proxy is not working due to network29issues. Invalidating cache entries quickly is also good from30a permissions and security point of view.31*/3233const cache = new LRU<string, ProxyServer>({34max: 5000,35ttl: 1000 * 60 * 3,36});3738async function handleProxyRequest(req, res): Promise<void> {39const dbg = (...args) => {40// for low level debugging -- silly isn't logged by default41logger.silly(req.url, ...args);42};43dbg("got request");44// dangerous/verbose to log...?45// dbg("headers = ", req.headers);4647if (!isPersonal && versionCheckFails(req, res)) {48dbg("version check failed");49// note that the versionCheckFails function already sent back an error response.50throw Error("version check failed");51}5253// Before doing anything further with the request on to the proxy, we remove **all** cookies whose54// name contains "remember_me", to prevent the project backend from getting at55// the user's session cookie, since one project shouldn't be able to get56// access to any user's account.57let remember_me, api_key;58if (req.headers["cookie"] != null) {59let cookie;60({ cookie, remember_me, api_key } = stripRememberMeCookie(61req.headers["cookie"],62));63req.headers["cookie"] = cookie;64}6566if (!isPersonal && !remember_me && !api_key) {67dbg("no rememember me set, so blocking");68// Not in personal mode and there is no remember_me or api_key set all, so69// definitely block access. 4xx since this is a *client* problem.70const url = await siteUrl();71throw Error(72`Please login to <a target='_blank' href='${url}'>${url}</a> with cookies enabled, then refresh this page.`,73);74}7576const url = stripBasePath(req.url);77const parsed = parseReq(url, remember_me, api_key);78// TODO: parseReq is called again in getTarget so need to refactor...79const { type, project_id } = parsed;80if (type == "files") {81if (82!(await hasAccess({83project_id,84remember_me,85api_key,86type: "read",87isPersonal,88}))89) {90throw Error(`user does not have read access to project`);91}92await handleFileDownload(req, res, url, project_id);93return;94}9596const { host, port, internal_url } = await getTarget({97remember_me,98api_key,99url,100isPersonal,101projectControl,102parsed,103});104105// It's http here because we've already got past the ssl layer. This is all internal.106const target = `http://${host}:${port}`;107dbg("target resolves to", target);108109let proxy;110if (cache.has(target)) {111// we already have the proxy for this target in the cache112dbg("using cached proxy");113proxy = cache.get(target);114} else {115logger.debug("make a new proxy server to", target);116proxy = createProxyServer({117ws: false,118target,119});120// and cache it.121cache.set(target, proxy);122logger.debug("created new proxy");123124proxy.on("error", (err) => {125logger.debug(`http proxy error -- ${err}`);126});127}128129if (internal_url != null) {130dbg("changing req url from ", req.url, " to ", internal_url);131req.url = internal_url;132}133dbg("handling the request using the proxy");134proxy.web(req, res);135}136137return async (req, res) => {138try {139await handleProxyRequest(req, res);140} catch (err) {141const msg = `WARNING: error proxying request ${req.url} -- ${err}`;142try {143// this will fail if handleProxyRequest already wrote a header, so we144// try/catch it.145res.writeHead(500, { "Content-Type": "text/html" });146} catch {}147try {148res.end(msg);149} catch {}150// Not something to log as an error -- just debug; it's normal for it to happen, e.g., when151// a project isn't running.152logger.debug(msg);153}154};155}156157158