From bd1c104f688ee7555518be3297b37cc838223efa Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 12 Jan 2023 09:50:31 -0500 Subject: [PATCH] Remove LensProxy class as unnecessary Signed-off-by: Sebastian Malton --- ...ns-proxy.global-override-for-injectable.ts | 19 ++ src/main/lens-proxy/lens-proxy.injectable.ts | 240 ++++++++++++++++- src/main/lens-proxy/lens-proxy.ts | 252 ------------------ src/main/router/route-request.injectable.ts | 9 +- 4 files changed, 252 insertions(+), 268 deletions(-) create mode 100644 src/main/lens-proxy/lens-proxy.global-override-for-injectable.ts delete mode 100644 src/main/lens-proxy/lens-proxy.ts diff --git a/src/main/lens-proxy/lens-proxy.global-override-for-injectable.ts b/src/main/lens-proxy/lens-proxy.global-override-for-injectable.ts new file mode 100644 index 0000000000..7539163abe --- /dev/null +++ b/src/main/lens-proxy/lens-proxy.global-override-for-injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../common/test-utils/get-global-override"; +import lensProxyPortInjectable from "./lens-proxy-port.injectable"; +import lensProxyInjectable from "./lens-proxy.injectable"; + +export default getGlobalOverride(lensProxyInjectable, (di) => { + const lensProxyPort = di.inject(lensProxyPortInjectable); + + return ({ + close: () => { }, + listen: async () => { + lensProxyPort.set(12345); + }, + }); +}); diff --git a/src/main/lens-proxy/lens-proxy.injectable.ts b/src/main/lens-proxy/lens-proxy.injectable.ts index 1b5ca52ee8..852a766db2 100644 --- a/src/main/lens-proxy/lens-proxy.injectable.ts +++ b/src/main/lens-proxy/lens-proxy.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { LensProxy } from "./lens-proxy"; +import type { ProxyApiRequestArgs } from "./proxy-functions"; import { kubeApiUpgradeRequest } from "./proxy-functions"; import httpProxy from "http-proxy"; import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable"; @@ -14,22 +14,236 @@ import loggerInjectable from "../../common/logger.injectable"; import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; import getClusterForRequestInjectable from "./get-cluster-for-request.injectable"; import routeRequestInjectable from "../router/route-request.injectable"; +import type { IncomingMessage, ServerResponse } from "http"; +import assert from "assert"; +import net from "net"; +import type { Cluster } from "../../common/cluster/cluster"; +import { getBoolean } from "../utils/parse-query"; +import type { ClusterContextHandler } from "../context-handler/context-handler"; +import { apiKubePrefix, apiPrefix } from "../../common/vars"; +import { createServer } from "https"; + +export type GetClusterForRequest = (req: IncomingMessage) => Cluster | undefined; +export type LensProxyApiRequest = (args: ProxyApiRequestArgs) => void | Promise; + +export interface LensProxy { + listen: () => Promise; + close: () => void; +} + +const getRequestId = (req: IncomingMessage) => { + assert(req.headers.host); + + return req.headers.host + req.url; +}; + +const watchParam = "watch"; +const followParam = "follow"; + +const isLongRunningRequest = (reqUrl: string) => { + const url = new URL(reqUrl, "http://localhost"); + + return getBoolean(url.searchParams, watchParam) || getBoolean(url.searchParams, followParam); +}; + +/** + * This is the list of ports that chrome considers unsafe to allow HTTP + * conntections to. Because they are the standard ports for processes that are + * too forgiving in the connection types they accept. + * + * If we get one of these ports, the easiest thing to do is to just try again. + * + * Source: https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc + */ +const disallowedPorts = new Set([ + 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, + 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, + 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, + 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723, + 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697, + 10080, +]); const lensProxyInjectable = getInjectable({ id: "lens-proxy", - instantiate: (di) => new LensProxy({ - routeRequest: di.inject(routeRequestInjectable), - proxy: httpProxy.createProxy(), - kubeApiUpgradeRequest, - shellApiRequest: di.inject(shellApiRequestInjectable), - getClusterForRequest: di.inject(getClusterForRequestInjectable), - lensProxyPort: di.inject(lensProxyPortInjectable), - contentSecurityPolicy: di.inject(contentSecurityPolicyInjectable), - emitAppEvent: di.inject(emitAppEventInjectable), - logger: di.inject(loggerInjectable), - certificate: di.inject(lensProxyCertificateInjectable).get(), - }), + instantiate: (di): LensProxy => { + const routeRequest = di.inject(routeRequestInjectable); + const shellApiRequest = di.inject(shellApiRequestInjectable); + const getClusterForRequest = di.inject(getClusterForRequestInjectable); + const lensProxyPort = di.inject(lensProxyPortInjectable); + const contentSecurityPolicy = di.inject(contentSecurityPolicyInjectable); + const emitAppEvent = di.inject(emitAppEventInjectable); + const logger = di.inject(loggerInjectable); + const certificate = di.inject(lensProxyCertificateInjectable).get(); + + const retryCounters = new Map(); + let closed = false; + + const proxy = httpProxy.createProxy() + .on("proxyRes", (proxyRes, req, res) => { + retryCounters.delete(getRequestId(req)); + + proxyRes.on("aborted", () => { // happens when proxy target aborts connection + res.end(); + }); + }) + .on("error", (error, req, res, target) => { + if (closed || res instanceof net.Socket) { + return; + } + + logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url }); + + if (target) { + logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`); + + if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { + const reqId = getRequestId(req); + const retryCount = retryCounters.get(reqId) || 0; + const timeoutMs = retryCount * 250; + + if (retryCount < 20) { + logger.debug(`Retrying proxy request to url: ${reqId}`); + setTimeout(() => { + retryCounters.set(reqId, retryCount + 1); + + (async () => { + try { + await handleRequest(req, res); + } catch (error) { + logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`); + } + })(); + }, timeoutMs); + } + } + } + + try { + res.writeHead(500).end(`Oops, something went wrong.\n${error}`); + } catch (e) { + logger.error(`[LENS-PROXY]: Failed to write headers: `, e); + } + }); + + const getProxyTarget = async (req: IncomingMessage, contextHandler: ClusterContextHandler) => { + if (req.url?.startsWith(apiKubePrefix)) { + delete req.headers.authorization; + req.url = req.url.replace(apiKubePrefix, ""); + + return contextHandler.getApiTarget(isLongRunningRequest(req.url)); + } + + return undefined; + }; + + const handleRequest = async (req: IncomingMessage, res: ServerResponse) => { + const cluster = getClusterForRequest(req); + + if (cluster) { + const proxyTarget = await getProxyTarget(req, cluster.contextHandler); + + if (proxyTarget) { + return proxy.web(req, res, proxyTarget); + } + } + + res.setHeader("Content-Security-Policy", contentSecurityPolicy); + await routeRequest(cluster, req, res); + }; + + const proxyServer = createServer( + { + key: certificate.private, + cert: certificate.cert, + }, + handleRequest, + ) + .on("upgrade", (req, socket, head) => { + const cluster = getClusterForRequest(req); + + if (!cluster || !req.url) { + logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`); + socket.destroy(); + } else { + const isInternal = req.url.startsWith(`${apiPrefix}?`); + const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest; + + (async () => { + try { + await reqHandler({ req, socket, head, cluster }); + } catch (error) { + logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error); + } + })(); + } + }); + + const attemptToListen = () => new Promise((resolve, reject) => { + proxyServer.listen(0, "127.0.0.1"); + + const onInitialError = (error: Error) => { + logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`); + reject(error); + }; + + proxyServer + .once("listening", () => { + proxyServer.removeListener("error", onInitialError); + + const { address, port } = proxyServer.address() as net.AddressInfo; + + lensProxyPort.set(port); + logger.info(`[LENS-PROXY]: Proxy server has started at ${address}:${port}`); + + proxyServer.on("error", (error) => { + logger.info(`[LENS-PROXY]: Subsequent error: ${error}`); + }); + + emitAppEvent({ name: "lens-proxy", action: "listen", params: { port }}); + resolve(port); + }) + .once("error", onInitialError); + }); + + const listen = async () => { + const seenPorts = new Set(); + + for(;;) { + proxyServer.close(); + const port = await attemptToListen(); + + if (!disallowedPorts.has(port)) { + // We didn't get a port that would result in an ERR_UNSAFE_PORT error, use it + return; + } + + logger.warn(`[LENS-PROXY]: Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`); + + if (seenPorts.has(port)) { + /** + * Assume that if we have seen the port before, then the OS has looped + * through all the ports possible and we will not be able to get a safe + * port. + */ + throw new Error("Failed to start LensProxy due to seeing too many unsafe ports. Please restart Lens."); + } else { + seenPorts.add(port); + } + } + }; + + const close = () => { + logger.info("[LENS-PROXY]: Closing server"); + + proxyServer.close(); + closed = true; + }; + + return { close, listen }; + }, + causesSideEffects: true, }); export default lensProxyInjectable; diff --git a/src/main/lens-proxy/lens-proxy.ts b/src/main/lens-proxy/lens-proxy.ts deleted file mode 100644 index 7860f14f54..0000000000 --- a/src/main/lens-proxy/lens-proxy.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import net from "net"; -import https from "https"; -import type http from "http"; -import type httpProxy from "http-proxy"; -import { apiPrefix, apiKubePrefix } from "../../common/vars"; -import type { ClusterContextHandler } from "../context-handler/context-handler"; -import type { Cluster } from "../../common/cluster/cluster"; -import type { ProxyApiRequestArgs } from "./proxy-functions"; -import { getBoolean } from "../utils/parse-query"; -import assert from "assert"; -import type { SetRequired } from "type-fest"; -import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable"; -import type { Logger } from "../../common/logger"; -import type { SelfSignedCert } from "selfsigned"; -import type { RouteRequest } from "../router/route-request.injectable"; - -export type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined; -export type ServerIncomingMessage = SetRequired; -export type LensProxyApiRequest = (args: ProxyApiRequestArgs) => void | Promise; - -interface Dependencies { - getClusterForRequest: GetClusterForRequest; - shellApiRequest: LensProxyApiRequest; - kubeApiUpgradeRequest: LensProxyApiRequest; - emitAppEvent: EmitAppEvent; - routeRequest: RouteRequest; - readonly proxy: httpProxy; - readonly lensProxyPort: { set: (portNumber: number) => void }; - readonly contentSecurityPolicy: string; - readonly logger: Logger; - readonly certificate: SelfSignedCert; -} - -const watchParam = "watch"; -const followParam = "follow"; - -export function isLongRunningRequest(reqUrl: string) { - const url = new URL(reqUrl, "http://localhost"); - - return getBoolean(url.searchParams, watchParam) || getBoolean(url.searchParams, followParam); -} - -/** - * This is the list of ports that chrome considers unsafe to allow HTTP - * conntections to. Because they are the standard ports for processes that are - * too forgiving in the connection types they accept. - * - * If we get one of these ports, the easiest thing to do is to just try again. - * - * Source: https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc - */ -const disallowedPorts = new Set([ - 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, - 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, - 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, - 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723, - 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697, - 10080, -]); - -export class LensProxy { - protected proxyServer: https.Server; - protected closed = false; - protected retryCounters = new Map(); - - constructor(private readonly dependencies: Dependencies) { - this.configureProxy(dependencies.proxy); - - this.proxyServer = https.createServer( - { - key: dependencies.certificate.private, - cert: dependencies.certificate.cert, - }, - (req, res) => { - this.handleRequest(req as ServerIncomingMessage, res); - }, - ); - - this.proxyServer - .on("upgrade", (req: ServerIncomingMessage, socket: net.Socket, head: Buffer) => { - const cluster = this.dependencies.getClusterForRequest(req); - - if (!cluster) { - this.dependencies.logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`); - socket.destroy(); - } else { - const isInternal = req.url.startsWith(`${apiPrefix}?`); - const reqHandler = isInternal ? this.dependencies.shellApiRequest : this.dependencies.kubeApiUpgradeRequest; - - (async () => reqHandler({ req, socket, head, cluster }))() - .catch(error => this.dependencies.logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error)); - } - }); - } - - /** - * Starts to listen on an OS provided port. Will reject if the server throws - * an error. - * - * Resolves with the port number that was picked - */ - private attemptToListen(): Promise { - return new Promise((resolve, reject) => { - this.proxyServer.listen(0, "127.0.0.1"); - - this.proxyServer - .once("listening", () => { - this.proxyServer.removeAllListeners("error"); // don't reject the promise - - const { address, port } = this.proxyServer.address() as net.AddressInfo; - - this.dependencies.lensProxyPort.set(port); - - this.dependencies.logger.info(`[LENS-PROXY]: Proxy server has started at ${address}:${port}`); - - this.proxyServer.on("error", (error) => { - this.dependencies.logger.info(`[LENS-PROXY]: Subsequent error: ${error}`); - }); - - this.dependencies.emitAppEvent({ name: "lens-proxy", action: "listen", params: { port }}); - resolve(port); - }) - .once("error", (error) => { - this.dependencies.logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`); - reject(error); - }); - }); - } - - /** - * Starts the lens proxy. - * @resolves After the server is listening on a good port - * @rejects if there is an error before that happens - */ - async listen(): Promise { - const seenPorts = new Set(); - - while(true) { - this.proxyServer?.close(); - const port = await this.attemptToListen(); - - if (!disallowedPorts.has(port)) { - // We didn't get a port that would result in an ERR_UNSAFE_PORT error, use it - return; - } - - this.dependencies.logger.warn(`[LENS-PROXY]: Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`); - - if (seenPorts.has(port)) { - /** - * Assume that if we have seen the port before, then the OS has looped - * through all the ports possible and we will not be able to get a safe - * port. - */ - throw new Error("Failed to start LensProxy due to seeing too many unsafe ports. Please restart Lens."); - } else { - seenPorts.add(port); - } - } - } - - close() { - this.dependencies.logger.info("[LENS-PROXY]: Closing server"); - - this.proxyServer.close(); - this.closed = true; - } - - protected configureProxy(proxy: httpProxy): httpProxy { - proxy.on("proxyRes", (proxyRes, req, res) => { - const retryCounterId = this.getRequestId(req); - - if (this.retryCounters.has(retryCounterId)) { - this.retryCounters.delete(retryCounterId); - } - - proxyRes.on("aborted", () => { // happens when proxy target aborts connection - res.end(); - }); - }); - - proxy.on("error", (error, req, res, target) => { - if (this.closed || res instanceof net.Socket) { - return; - } - - this.dependencies.logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url }); - - if (target) { - this.dependencies.logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`); - - if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { - const reqId = this.getRequestId(req); - const retryCount = this.retryCounters.get(reqId) || 0; - const timeoutMs = retryCount * 250; - - if (retryCount < 20) { - this.dependencies.logger.debug(`Retrying proxy request to url: ${reqId}`); - setTimeout(() => { - this.retryCounters.set(reqId, retryCount + 1); - this.handleRequest(req as ServerIncomingMessage, res) - .catch(error => this.dependencies.logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`)); - }, timeoutMs); - } - } - } - - try { - res.writeHead(500).end(`Oops, something went wrong.\n${error}`); - } catch (e) { - this.dependencies.logger.error(`[LENS-PROXY]: Failed to write headers: `, e); - } - }); - - return proxy; - } - - protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise { - if (req.url?.startsWith(apiKubePrefix)) { - delete req.headers.authorization; - req.url = req.url.replace(apiKubePrefix, ""); - - return contextHandler.getApiTarget(isLongRunningRequest(req.url)); - } - } - - protected getRequestId(req: http.IncomingMessage): string { - assert(req.headers.host); - - return req.headers.host + req.url; - } - - protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) { - const cluster = this.dependencies.getClusterForRequest(req); - - if (cluster) { - const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); - - if (proxyTarget) { - return this.dependencies.proxy.web(req, res, proxyTarget); - } - } - - res.setHeader("Content-Security-Policy", this.dependencies.contentSecurityPolicy); - await this.dependencies.routeRequest(cluster, req, res); - } -} diff --git a/src/main/router/route-request.injectable.ts b/src/main/router/route-request.injectable.ts index ed2f567b26..2af812f82e 100644 --- a/src/main/router/route-request.injectable.ts +++ b/src/main/router/route-request.injectable.ts @@ -8,10 +8,9 @@ import type { Route, LensApiRequest } from "./route"; import createHandlerForRouteInjectable from "./create-handler-for-route.injectable"; import Call from "@hapi/call"; import Subtext from "@hapi/subtext"; -import type http from "http"; import type { Cluster } from "../../common/cluster/cluster"; -import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy"; import type { RouteHandler } from "./create-handler-for-route.injectable"; +import type { IncomingMessage, ServerResponse } from "http"; export const routeInjectionToken = getInjectionToken>({ id: "route-injection-token", @@ -26,7 +25,7 @@ export function getRouteInjectable( }); } -export type RouteRequest = (cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse) => Promise; +export type RouteRequest = (cluster: Cluster | undefined, req: IncomingMessage, res: ServerResponse) => Promise; const createRouter = (di: DiContainerForInjection) => { const routes = di.injectMany(routeInjectionToken); @@ -46,6 +45,10 @@ const routeRequestInjectable = getInjectable({ const router = createRouter(di); return async (cluster, req, res) => { + if (!req.url || !req.method) { + return false; + } + const url = new URL(req.url, "https://localhost"); const path = url.pathname; const method = req.method.toLowerCase();