From 180e5b944938739258b02c829ec97c1151abd4d7 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 21 Mar 2023 15:38:16 -0400 Subject: [PATCH] Move https server into injectable Signed-off-by: Sebastian Malton --- .../handle-route-request.injectable.ts | 47 +++++++++++ .../https-proxy/on-upgrade.injectable.ts | 44 +++++++++++ .../https-proxy/server.injectable.ts | 30 +++++++ .../main/lens-proxy/lens-proxy.injectable.ts | 22 ++---- .../core/src/main/lens-proxy/lens-proxy.ts | 79 +++---------------- packages/core/src/main/lens-proxy/messages.ts | 11 +++ .../src/main/lens-proxy/proxy.injectable.ts | 13 +++ 7 files changed, 163 insertions(+), 83 deletions(-) create mode 100644 packages/core/src/main/lens-proxy/handle-route-request.injectable.ts create mode 100644 packages/core/src/main/lens-proxy/https-proxy/on-upgrade.injectable.ts create mode 100644 packages/core/src/main/lens-proxy/https-proxy/server.injectable.ts create mode 100644 packages/core/src/main/lens-proxy/messages.ts create mode 100644 packages/core/src/main/lens-proxy/proxy.injectable.ts diff --git a/packages/core/src/main/lens-proxy/handle-route-request.injectable.ts b/packages/core/src/main/lens-proxy/handle-route-request.injectable.ts new file mode 100644 index 0000000000..d0bbc8017b --- /dev/null +++ b/packages/core/src/main/lens-proxy/handle-route-request.injectable.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { ServerResponse } from "http"; +import { apiKubePrefix } from "../../common/vars"; +import contentSecurityPolicyInjectable from "../../common/vars/content-security-policy.injectable"; +import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable"; +import routeRequestInjectable from "../router/route-request.injectable"; +import getClusterForRequestInjectable from "./get-cluster-for-request.injectable"; +import { isLongRunningRequest } from "./is-long-running-request"; +import type { ProxyIncomingMessage } from "./messages"; +import proxyInjectable from "./proxy.injectable"; + +export type HandleRouteRequest = (req: ProxyIncomingMessage, res: ServerResponse) => Promise; + +const handleRouteRequestInjectable = getInjectable({ + id: "handle-route-request", + instantiate: (di): HandleRouteRequest => { + const getClusterForRequest = di.inject(getClusterForRequestInjectable); + const routeRequest = di.inject(routeRequestInjectable); + const proxy = di.inject(proxyInjectable); + const contentSecurityPolicy = di.inject(contentSecurityPolicyInjectable); + + return async (req, res) => { + const cluster = getClusterForRequest(req); + + if (cluster && req.url.startsWith(apiKubePrefix)) { + delete req.headers.authorization; + req.url = req.url.replace(apiKubePrefix, ""); + + const kubeAuthProxyServer = di.inject(kubeAuthProxyServerInjectable, cluster); + const proxyTarget = await kubeAuthProxyServer.getApiTarget(isLongRunningRequest(req.url)); + + if (proxyTarget) { + return proxy.web(req, res, proxyTarget); + } + } + + res.setHeader("Content-Security-Policy", contentSecurityPolicy); + await routeRequest(cluster, req, res); + }; + }, +}); + +export default handleRouteRequestInjectable; diff --git a/packages/core/src/main/lens-proxy/https-proxy/on-upgrade.injectable.ts b/packages/core/src/main/lens-proxy/https-proxy/on-upgrade.injectable.ts new file mode 100644 index 0000000000..f9fb16a43f --- /dev/null +++ b/packages/core/src/main/lens-proxy/https-proxy/on-upgrade.injectable.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { Socket } from "net"; +import loggerInjectable from "../../../common/logger.injectable"; +import { apiPrefix } from "../../../common/vars"; +import getClusterForRequestInjectable from "../get-cluster-for-request.injectable"; +import type { ProxyIncomingMessage } from "../messages"; +import kubeApiUpgradeRequestInjectable from "../proxy-functions/kube-api-upgrade-request.injectable"; +import shellApiRequestInjectable from "../proxy-functions/shell-api-request.injectable"; + +const lensProxyHttpsServerOnUpgradeInjectable = getInjectable({ + id: "lens-proxy-https-server-on-upgrade", + instantiate: (di) => { + const getClusterForRequest = di.inject(getClusterForRequestInjectable); + const logger = di.inject(loggerInjectable); + const shellApiRequest = di.inject(shellApiRequestInjectable); + const kubeApiUpgradeRequest = di.inject(kubeApiUpgradeRequestInjectable); + + return (req: ProxyIncomingMessage, socket: Socket, head: Buffer) => { + const cluster = getClusterForRequest(req); + + if (!cluster) { + 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; + + void (async () => { + try { + await reqHandler({ req, socket, head, cluster }); + } catch (error) { + logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error); + } + })(); + } + }; + }, +}); + +export default lensProxyHttpsServerOnUpgradeInjectable; diff --git a/packages/core/src/main/lens-proxy/https-proxy/server.injectable.ts b/packages/core/src/main/lens-proxy/https-proxy/server.injectable.ts new file mode 100644 index 0000000000..aefe4dbd26 --- /dev/null +++ b/packages/core/src/main/lens-proxy/https-proxy/server.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { createServer } from "https"; +import lensProxyCertificateInjectable from "../../../common/certificate/lens-proxy-certificate.injectable"; +import handleRouteRequestInjectable from "../handle-route-request.injectable"; +import lensProxyHttpsServerOnUpgradeInjectable from "./on-upgrade.injectable"; +import { ProxyIncomingMessage } from "../messages"; + +const lensProxyHttpsServerInjectable = getInjectable({ + id: "lens-proxy-https-server", + instantiate: (di) => { + const certificate = di.inject(lensProxyCertificateInjectable).get(); + const handleRouteRequest = di.inject(handleRouteRequestInjectable); + + const server = createServer({ + key: certificate.private, + cert: certificate.cert, + IncomingMessage: ProxyIncomingMessage, + }, handleRouteRequest); + + server.on("upgrade", di.inject(lensProxyHttpsServerOnUpgradeInjectable)); + + return server; + }, +}); + +export default lensProxyHttpsServerInjectable; diff --git a/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts b/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts index 57f107415d..2b2af200b2 100644 --- a/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts +++ b/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts @@ -4,33 +4,23 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { LensProxy } from "./lens-proxy"; -import routeRequestInjectable from "../router/route-request.injectable"; -import httpProxy from "http-proxy"; -import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable"; import lensProxyPortInjectable from "./lens-proxy-port.injectable"; -import contentSecurityPolicyInjectable from "../../common/vars/content-security-policy.injectable"; import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable"; import loggerInjectable from "../../common/logger.injectable"; -import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; -import getClusterForRequestInjectable from "./get-cluster-for-request.injectable"; -import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable"; -import kubeApiUpgradeRequestInjectable from "./proxy-functions/kube-api-upgrade-request.injectable"; +import proxyInjectable from "./proxy.injectable"; +import handleRouteRequestInjectable from "./handle-route-request.injectable"; +import lensProxyHttpsServerInjectable from "./https-proxy/server.injectable"; const lensProxyInjectable = getInjectable({ id: "lens-proxy", instantiate: (di) => new LensProxy({ - routeRequest: di.inject(routeRequestInjectable), - proxy: httpProxy.createProxy(), - kubeApiUpgradeRequest: di.inject(kubeApiUpgradeRequestInjectable), - shellApiRequest: di.inject(shellApiRequestInjectable), - getClusterForRequest: di.inject(getClusterForRequestInjectable), + proxy: di.inject(proxyInjectable), + proxyServer: di.inject(lensProxyHttpsServerInjectable), + handleRouteRequest: di.inject(handleRouteRequestInjectable), lensProxyPort: di.inject(lensProxyPortInjectable), - contentSecurityPolicy: di.inject(contentSecurityPolicyInjectable), emitAppEvent: di.inject(emitAppEventInjectable), logger: di.inject(loggerInjectable), - certificate: di.inject(lensProxyCertificateInjectable).get(), - getKubeAuthProxyServer: (cluster) => di.inject(kubeAuthProxyServerInjectable, cluster), }), }); diff --git a/packages/core/src/main/lens-proxy/lens-proxy.ts b/packages/core/src/main/lens-proxy/lens-proxy.ts index 5e8993cdb3..52c140055e 100644 --- a/packages/core/src/main/lens-proxy/lens-proxy.ts +++ b/packages/core/src/main/lens-proxy/lens-proxy.ts @@ -4,73 +4,37 @@ */ import net from "net"; -import https from "https"; +import type https from "https"; import type http from "http"; import type httpProxy from "http-proxy"; -import { apiPrefix, apiKubePrefix } from "../../common/vars"; -import type { RouteRequest } from "../router/route-request.injectable"; import type { Cluster } from "../../common/cluster/cluster"; import type { ProxyApiRequestArgs } from "./proxy-functions"; 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 { KubeAuthProxyServer } from "../cluster/kube-auth-proxy-server.injectable"; -import { isLongRunningRequest } from "./is-long-running-request"; import { disallowedPorts } from "./disallowed-ports"; +import type { HandleRouteRequest } from "./handle-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; - getKubeAuthProxyServer: (cluster: Cluster) => KubeAuthProxyServer; - routeRequest: RouteRequest; + handleRouteRequest: HandleRouteRequest; readonly proxy: httpProxy; readonly lensProxyPort: { set: (portNumber: number) => void }; - readonly contentSecurityPolicy: string; readonly logger: Logger; - readonly certificate: SelfSignedCert; + readonly proxyServer: https.Server; } 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)); - } - }); } /** @@ -81,19 +45,19 @@ export class LensProxy { */ private attemptToListen(): Promise { return new Promise((resolve, reject) => { - this.proxyServer.listen(0, "127.0.0.1"); + this.dependencies.proxyServer.listen(0, "127.0.0.1"); - this.proxyServer + this.dependencies.proxyServer .once("listening", () => { - this.proxyServer.removeAllListeners("error"); // don't reject the promise + this.dependencies.proxyServer.removeAllListeners("error"); // don't reject the promise - const { address, port } = this.proxyServer.address() as net.AddressInfo; + const { address, port } = this.dependencies.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.proxyServer.on("error", (error) => { this.dependencies.logger.info(`[LENS-PROXY]: Subsequent error: ${error}`); }); @@ -116,7 +80,7 @@ export class LensProxy { const seenPorts = new Set(); while(true) { - this.proxyServer?.close(); + this.dependencies.proxyServer?.close(); const port = await this.attemptToListen(); if (!disallowedPorts.has(port)) { @@ -142,7 +106,7 @@ export class LensProxy { close() { this.dependencies.logger.info("[LENS-PROXY]: Closing server"); - this.proxyServer.close(); + this.dependencies.proxyServer.close(); this.closed = true; } @@ -178,7 +142,7 @@ export class LensProxy { this.dependencies.logger.debug(`Retrying proxy request to url: ${reqId}`); setTimeout(() => { this.retryCounters.set(reqId, retryCount + 1); - this.handleRequest(req as ServerIncomingMessage, res) + this.dependencies.handleRouteRequest(req as any, res) .catch(error => this.dependencies.logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`)); }, timeoutMs); } @@ -200,23 +164,4 @@ export class LensProxy { return req.headers.host + req.url; } - - protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) { - const cluster = this.dependencies.getClusterForRequest(req); - - if (cluster && req.url.startsWith(apiKubePrefix)) { - delete req.headers.authorization; - req.url = req.url.replace(apiKubePrefix, ""); - - const kubeAuthProxyServer = this.dependencies.getKubeAuthProxyServer(cluster); - const proxyTarget = await kubeAuthProxyServer.getApiTarget(isLongRunningRequest(req.url)); - - 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/packages/core/src/main/lens-proxy/messages.ts b/packages/core/src/main/lens-proxy/messages.ts new file mode 100644 index 0000000000..dccbeb046d --- /dev/null +++ b/packages/core/src/main/lens-proxy/messages.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import http from "http"; + +export class ProxyIncomingMessage extends http.IncomingMessage { + declare url: string; + declare method: string; +} diff --git a/packages/core/src/main/lens-proxy/proxy.injectable.ts b/packages/core/src/main/lens-proxy/proxy.injectable.ts new file mode 100644 index 0000000000..4a43c9811b --- /dev/null +++ b/packages/core/src/main/lens-proxy/proxy.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { createProxy } from "http-proxy"; + +const proxyInjectable = getInjectable({ + id: "proxy", + instantiate: () => createProxy(), +}); + +export default proxyInjectable;