diff --git a/src/main/index.ts b/src/main/index.ts index ae3d747958..c8a9d105d2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -28,7 +28,7 @@ import * as LensExtensionsMainApi from "../extensions/main-api"; import { app, autoUpdater, dialog, powerMonitor } from "electron"; import { appName, isMac, productName } from "../common/vars"; import path from "path"; -import { LensProxy } from "./proxy/lens-proxy"; +import { LensProxy } from "./lens-proxy"; import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; import { shellSync } from "./shell-sync"; @@ -49,7 +49,6 @@ import { pushCatalogToRenderer } from "./catalog-pusher"; import { catalogEntityRegistry } from "./catalog"; import { HelmRepoManager } from "./helm/helm-repo-manager"; import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources"; -import { handleWsUpgrade } from "./proxy/ws-upgrade"; import configurePackages from "../common/configure-packages"; import { PrometheusProviderRegistry } from "./prometheus"; import * as initializers from "./initializers"; @@ -61,6 +60,10 @@ import { ExtensionsStore } from "../extensions/extensions-store"; import { FilesystemProvisionerStore } from "./extension-filesystem"; import { SentryInit } from "../common/sentry"; import { ensureDir } from "fs-extra"; +import { Router } from "./router"; +import { initMenu } from "./menu"; +import { initTray } from "./tray"; +import { kubeApiRequest, shellApiRequest } from "./proxy-functions"; SentryInit(); @@ -158,10 +161,11 @@ app.on("ready", async () => { HelmRepoManager.createInstance(); // create the instance - const lensProxy = LensProxy.createInstance( - handleWsUpgrade, - req => ClusterManager.getInstance().getClusterForRequest(req), - ); + const lensProxy = LensProxy.createInstance(new Router(), { + getClusterForRequest: req => ClusterManager.getInstance().getClusterForRequest(req), + kubeApiRequest, + shellApiRequest, + }); ClusterManager.createInstance().init(); KubeconfigSyncManager.createInstance(); @@ -205,6 +209,11 @@ app.on("ready", async () => { logger.info("🖥️ Starting WindowManager"); const windowManager = WindowManager.createInstance(); + cleanup.push( + initMenu(windowManager), + initTray(windowManager), + ); + installDeveloperTools(); if (!startHidden) { diff --git a/src/main/k8s-request.ts b/src/main/k8s-request.ts index b501bd5ce1..513860fbd9 100644 --- a/src/main/k8s-request.ts +++ b/src/main/k8s-request.ts @@ -22,7 +22,7 @@ import request, { RequestPromiseOptions } from "request-promise-native"; import { apiKubePrefix } from "../common/vars"; import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; -import { LensProxy } from "./proxy/lens-proxy"; +import { LensProxy } from "./lens-proxy"; import type { Cluster } from "./cluster"; export async function k8sRequest(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise { diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index fc2e14a3cb..ab53118f60 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -27,7 +27,7 @@ import path from "path"; import fs from "fs-extra"; import { dumpConfigYaml } from "../common/kube-helpers"; import logger from "./logger"; -import { LensProxy } from "./proxy/lens-proxy"; +import { LensProxy } from "./lens-proxy"; export class KubeconfigManager { protected configDir = app.getPath("temp"); diff --git a/src/main/proxy/lens-proxy.ts b/src/main/lens-proxy.ts similarity index 65% rename from src/main/proxy/lens-proxy.ts rename to src/main/lens-proxy.ts index 9ffff0fd6a..fef1183f32 100644 --- a/src/main/proxy/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -19,33 +19,42 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import net from "net"; +import type net from "net"; import type http from "http"; import spdy from "spdy"; import httpProxy from "http-proxy"; -import url from "url"; -import { apiPrefix, apiKubePrefix } from "../../common/vars"; -import { Router } from "../router"; -import type { ContextHandler } from "../context-handler"; -import logger from "../logger"; -import { Singleton } from "../../common/utils"; -import type { Cluster } from "../cluster"; +import { apiPrefix, apiKubePrefix } from "../common/vars"; +import type { Router } from "./router"; +import type { ContextHandler } from "./context-handler"; +import logger from "./logger"; +import { Singleton } from "../common/utils"; +import type { Cluster } from "./cluster"; +import type { ProxyApiRequestArgs } from "./proxy-functions"; -type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void; +type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null; + +export interface LensProxyFunctions { + getClusterForRequest: GetClusterForRequest, + shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise; + kubeApiRequest: (args: ProxyApiRequestArgs) => void | Promise; +} export class LensProxy extends Singleton { protected origin: string; protected proxyServer: http.Server; - protected router = new Router(); protected closed = false; protected retryCounters = new Map(); + protected proxy = this.createProxy(); + protected getClusterForRequest: GetClusterForRequest; public port: number; - constructor(handleWsUpgrade: WSUpgradeHandler, protected getClusterForRequest: (req: http.IncomingMessage) => Cluster | undefined) { + constructor(protected router: Router, functions: LensProxyFunctions) { super(); - const proxy = this.createProxy(); + const { shellApiRequest, kubeApiRequest } = functions; + + this.getClusterForRequest = functions.getClusterForRequest; this.proxyServer = spdy.createServer({ spdy: { @@ -53,17 +62,16 @@ export class LensProxy extends Singleton { protocols: ["http/1.1", "spdy/3.1"] } }, (req: http.IncomingMessage, res: http.ServerResponse) => { - this.handleRequest(proxy, req, res); + this.handleRequest(req, res); }); this.proxyServer .on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { - if (req.url.startsWith(`${apiPrefix}?`)) { - handleWsUpgrade(req, socket, head); - } else { - this.handleProxyUpgrade(proxy, req, socket, head) - .catch(error => logger.error(`[LENS-PROXY]: failed to handle proxy upgrade: ${error}`)); - } + const isInternal = req.url.startsWith(`${apiPrefix}?`); + const reqHandler = isInternal ? shellApiRequest : kubeApiRequest; + + (async () => reqHandler({ req, socket, head }))() + .catch(error => logger.error(logger.error(`[LENS-PROXY]: failed to handle proxy upgrade: ${error}`))); }); } @@ -104,58 +112,6 @@ export class LensProxy extends Singleton { this.closed = true; } - protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { - const cluster = this.getClusterForRequest(req); - - if (cluster) { - const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); - const apiUrl = url.parse(cluster.apiUrl); - const pUrl = url.parse(proxyUrl); - const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }; - const proxySocket = new net.Socket(); - - proxySocket.connect(connectOpts, () => { - proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`); - proxySocket.write(`Host: ${apiUrl.host}\r\n`); - - for (let i = 0; i < req.rawHeaders.length; i += 2) { - const key = req.rawHeaders[i]; - - if (key !== "Host" && key !== "Authorization") { - proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`); - } - } - proxySocket.write("\r\n"); - proxySocket.write(head); - }); - - proxySocket.setKeepAlive(true); - socket.setKeepAlive(true); - proxySocket.setTimeout(0); - socket.setTimeout(0); - - proxySocket.on("data", function (chunk) { - socket.write(chunk); - }); - proxySocket.on("end", function () { - socket.end(); - }); - proxySocket.on("error", function () { - socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`); - socket.end(); - }); - socket.on("data", function (chunk) { - proxySocket.write(chunk); - }); - socket.on("end", function () { - proxySocket.end(); - }); - socket.on("error", function () { - proxySocket.end(); - }); - } - } - protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); @@ -195,7 +151,7 @@ export class LensProxy extends Singleton { logger.debug(`Retrying proxy request to url: ${reqId}`); setTimeout(() => { this.retryCounters.set(reqId, retryCount + 1); - this.handleRequest(proxy, req, res) + this.handleRequest(req, res) .catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`)); }, timeoutMs); } @@ -226,7 +182,7 @@ export class LensProxy extends Singleton { return req.headers.host + req.url; } - protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) { + protected async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { const cluster = this.getClusterForRequest(req); if (cluster) { @@ -237,7 +193,7 @@ export class LensProxy extends Singleton { // this should be safe because we have already validated cluster uuid res.setHeader("Access-Control-Allow-Origin", "*"); - return proxy.web(req, res, proxyTarget); + return this.proxy.web(req, res, proxyTarget); } } this.router.route(cluster, req, res); diff --git a/src/main/proxy/index.ts b/src/main/proxy-functions/index.ts similarity index 91% rename from src/main/proxy/index.ts rename to src/main/proxy-functions/index.ts index def9c80b5a..2154d81014 100644 --- a/src/main/proxy/index.ts +++ b/src/main/proxy-functions/index.ts @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -// Don't export the contents here -// It will break the extension webpack - -export default {}; +export * from "./shell-api-request"; +export * from "./kube-api-request"; +export * from "./types"; diff --git a/src/main/proxy-functions/kube-api-request.ts b/src/main/proxy-functions/kube-api-request.ts new file mode 100644 index 0000000000..cbd18f9beb --- /dev/null +++ b/src/main/proxy-functions/kube-api-request.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { chunk } from "lodash"; +import net from "net"; +import url from "url"; +import { apiKubePrefix } from "../../common/vars"; +import { ClusterManager } from "../cluster-manager"; +import type { ProxyApiRequestArgs } from "./types"; + +const skipRawHeaders = new Set(["Host", "Authorization"]); + +export async function kubeApiRequest({ req, socket, head }: ProxyApiRequestArgs) { + const cluster = ClusterManager.getInstance().getClusterForRequest(req); + + if (!cluster) { + return; + } + + const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); + const apiUrl = url.parse(cluster.apiUrl); + const pUrl = url.parse(proxyUrl); + const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }; + const proxySocket = new net.Socket(); + + proxySocket.connect(connectOpts, () => { + proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`); + proxySocket.write(`Host: ${apiUrl.host}\r\n`); + + for (const [key, value] of chunk(req.rawHeaders, 2)) { + if (skipRawHeaders.has(key)) { + continue; + } + + proxySocket.write(`${key}: ${value}\r\n`); + } + + proxySocket.write("\r\n"); + proxySocket.write(head); + }); + + proxySocket.setKeepAlive(true); + socket.setKeepAlive(true); + proxySocket.setTimeout(0); + socket.setTimeout(0); + + proxySocket.on("data", function (chunk) { + socket.write(chunk); + }); + proxySocket.on("end", function () { + socket.end(); + }); + proxySocket.on("error", function () { + socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`); + socket.end(); + }); + socket.on("data", function (chunk) { + proxySocket.write(chunk); + }); + socket.on("end", function () { + proxySocket.end(); + }); + socket.on("error", function () { + proxySocket.end(); + }); +} diff --git a/src/main/proxy/ws-upgrade.ts b/src/main/proxy-functions/shell-api-request.ts similarity index 75% rename from src/main/proxy/ws-upgrade.ts rename to src/main/proxy-functions/shell-api-request.ts index 595b05430d..3edaecff81 100644 --- a/src/main/proxy/ws-upgrade.ts +++ b/src/main/proxy-functions/shell-api-request.ts @@ -19,24 +19,18 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/** - * This file is here so that the "../shell-session" import can be injected into - * LensProxy at creation time. So that the `pty.node` extension isn't loaded - * into Lens Extension webpack bundle. - */ - -import * as WebSocket from "ws"; import type http from "http"; -import type net from "net"; import url from "url"; -import { NodeShellSession, LocalShellSession } from "../shell-session"; -import { ClusterManager } from "../cluster-manager"; import logger from "../logger"; +import * as WebSocket from "ws"; +import { NodeShellSession, LocalShellSession } from "../shell-session"; +import type { ProxyApiRequestArgs } from "./types"; +import { ClusterManager } from "../cluster-manager"; -function createWsListener(): WebSocket.Server { +export function shellApiRequest({ req, socket, head }: ProxyApiRequestArgs) { const ws = new WebSocket.Server({ noServer: true }); - return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => { + ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => { const cluster = ClusterManager.getInstance().getClusterForRequest(req); const nodeParam = url.parse(req.url, true).query["node"]?.toString(); const shell = nodeParam @@ -46,12 +40,8 @@ function createWsListener(): WebSocket.Server { shell.open() .catch(error => logger.error(`[SHELL-SESSION]: failed to open: ${error}`, { error })); })); -} -export async function handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) { - const wsServer = createWsListener(); - - wsServer.handleUpgrade(req, socket, head, (con) => { - wsServer.emit("connection", con, req); + ws.handleUpgrade(req, socket, head, (con) => { + ws.emit("connection", con, req); }); } diff --git a/src/main/proxy-functions/types.ts b/src/main/proxy-functions/types.ts new file mode 100644 index 0000000000..57d3db1b7c --- /dev/null +++ b/src/main/proxy-functions/types.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import type http from "http"; +import type net from "net"; + +export interface ProxyApiRequestArgs { + req: http.IncomingMessage, + socket: net.Socket, + head: Buffer, +} diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index d1b0f5da2a..13045bc496 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -25,14 +25,12 @@ import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electro import windowStateKeeper from "electron-window-state"; import { appEventBus } from "../common/event-bus"; import { ipcMainOn } from "../common/ipc"; -import { initMenu } from "./menu"; -import { initTray } from "./tray"; import { delay, iter, Singleton } from "../common/utils"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import logger from "./logger"; import { productName } from "../common/vars"; -import { LensProxy } from "./proxy/lens-proxy"; +import { LensProxy } from "./lens-proxy"; function isHideable(window: BrowserWindow | null): boolean { return Boolean(window && !window.isDestroyed()); @@ -56,8 +54,6 @@ export class WindowManager extends Singleton { super(); makeObservable(this); this.bindEvents(); - this.initMenu(); - this.initTray(); } get mainUrl() { @@ -136,14 +132,6 @@ export class WindowManager extends Singleton { } } - protected async initMenu() { - this.disposers.menuAutoUpdater = initMenu(this); - } - - protected initTray() { - this.disposers.trayAutoUpdater = initTray(this); - } - protected bindEvents() { // track visible cluster from ui ipcMainOn(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {