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-upgrade.ts
Views: 687
// Websocket support12import { createProxyServer } from "http-proxy";3import LRU from "lru-cache";4import { getEventListeners } from "node:events";56import getLogger from "@cocalc/hub/logger";7import stripRememberMeCookie from "./strip-remember-me-cookie";8import { getTarget } from "./target";9import { stripBasePath } from "./util";10import { versionCheckFails } from "./version";1112const logger = getLogger("proxy:handle-upgrade");1314export default function init(15{ projectControl, isPersonal, httpServer, listenersHack },16proxy_regexp: string,17) {18const cache = new LRU({19max: 5000,20ttl: 1000 * 60 * 3,21});2223const re = new RegExp(proxy_regexp);2425async function handleProxyUpgradeRequest(req, socket, head): Promise<void> {26socket.on("error", (err) => {27// server will crash sometimes without this:28logger.debug("WARNING -- websocket socket error", err);29});30const dbg = (...args) => {31logger.silly(req.url, ...args);32};33dbg("got upgrade request from url=", req.url);34if (!req.url.match(re)) {35throw Error(`url=${req.url} does not support upgrade`);36}3738// Check that minimum version requirement is satisfied (this is in the header).39// This is to have a way to stop buggy clients from causing trouble. It's a purely40// honor system sort of thing, but makes it possible for an admin to block clients41// until they run newer code. I used to have to use this a lot long ago...42if (versionCheckFails(req)) {43throw Error("client version check failed");44}4546const url = stripBasePath(req.url);4748let remember_me, api_key;49if (req.headers["cookie"] != null) {50let cookie;51({ cookie, remember_me, api_key } = stripRememberMeCookie(52req.headers["cookie"],53));54req.headers["cookie"] = cookie;55}5657dbg("calling getTarget");58const { host, port, internal_url } = await getTarget({59url,60isPersonal,61projectControl,62remember_me,63api_key,64});65dbg("got ", { host, port });6667const target = `ws://${host}:${port}`;68if (internal_url != null) {69req.url = internal_url;70}71if (cache.has(target)) {72dbg("using cache");73const proxy = cache.get(target);74(proxy as any)?.ws(req, socket, head);75return;76}7778dbg("target", target);79dbg("not using cache");80const proxy = createProxyServer({81ws: true,82target,83timeout: 3000,84});85cache.set(target, proxy);8687// taken from https://github.com/http-party/node-http-proxy/issues/140188proxy.on("proxyRes", function (proxyRes) {89//console.log(90// "Raw [target] response",91// JSON.stringify(proxyRes.headers, true, 2)92//);9394proxyRes.headers["x-reverse-proxy"] = "custom-proxy";95proxyRes.headers["cache-control"] = "no-cache, no-store";9697//console.log(98// "Updated [proxied] response",99// JSON.stringify(proxyRes.headers, true, 2)100//);101});102103proxy.on("error", (err) => {104logger.debug(`websocket proxy error, so clearing cache -- ${err}`);105cache.delete(target);106});107proxy.on("close", () => {108dbg("websocket proxy closed, so removing from cache");109cache.delete(target);110});111proxy.ws(req, socket, head);112}113114let handler;115if (listenersHack) {116// This is an insane horrible hack to fix https://github.com/sagemathinc/cocalc/issues/7067117// The problem is that there are four separate websocket "upgrade" handlers when we are doing118// development, and nodejs just doesn't have a good solution to multiple websocket handlers,119// as explained here: https://github.com/nodejs/node/issues/6339120// The four upgrade handlers are:121// - this proxy here122// - the main hub primus one123// - the HMR reloader for that static webpack server for the app124// - the HMR reloader for nextjs125// These all just sort of randomly fight for any incoming "upgrade" event,126// and if they don't like it, tend to try to kill the socket. It's totally insane.127// What's worse is that getEventListeners only seems to ever return *two*128// listeners. By extensive trial and error, it seems to return first the primus129// listener, then the nextjs one. I have no idea why the order is that way; I would130// expect the reverse. (Update: it's because nextjs uses a hack -- it only installs131// a listener once a request comes in. Until there is a request, nextjs does not have132// access to the server and can't mess with it.)133// And I don't know why this handler here isn't in the list.134// In any case, once we get a failed request *and* we see there are at least two135// other handlers (it's exactly two), we completely steal handling of the upgrade136// event here. We then call the appropriate other handler when needed.137// I have no idea how the HMR reloader for that static webpack plays into this,138// but it appears to just work for some reason.139140// NOTE: I had to do something similar that is in packages/next/lib/init.js,141// and is NOT a hack. That technique could probably be used to fix this properly.142143let listeners: any[] = [];144handler = async (req, socket, head) => {145logger.debug("Proxy websocket handling -- using listenersHack");146try {147await handleProxyUpgradeRequest(req, socket, head);148} catch (err) {149if (listeners.length == 0) {150const x = getEventListeners(httpServer, "upgrade");151if (x.length >= 2) {152logger.debug(153"Proxy websocket handling -- installing listenersHack",154);155listeners = [...x];156httpServer.removeAllListeners("upgrade");157httpServer.on("upgrade", handler);158}159}160if (req.url.includes("hub?_primus") && listeners.length >= 2) {161listeners[0](req, socket, head);162return;163}164if (req.url.includes("_next/webpack-hmr") && listeners.length >= 2) {165listeners[1](req, socket, head);166return;167}168const msg = `WARNING: error upgrading websocket url=${req.url} -- ${err}`;169logger.debug(msg);170denyUpgrade(socket);171}172};173} else {174handler = async (req, socket, head) => {175try {176await handleProxyUpgradeRequest(req, socket, head);177} catch (err) {178const msg = `WARNING: error upgrading websocket url=${req.url} -- ${err}`;179logger.debug(msg);180denyUpgrade(socket);181}182};183}184185return handler;186}187188function denyUpgrade(socket) {189socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");190socket.destroy();191}192193194