From 7052dc0dbad276900947f1ce287eb9e89db27777 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 4 Jan 2023 19:18:02 +0200 Subject: [PATCH] Lens proxy with TLS (#6851) * lens proxy tls support Signed-off-by: Jari Kolehmainen * integration test fix Signed-off-by: Jari Kolehmainen * don't override getRequestOptions if they are set Signed-off-by: Jari Kolehmainen * fix electronAppInjectable override Signed-off-by: Jari Kolehmainen * use runnables on renderer Signed-off-by: Jari Kolehmainen * move certificate generation to runnables Signed-off-by: Jari Kolehmainen * simplify Signed-off-by: Jari Kolehmainen Signed-off-by: Jari Kolehmainen --- integration/helpers/utils.ts | 2 +- package.json | 2 +- .../lens-proxy-certificate-channel.ts | 9 ++++ ...tificate.global-override-for-injectable.ts | 19 +++++++ .../lens-proxy-certificate.injectable.ts | 33 ++++++++++++ .../k8s-api/create-json-api.injectable.ts | 19 ++++++- .../create-kube-api-for-cluster.injectable.ts | 2 +- .../create-kube-json-api.injectable.ts | 19 ++++++- src/common/k8s-api/kube-api-parse.ts | 2 +- .../__tests__/cluster-id-url-parsing.test.ts | 22 ++++---- src/common/utils/app-version.ts | 17 ------ .../utils/channel/get-request-channel.ts | 9 ++++ src/common/utils/cluster-id-url-parsing.ts | 2 +- src/common/utils/index.ts | 1 - ...ning-application-window-using-tray.test.ts | 2 +- src/main/__test__/kubeconfig-manager.test.ts | 12 +++-- ...tron-app.global-override-for-injectable.ts | 7 +++ src/main/k8s-request.injectable.ts | 7 +-- .../k8s/api-base-server-address.injectable.ts | 2 +- .../create-kubeconfig-manager.injectable.ts | 2 + .../kubeconfig-manager/kubeconfig-manager.ts | 9 +++- ...-certificate-request-handler.injectable.ts | 23 ++++++++ src/main/lens-proxy/lens-proxy.injectable.ts | 2 + src/main/lens-proxy/lens-proxy.ts | 24 ++++++--- .../routes/files/development.injectable.ts | 2 - .../create-application-window.injectable.ts | 2 +- .../create-electron-window.injectable.ts | 12 ++++- .../runnables/setup-hostnames.injectable.ts | 32 +++++++++++ ...setup-lens-proxy-certificate.injectable.ts | 53 +++++++++++++++++++ .../runnables/setup-lens-proxy.injectable.ts | 16 ++++-- ...setup-lens-proxy-certificate.injectable.ts | 24 +++++++++ ...quest-lens-proxy-certificate.injectable.ts | 18 +++++++ .../namespace-select-filter.test.tsx | 4 +- .../k8s/api-base-server-address.injectable.ts | 2 +- src/renderer/k8s/api-kube.injectable.ts | 6 ++- 35 files changed, 353 insertions(+), 66 deletions(-) create mode 100644 src/common/certificate/lens-proxy-certificate-channel.ts create mode 100644 src/common/certificate/lens-proxy-certificate.global-override-for-injectable.ts create mode 100644 src/common/certificate/lens-proxy-certificate.injectable.ts delete mode 100644 src/common/utils/app-version.ts create mode 100644 src/common/utils/channel/get-request-channel.ts create mode 100644 src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-hostnames.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-lens-proxy-certificate.injectable.ts create mode 100644 src/renderer/before-frame-starts/runnables/setup-lens-proxy-certificate.injectable.ts create mode 100644 src/renderer/certificate/request-lens-proxy-certificate.injectable.ts diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 02aef4f3fd..1d8f5ffb94 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -26,7 +26,7 @@ async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promis const onWindow = (page: Page) => { console.log(`Page opened: ${page.url()}`); - if (page.url().startsWith("http://localhost")) { + if (page.url().startsWith("https://lens.app")) { cleanup(); console.log(stdoutBuf); resolve(page); diff --git a/package.json b/package.json index e19c677e43..9eaeb27df8 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "bundledKubectlVersion": "1.23.3", "bundledHelmVersion": "3.7.2", "sentryDsn": "", - "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:", + "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src https://*.lens.app:*/; img-src * data:", "welcomeRoute": "/welcome" }, "engines": { diff --git a/src/common/certificate/lens-proxy-certificate-channel.ts b/src/common/certificate/lens-proxy-certificate-channel.ts new file mode 100644 index 0000000000..7d9652ce5c --- /dev/null +++ b/src/common/certificate/lens-proxy-certificate-channel.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { SelfSignedCert } from "selfsigned"; +import { getRequestChannel } from "../utils/channel/get-request-channel"; + +export const lensProxyCertificateChannel = getRequestChannel("request-lens-proxy-certificate"); diff --git a/src/common/certificate/lens-proxy-certificate.global-override-for-injectable.ts b/src/common/certificate/lens-proxy-certificate.global-override-for-injectable.ts new file mode 100644 index 0000000000..d547516062 --- /dev/null +++ b/src/common/certificate/lens-proxy-certificate.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 "../test-utils/get-global-override"; +import lensProxyCertificateInjectable from "./lens-proxy-certificate.injectable"; + +export default getGlobalOverride(lensProxyCertificateInjectable, () => { + return { + get: () => ({ + public: "", + private: "", + cert: "", + }), + set: () => {}, + }; +}); + diff --git a/src/common/certificate/lens-proxy-certificate.injectable.ts b/src/common/certificate/lens-proxy-certificate.injectable.ts new file mode 100644 index 0000000000..b5e00e0094 --- /dev/null +++ b/src/common/certificate/lens-proxy-certificate.injectable.ts @@ -0,0 +1,33 @@ +/** + * 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 { SelfSignedCert } from "selfsigned"; + +const lensProxyCertificateInjectable = getInjectable({ + id: "lens-proxy-certificate", + instantiate: () => { + let certState: SelfSignedCert; + const cert = { + get: () => { + if (!certState) { + throw "certificate has not been set"; + } + + return certState; + }, + set: (certificate: SelfSignedCert) => { + if (certState) { + throw "certificate has already been set"; + } + + certState = certificate; + }, + }; + + return cert; + }, +}); + +export default lensProxyCertificateInjectable; diff --git a/src/common/k8s-api/create-json-api.injectable.ts b/src/common/k8s-api/create-json-api.injectable.ts index aa05a5c157..7f8559bf3a 100644 --- a/src/common/k8s-api/create-json-api.injectable.ts +++ b/src/common/k8s-api/create-json-api.injectable.ts @@ -3,7 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { Agent } from "https"; import type { RequestInit } from "node-fetch"; +import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable"; import fetchInjectable from "../fetch/fetch.injectable"; import loggerInjectable from "../logger.injectable"; import type { JsonApiConfig, JsonApiData, JsonApiDependencies, JsonApiParams } from "./json-api"; @@ -18,8 +20,23 @@ const createJsonApiInjectable = getInjectable({ fetch: di.inject(fetchInjectable), logger: di.inject(loggerInjectable), }; + const lensProxyCert = di.inject(lensProxyCertificateInjectable); - return (config, reqInit) => new JsonApi(deps, config, reqInit); + return (config, reqInit) => { + if (!config.getRequestOptions) { + config.getRequestOptions = async () => { + const agent = new Agent({ + ca: lensProxyCert.get().cert, + }); + + return { + agent, + }; + }; + } + + return new JsonApi(deps, config, reqInit); + }; }, }); diff --git a/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts b/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts index eec3752e3a..a90a52456d 100644 --- a/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts +++ b/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts @@ -51,7 +51,7 @@ const createKubeApiForClusterInjectable = getInjectable({ debug: isDevelopment, }, { headers: { - "Host": `${cluster.metadata.uid}.localhost:${new URL(apiBase.config.serverAddress).port}`, + "Host": `${cluster.metadata.uid}.lens.app:${new URL(apiBase.config.serverAddress).port}`, }, }, ), diff --git a/src/common/k8s-api/create-kube-json-api.injectable.ts b/src/common/k8s-api/create-kube-json-api.injectable.ts index 93de5a0d21..f7b5a152ee 100644 --- a/src/common/k8s-api/create-kube-json-api.injectable.ts +++ b/src/common/k8s-api/create-kube-json-api.injectable.ts @@ -3,7 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { Agent } from "https"; import type { RequestInit } from "node-fetch"; +import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable"; import fetchInjectable from "../fetch/fetch.injectable"; import loggerInjectable from "../logger.injectable"; import type { JsonApiConfig, JsonApiDependencies } from "./json-api"; @@ -18,8 +20,23 @@ const createKubeJsonApiInjectable = getInjectable({ fetch: di.inject(fetchInjectable), logger: di.inject(loggerInjectable), }; + const lensProxyCert = di.inject(lensProxyCertificateInjectable); - return (config, reqInit) => new KubeJsonApi(dependencies, config, reqInit); + return (config, reqInit) => { + if (!config.getRequestOptions) { + config.getRequestOptions = async () => { + const agent = new Agent({ + ca: lensProxyCert.get().cert, + }); + + return { + agent, + }; + }; + } + + return new KubeJsonApi(dependencies, config, reqInit); + }; }, }); diff --git a/src/common/k8s-api/kube-api-parse.ts b/src/common/k8s-api/kube-api-parse.ts index dcb8b18636..cb5315b50c 100644 --- a/src/common/k8s-api/kube-api-parse.ts +++ b/src/common/k8s-api/kube-api-parse.ts @@ -23,7 +23,7 @@ export interface IKubeApiParsed extends IKubeApiLinkRef { } export function parseKubeApi(path: string): IKubeApiParsed { - const apiPath = new URL(path, "http://localhost").pathname; + const apiPath = new URL(path, "https://localhost").pathname; const [, prefix, ...parts] = apiPath.split("/"); const apiPrefix = `/${prefix}`; const [left, right, namespaced] = splitArray(parts, "namespaces"); diff --git a/src/common/utils/__tests__/cluster-id-url-parsing.test.ts b/src/common/utils/__tests__/cluster-id-url-parsing.test.ts index 9f60eff8ac..3a4ccf605b 100644 --- a/src/common/utils/__tests__/cluster-id-url-parsing.test.ts +++ b/src/common/utils/__tests__/cluster-id-url-parsing.test.ts @@ -9,22 +9,22 @@ describe("getClusterIdFromHost", () => { const clusterFakeId = "fe540901-0bd6-4f6c-b472-bce1559d7c4a"; it("should return undefined for non cluster frame hosts", () => { - expect(getClusterIdFromHost("localhost:45345")).toBeUndefined(); + expect(getClusterIdFromHost("lens.app:45345")).toBeUndefined(); }); it("should return ClusterId for cluster frame hosts", () => { - expect(getClusterIdFromHost(`${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); }); it("should return ClusterId for cluster frame hosts with additional subdomains", () => { - expect(getClusterIdFromHost(`abc.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); - expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId); }); }); diff --git a/src/common/utils/app-version.ts b/src/common/utils/app-version.ts deleted file mode 100644 index 183cc3e6b2..0000000000 --- a/src/common/utils/app-version.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import requestPromise from "request-promise-native"; - -export async function getAppVersionFromProxyServer(proxyPort: number): Promise { - const response = await requestPromise({ - method: "GET", - uri: `http://127.0.0.1:${proxyPort}/version`, - resolveWithFullResponse: true, - proxy: undefined, - }); - - return JSON.parse(response.body).version; -} diff --git a/src/common/utils/channel/get-request-channel.ts b/src/common/utils/channel/get-request-channel.ts new file mode 100644 index 0000000000..4dc5b4914e --- /dev/null +++ b/src/common/utils/channel/get-request-channel.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RequestChannel } from "./request-channel-listener-injection-token"; + +export const getRequestChannel = (id: string): RequestChannel => ({ + id, +}); diff --git a/src/common/utils/cluster-id-url-parsing.ts b/src/common/utils/cluster-id-url-parsing.ts index cb02f36484..393a791f09 100644 --- a/src/common/utils/cluster-id-url-parsing.ts +++ b/src/common/utils/cluster-id-url-parsing.ts @@ -14,7 +14,7 @@ export function getClusterIdFromHost(host: string): ClusterId | undefined { // e.g host == "%clusterId.localhost:45345" const subDomains = host.split(":")[0].split("."); - return subDomains.slice(-2, -1)[0]; // ClusterId or undefined + return subDomains.slice(-3, -2)[0]; // ClusterId or undefined } /** diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 36e77c6e79..4857d04418 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -4,7 +4,6 @@ */ export * from "./abort-controller"; -export * from "./app-version"; export * from "./autobind"; export * from "./camelCase"; export * from "./cluster-id-url-parsing"; diff --git a/src/features/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts b/src/features/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts index 1dccbf71da..23b8d55992 100644 --- a/src/features/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts +++ b/src/features/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts @@ -132,7 +132,7 @@ describe("opening application window using tray", () => { }); it("starts loading of content for the application window", () => { - expect(callForApplicationWindowHtmlMock).toHaveBeenCalledWith("http://localhost:42"); + expect(callForApplicationWindowHtmlMock).toHaveBeenCalledWith("https://lens.app:42"); }); describe("given static HTML of application window has not resolved yet, when opening from tray again", () => { diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 43c8777b2e..689ef13ce8 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -102,7 +102,7 @@ describe("kubeconfig manager tests", () => { clusterServerUrl, }); - jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo"); + jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("https://127.0.0.1:9191/foo"); kubeConfManager = createKubeconfigManager(clusterFake); }); @@ -173,7 +173,10 @@ describe("kubeconfig manager tests", () => { describe("when writing out new proxy kubeconfig resolves", () => { beforeEach(async () => { await writeFileMock.resolveSpecific( - ["/some-directory-for-temp/kubeconfig-foo", "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n server: http://127.0.0.1:9191/foo\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user: {}\n"], + [ + "/some-directory-for-temp/kubeconfig-foo", + "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n", + ], ); }); @@ -299,7 +302,10 @@ describe("kubeconfig manager tests", () => { describe("when writing out new proxy kubeconfig resolves", () => { beforeEach(async () => { await writeFileMock.resolveSpecific( - ["/some-directory-for-temp/kubeconfig-foo", "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n server: http://127.0.0.1:9191/foo\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user: {}\n"], + [ + "/some-directory-for-temp/kubeconfig-foo", + "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n", + ], ); }); diff --git a/src/main/electron-app/electron-app.global-override-for-injectable.ts b/src/main/electron-app/electron-app.global-override-for-injectable.ts index ea74c209e1..c7cc065cb2 100644 --- a/src/main/electron-app/electron-app.global-override-for-injectable.ts +++ b/src/main/electron-app/electron-app.global-override-for-injectable.ts @@ -9,4 +9,11 @@ import electronAppInjectable from "./electron-app.injectable"; export default getGlobalOverride(electronAppInjectable, () => ({ getVersion: () => "6.0.0", setLoginItemSettings: () => {}, + commandLine: { + appendArgument: () => {}, + appendSwitch: () => {}, + getSwitchValue: () => "", + hasSwitch: () => false, + removeSwitch: () => {}, + }, } as Partial as Electron.App)); diff --git a/src/main/k8s-request.injectable.ts b/src/main/k8s-request.injectable.ts index a1a56be7dd..c00e70ec3f 100644 --- a/src/main/k8s-request.injectable.ts +++ b/src/main/k8s-request.injectable.ts @@ -4,10 +4,10 @@ */ import type { RequestPromiseOptions } from "request-promise-native"; import request from "request-promise-native"; -import { apiKubePrefix } from "../common/vars"; import type { Cluster } from "../common/cluster/cluster"; import { getInjectable } from "@ogre-tools/injectable"; import lensProxyPortInjectable from "./lens-proxy/lens-proxy-port.injectable"; +import lensProxyCertificateInjectable from "../common/certificate/lens-proxy-certificate.injectable"; export type K8sRequest = (cluster: Cluster, path: string, options?: RequestPromiseOptions) => Promise; @@ -16,18 +16,19 @@ const k8sRequestInjectable = getInjectable({ instantiate: (di) => { const lensProxyPort = di.inject(lensProxyPortInjectable); + const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); return async ( cluster: Cluster, path: string, options: RequestPromiseOptions = {}, ) => { - const kubeProxyUrl = `http://localhost:${lensProxyPort.get()}${apiKubePrefix}`; + const kubeProxyUrl = `https://127.0.0.1:${lensProxyPort.get()}/${cluster.id}`; + options.ca = lensProxyCertificate.get().cert; options.headers ??= {}; options.json ??= true; options.timeout ??= 30000; - options.headers.Host = `${cluster.id}.${new URL(kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest() return request(kubeProxyUrl + path, options); }; diff --git a/src/main/k8s/api-base-server-address.injectable.ts b/src/main/k8s/api-base-server-address.injectable.ts index ae217cb3e2..8d3a960ed4 100644 --- a/src/main/k8s/api-base-server-address.injectable.ts +++ b/src/main/k8s/api-base-server-address.injectable.ts @@ -11,7 +11,7 @@ const apiBaseServerAddressInjectable = getInjectable({ instantiate: (di) => { const lensProxyPort = di.inject(lensProxyPortInjectable); - return `http://127.0.0.1:${lensProxyPort.get()}`; + return `https://127.0.0.1:${lensProxyPort.get()}`; }, injectionToken: apiBaseServerAddressInjectionToken, }); diff --git a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts index 6bca7cb086..010ec7174e 100644 --- a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts +++ b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts @@ -14,6 +14,7 @@ import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import writeFileInjectable from "../../common/fs/write-file.injectable"; import removePathInjectable from "../../common/fs/remove.injectable"; +import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; export interface KubeConfigManagerInstantiationParameter { cluster: Cluster; @@ -34,6 +35,7 @@ const createKubeconfigManagerInjectable = getInjectable({ removePath: di.inject(removePathInjectable), pathExists: di.inject(pathExistsInjectable), writeFile: di.inject(writeFileInjectable), + certificate: di.inject(lensProxyCertificateInjectable).get(), }; return (cluster) => new KubeconfigManager(dependencies, cluster); diff --git a/src/main/kubeconfig-manager/kubeconfig-manager.ts b/src/main/kubeconfig-manager/kubeconfig-manager.ts index f73baf1782..0486521e21 100644 --- a/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -15,6 +15,7 @@ import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable" import type { PathExists } from "../../common/fs/path-exists.injectable"; import type { RemovePath } from "../../common/fs/remove.injectable"; import type { WriteFile } from "../../common/fs/write-file.injectable"; +import type { SelfSignedCert } from "selfsigned"; export interface KubeconfigManagerDependencies { readonly directoryForTemp: string; @@ -25,6 +26,7 @@ export interface KubeconfigManagerDependencies { pathExists: PathExists; removePath: RemovePath; writeFile: WriteFile; + certificate: SelfSignedCert; } export class KubeconfigManager { @@ -86,7 +88,7 @@ export class KubeconfigManager { } get resolveProxyUrl() { - return `http://127.0.0.1:${this.dependencies.lensProxyPort.get()}/${this.cluster.id}`; + return `https://127.0.0.1:${this.dependencies.lensProxyPort.get()}/${this.cluster.id}`; } /** @@ -101,16 +103,19 @@ export class KubeconfigManager { `kubeconfig-${id}`, ); const kubeConfig = await cluster.getKubeconfig(); + const { certificate } = this.dependencies; const proxyConfig: PartialDeep = { currentContext: contextName, clusters: [ { name: contextName, server: this.resolveProxyUrl, + skipTLSVerify: false, + caData: Buffer.from(certificate.cert).toString("base64"), }, ], users: [ - { name: "proxy" }, + { name: "proxy", username: "lens", password: "fake" }, ], contexts: [ { diff --git a/src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts b/src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts new file mode 100644 index 0000000000..c26d1ac275 --- /dev/null +++ b/src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { SelfSignedCert } from "selfsigned"; +import { lensProxyCertificateChannel } from "../../common/certificate/lens-proxy-certificate-channel"; +import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; +import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; + +const lensProxyCertificateRequestHandlerInjectable = getRequestChannelListenerInjectable({ + channel: lensProxyCertificateChannel, + handler: (di) => { + const lensProxyCertificate = di.inject(lensProxyCertificateInjectable).get() as SelfSignedCert; + + return () => ({ + cert: lensProxyCertificate.cert, + public: lensProxyCertificate.public, + private: "", + }); + }, +}); + +export default lensProxyCertificateRequestHandlerInjectable; diff --git a/src/main/lens-proxy/lens-proxy.injectable.ts b/src/main/lens-proxy/lens-proxy.injectable.ts index 1c6444ccaa..998312b535 100644 --- a/src/main/lens-proxy/lens-proxy.injectable.ts +++ b/src/main/lens-proxy/lens-proxy.injectable.ts @@ -13,6 +13,7 @@ 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"; const lensProxyInjectable = getInjectable({ id: "lens-proxy", @@ -27,6 +28,7 @@ const lensProxyInjectable = getInjectable({ contentSecurityPolicy: di.inject(contentSecurityPolicyInjectable), emitAppEvent: di.inject(emitAppEventInjectable), logger: di.inject(loggerInjectable), + certificate: di.inject(lensProxyCertificateInjectable).get(), }), }); diff --git a/src/main/lens-proxy/lens-proxy.ts b/src/main/lens-proxy/lens-proxy.ts index 5dc2794299..9b1d80270a 100644 --- a/src/main/lens-proxy/lens-proxy.ts +++ b/src/main/lens-proxy/lens-proxy.ts @@ -4,7 +4,8 @@ */ import net from "net"; -import http from "http"; +import https from "https"; +import type http from "http"; import type httpProxy from "http-proxy"; import { apiPrefix, apiKubePrefix } from "../../common/vars"; import type { Router } from "../router/router"; @@ -16,6 +17,7 @@ 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"; type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined; @@ -31,6 +33,7 @@ interface Dependencies { readonly lensProxyPort: { set: (portNumber: number) => void }; readonly contentSecurityPolicy: string; readonly logger: Logger; + readonly certificate: SelfSignedCert; } const watchParam = "watch"; @@ -61,27 +64,33 @@ const disallowedPorts = new Set([ ]); export class LensProxy { - protected proxyServer: http.Server; + protected proxyServer: https.Server; protected closed = false; protected retryCounters = new Map(); constructor(private readonly dependencies: Dependencies) { this.configureProxy(dependencies.proxy); - this.proxyServer = http.createServer((req, res) => { - this.handleRequest(req as ServerIncomingMessage, res); - }); + 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 = dependencies.getClusterForRequest(req); + 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 ? dependencies.shellApiRequest : dependencies.kubeApiUpgradeRequest; + 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)); @@ -157,6 +166,7 @@ export class LensProxy { close() { this.dependencies.logger.info("[LENS-PROXY]: Closing server"); + this.proxyServer.close(); this.closed = true; } diff --git a/src/main/routes/files/development.injectable.ts b/src/main/routes/files/development.injectable.ts index 6bad03f7b9..3251a9937b 100644 --- a/src/main/routes/files/development.injectable.ts +++ b/src/main/routes/files/development.injectable.ts @@ -17,8 +17,6 @@ const devStaticFileRouteHandlerInjectable = getInjectable({ return async ({ raw: { req, res }}: LensApiRequest<"/{path*}">): Promise> => { if (req.url === "/" || !req.url || !req.url.startsWith(publicPath)) { req.url = `${publicPath}/index.html`; - } else if (!req.url.startsWith("/build/")) { - return { statusCode: 404 }; } proxy.web(req, res, { target: proxyTarget }); diff --git a/src/main/start-main-application/lens-window/application-window/create-application-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-application-window.injectable.ts index 703542faaa..b0e5cac982 100644 --- a/src/main/start-main-application/lens-window/application-window/create-application-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-application-window.injectable.ts @@ -33,7 +33,7 @@ const createApplicationWindowInjectable = getInjectable({ defaultHeight: 900, defaultWidth: 1440, getContentSource: () => ({ - url: `http://localhost:${lensProxyPort.get()}`, + url: `https://lens.app:${lensProxyPort.get()}`, }), resizable: true, windowFrameUtilitiesAreShown: isMac, diff --git a/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts index 2d763c6361..bd84909a49 100644 --- a/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts @@ -3,6 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { timingSafeEqual, X509Certificate } from "crypto"; import loggerInjectable from "../../../../common/logger.injectable"; import applicationWindowStateInjectable from "./application-window-state.injectable"; import { BrowserWindow } from "electron"; @@ -14,7 +15,7 @@ import lensResourcesDirInjectable from "../../../../common/vars/lens-resources-d import isLinuxInjectable from "../../../../common/vars/is-linux.injectable"; import applicationInformationToken from "../../../../common/vars/application-information-token"; import pathExistsSyncInjectable from "../../../../common/fs/path-exists-sync.injectable"; - +import lensProxyCertificateInjectable from "../../../../common/certificate/lens-proxy-certificate.injectable"; export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover"; @@ -56,6 +57,8 @@ const createElectronWindowInjectable = getInjectable({ const isLinux = di.inject(isLinuxInjectable); const applicationInformation = di.inject(applicationInformationToken); const pathExistsSync = di.inject(pathExistsSyncInjectable); + const lensProxyCertificate = di.inject(lensProxyCertificateInjectable).get(); + const lensProxyX509Cert = new X509Certificate(lensProxyCertificate.cert); return (configuration) => { const applicationWindowState = di.inject( @@ -123,6 +126,13 @@ const createElectronWindowInjectable = getInjectable({ .webContents.on("dom-ready", () => { configuration.onDomReady?.(); }) + .on("certificate-error", (event, url, error, certificate, shouldBeTrusted) => { + const cert = new X509Certificate(certificate.data); + const shouldTrustCert = cert.raw.length === lensProxyX509Cert.raw.length + && timingSafeEqual(cert.raw, lensProxyX509Cert.raw); + + shouldBeTrusted(shouldTrustCert); + }) .on("did-fail-load", (_event, code, desc) => { logger.error( `[CREATE-ELECTRON-WINDOW]: Failed to load window "${configuration.id}"`, diff --git a/src/main/start-main-application/runnables/setup-hostnames.injectable.ts b/src/main/start-main-application/runnables/setup-hostnames.injectable.ts new file mode 100644 index 0000000000..265ff49162 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-hostnames.injectable.ts @@ -0,0 +1,32 @@ +/** + * 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 electronAppInjectable from "../../electron-app/electron-app.injectable"; +import { beforeElectronIsReadyInjectionToken } from "../runnable-tokens/before-electron-is-ready-injection-token"; + +const setupHostnamesInjectable = getInjectable({ + id: "setup-hostnames", + + instantiate: (di) => { + const app = di.inject(electronAppInjectable); + + return { + id: "setup-hostnames", + run: () => { + app.commandLine.appendSwitch("host-rules", [ + "MAP localhost 127.0.0.1", + "MAP lens.app 127.0.0.1", + "MAP *.lens.app 127.0.0.1", + ].join()); + + return undefined; + }, + }; + }, + + injectionToken: beforeElectronIsReadyInjectionToken, +}); + +export default setupHostnamesInjectable; diff --git a/src/main/start-main-application/runnables/setup-lens-proxy-certificate.injectable.ts b/src/main/start-main-application/runnables/setup-lens-proxy-certificate.injectable.ts new file mode 100644 index 0000000000..9957877ab5 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-lens-proxy-certificate.injectable.ts @@ -0,0 +1,53 @@ +/** + * 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 { generate } from "selfsigned"; +import lensProxyCertificateInjectable from "../../../common/certificate/lens-proxy-certificate.injectable"; +import { beforeElectronIsReadyInjectionToken } from "../runnable-tokens/before-electron-is-ready-injection-token"; + +const setupLensProxyCertificateInjectable = getInjectable({ + id: "setup-lens-proxy-certificate", + + instantiate: (di) => { + const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); + + return { + id: "setup-lens-proxy-certificate", + run: () => { + const cert = generate([ + { name: "commonName", value: "Lens Certificate Authority" }, + { name: "organizationName", value: "Lens" }, + ], { + keySize: 2048, + algorithm: "sha256", + days: 365, + extensions: [ + { + name: "basicConstraints", + cA: true, + }, + { + name: "subjectAltName", + altNames: [ + { type: 2, value: "*.lens.app" }, + { type: 2, value: "lens.app" }, + { type: 2, value: "localhost" }, + { type: 7, ip: "127.0.0.1" }, + ], + }, + ], + }); + + lensProxyCertificate.set(cert); + + return undefined; + }, + }; + }, + + injectionToken: beforeElectronIsReadyInjectionToken, +}); + +export default setupLensProxyCertificateInjectable; diff --git a/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts b/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts index 281c1ff844..69184ddec2 100644 --- a/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts +++ b/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { getAppVersionFromProxyServer } from "../../../common/utils"; import exitAppInjectable from "../../electron-app/features/exit-app.injectable"; import lensProxyInjectable from "../../lens-proxy/lens-proxy.injectable"; import loggerInjectable from "../../../common/logger.injectable"; @@ -13,6 +12,9 @@ import showErrorPopupInjectable from "../../electron-app/features/show-error-pop import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import initializeBuildVersionInjectable from "../../vars/build-version/init.injectable"; +import lensProxyCertificateInjectable from "../../../common/certificate/lens-proxy-certificate.injectable"; +import fetchInjectable from "../../../common/fetch/fetch.injectable"; +import { Agent } from "https"; const setupLensProxyInjectable = getInjectable({ id: "setup-lens-proxy", @@ -25,6 +27,8 @@ const setupLensProxyInjectable = getInjectable({ const isWindows = di.inject(isWindowsInjectable); const showErrorPopup = di.inject(showErrorPopupInjectable); const buildVersion = di.inject(buildVersionInjectable); + const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); + const fetch = di.inject(fetchInjectable); return { id: "setup-lens-proxy", @@ -41,9 +45,13 @@ const setupLensProxyInjectable = getInjectable({ // test proxy connection try { logger.info("🔎 Testing LensProxy connection ..."); - const versionFromProxy = await getAppVersionFromProxyServer( - lensProxyPort.get(), - ); + const versionResponse = await fetch(`https://127.0.0.1:${lensProxyPort.get()}/version`, { + agent: new Agent({ + ca: lensProxyCertificate.get()?.cert, + }), + }); + + const { version: versionFromProxy } = await versionResponse.json() as { version: string }; if (buildVersion.get() !== versionFromProxy) { logger.error("Proxy server responded with invalid response"); diff --git a/src/renderer/before-frame-starts/runnables/setup-lens-proxy-certificate.injectable.ts b/src/renderer/before-frame-starts/runnables/setup-lens-proxy-certificate.injectable.ts new file mode 100644 index 0000000000..e698eda6dd --- /dev/null +++ b/src/renderer/before-frame-starts/runnables/setup-lens-proxy-certificate.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 { beforeFrameStartsFirstInjectionToken } from "../tokens"; +import lensProxyCertificateInjectable from "../../../common/certificate/lens-proxy-certificate.injectable"; +import requestLensProxyCertificateInjectable from "../../certificate/request-lens-proxy-certificate.injectable"; + +const setupLensProxyCertificateInjectable = getInjectable({ + id: "setup-lens-proxy-certificate", + instantiate: (di) => ({ + id: "setup-lens-proxy-certificate", + run: async () => { + const requestLensProxyCertificate = di.inject(requestLensProxyCertificateInjectable); + const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); + + lensProxyCertificate.set(await requestLensProxyCertificate()); + }, + }), + injectionToken: beforeFrameStartsFirstInjectionToken, +}); + +export default setupLensProxyCertificateInjectable; diff --git a/src/renderer/certificate/request-lens-proxy-certificate.injectable.ts b/src/renderer/certificate/request-lens-proxy-certificate.injectable.ts new file mode 100644 index 0000000000..e1322096bf --- /dev/null +++ b/src/renderer/certificate/request-lens-proxy-certificate.injectable.ts @@ -0,0 +1,18 @@ +/** + * 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 { lensProxyCertificateChannel } from "../../common/certificate/lens-proxy-certificate-channel"; +import requestFromChannelInjectable from "../utils/channel/request-from-channel.injectable"; + +const requestLensProxyCertificateInjectable = getInjectable({ + id: "request-lens-proxy-certificate", + instantiate: (di) => { + const requestFromChannel = di.inject(requestFromChannelInjectable); + + return () => requestFromChannel(lensProxyCertificateChannel); + }, +}); + +export default requestLensProxyCertificateInjectable; diff --git a/src/renderer/components/+namespaces/namespace-select-filter.test.tsx b/src/renderer/components/+namespaces/namespace-select-filter.test.tsx index 0a4626b251..7d7095367b 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.test.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.test.tsx @@ -88,8 +88,8 @@ describe("", () => { describe("once the subscribe resolves", () => { beforeEach(async () => { await fetchMock.resolveSpecific([ - "http://127.0.0.1:12345/api-kube/api/v1/namespaces", - ], createMockResponseFromString("http://127.0.0.1:12345/api-kube/api/v1/namespaces", JSON.stringify({ + "https://127.0.0.1:12345/api-kube/api/v1/namespaces", + ], createMockResponseFromString("https://127.0.0.1:12345/api-kube/api/v1/namespaces", JSON.stringify({ apiVersion: "v1", kind: "NamespaceList", metadata: {}, diff --git a/src/renderer/k8s/api-base-server-address.injectable.ts b/src/renderer/k8s/api-base-server-address.injectable.ts index acb1f525d3..17f4028ea8 100644 --- a/src/renderer/k8s/api-base-server-address.injectable.ts +++ b/src/renderer/k8s/api-base-server-address.injectable.ts @@ -11,7 +11,7 @@ const apiBaseServerAddressInjectable = getInjectable({ instantiate: (di) => { const { port } = di.inject(windowLocationInjectable); - return `http://127.0.0.1:${port}`; + return `https://127.0.0.1:${port}`; }, injectionToken: apiBaseServerAddressInjectionToken, }); diff --git a/src/renderer/k8s/api-kube.injectable.ts b/src/renderer/k8s/api-kube.injectable.ts index c7c87194a5..9e6bd8c7f8 100644 --- a/src/renderer/k8s/api-kube.injectable.ts +++ b/src/renderer/k8s/api-kube.injectable.ts @@ -11,18 +11,20 @@ import createKubeJsonApiInjectable from "../../common/k8s-api/create-kube-json-a import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; import showErrorNotificationInjectable from "../components/notifications/show-error-notification.injectable"; import windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; +import { apiBaseServerAddressInjectionToken } from "../../common/k8s-api/api-base-configs"; const apiKubeInjectable = getInjectable({ id: "api-kube", instantiate: (di) => { assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "apiKube is only available in certain environments"); const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); + const apiBaseServerAddress = di.inject(apiBaseServerAddressInjectionToken); const isDevelopment = di.inject(isDevelopmentInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); - const { port, host } = di.inject(windowLocationInjectable); + const { host } = di.inject(windowLocationInjectable); const apiKube = createKubeJsonApi({ - serverAddress: `http://127.0.0.1:${port}`, + serverAddress: apiBaseServerAddress, apiBase: apiKubePrefix, debug: isDevelopment, }, {