mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Lens proxy with TLS (#6851)
* lens proxy tls support Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * don't override getRequestOptions if they are set Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix electronAppInjectable override Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use runnables on renderer Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move certificate generation to runnables Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * simplify Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
fba93b889e
commit
7052dc0dba
@ -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);
|
||||
|
||||
@ -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": {
|
||||
|
||||
9
src/common/certificate/lens-proxy-certificate-channel.ts
Normal file
9
src/common/certificate/lens-proxy-certificate-channel.ts
Normal file
@ -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<void, SelfSignedCert>("request-lens-proxy-certificate");
|
||||
@ -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: "<public-data>",
|
||||
private: "<private-data>",
|
||||
cert: "<ca-data>",
|
||||
}),
|
||||
set: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
33
src/common/certificate/lens-proxy-certificate.injectable.ts
Normal file
33
src/common/certificate/lens-proxy-certificate.injectable.ts
Normal file
@ -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;
|
||||
@ -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);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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}`,
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
@ -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);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<string> {
|
||||
const response = await requestPromise({
|
||||
method: "GET",
|
||||
uri: `http://127.0.0.1:${proxyPort}/version`,
|
||||
resolveWithFullResponse: true,
|
||||
proxy: undefined,
|
||||
});
|
||||
|
||||
return JSON.parse(response.body).version;
|
||||
}
|
||||
9
src/common/utils/channel/get-request-channel.ts
Normal file
9
src/common/utils/channel/get-request-channel.ts
Normal file
@ -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 = <Request, Response>(id: string): RequestChannel<Request, Response> => ({
|
||||
id,
|
||||
});
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
export * from "./abort-controller";
|
||||
export * from "./app-version";
|
||||
export * from "./autobind";
|
||||
export * from "./camelCase";
|
||||
export * from "./cluster-id-url-parsing";
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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",
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -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<Electron.App> as Electron.App));
|
||||
|
||||
@ -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<any>;
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<KubeConfig> = {
|
||||
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: [
|
||||
{
|
||||
|
||||
@ -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;
|
||||
@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -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<string, number>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -17,8 +17,6 @@ const devStaticFileRouteHandlerInjectable = getInjectable({
|
||||
return async ({ raw: { req, res }}: LensApiRequest<"/{path*}">): Promise<RouteResponse<Buffer>> => {
|
||||
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 });
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}"`,
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -88,8 +88,8 @@ describe("<NamespaceSelectFilter />", () => {
|
||||
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: {},
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
}, {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user