From 7d430b57ad6e9e2628c83b802785a50798eb0aa3 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 8 Nov 2021 15:05:03 -0500 Subject: [PATCH] Add IPC based authentication tokens Signed-off-by: Sebastian Malton --- src/main/index.ts | 5 +- src/main/proxy-functions/shell-api-request.ts | 66 +++++++++++++++---- src/renderer/api/terminal-api.ts | 22 ++++--- .../components/dock/terminal.store.ts | 4 +- 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 439d2e6471..fc443c3896 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -63,7 +63,7 @@ import { ensureDir } from "fs-extra"; import { Router } from "./router"; import { initMenu } from "./menu"; import { initTray } from "./tray"; -import { kubeApiRequest, shellApiRequest } from "./proxy-functions"; +import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions"; import { AppPaths } from "../common/app-paths"; injectSystemCAs(); @@ -122,7 +122,7 @@ app.on("second-instance", (event, argv) => { WindowManager.getInstance(false)?.ensureMainWindow(); }); -app.on("ready", async () => { +app.on("ready", async () => { logger.info(`🚀 Starting ${productName} from "${AppPaths.get("exe")}"`); logger.info("🐚 Syncing shell environment"); await shellSync(); @@ -134,6 +134,7 @@ app.on("ready", async () => { registerFileProtocol("static", __static); PrometheusProviderRegistry.createInstance(); + ShellRequestAuthenticator.createInstance().init(); initializers.initPrometheusProviderRegistry(); /** diff --git a/src/main/proxy-functions/shell-api-request.ts b/src/main/proxy-functions/shell-api-request.ts index 4ad75cd94e..db88336a2a 100644 --- a/src/main/proxy-functions/shell-api-request.ts +++ b/src/main/proxy-functions/shell-api-request.ts @@ -19,24 +19,65 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import url from "url"; import logger from "../logger"; import { Server as WebSocketServer } from "ws"; import { NodeShellSession, LocalShellSession } from "../shell-session"; import type { ProxyApiRequestArgs } from "./types"; import { ClusterManager } from "../cluster-manager"; -import { appName, appSemVer } from "../../common/vars"; -import { LensProxy } from "../lens-proxy"; +import URLParse from "url-parse"; +import { ExtendedMap, Singleton } from "../../common/utils"; +import type { ClusterId } from "../../common/cluster-types"; +import { ipcMainHandle } from "../../common/ipc"; +import * as uuid from "uuid"; + +export class ShellRequestAuthenticator extends Singleton { + private tokens = new ExtendedMap>(); + + init() { + ipcMainHandle("cluster:shell-api", (event, clusterId, tabId) => { + const authToken = uuid.v4(); + + this.tokens + .getOrInsert(clusterId, () => new Map()) + .set(tabId, authToken); + + return authToken; + }); + } + + /** + * Authenticates a single use token for creating a new shell + * @param clusterId The `ClusterId` for the shell + * @param tabId The ID for the shell + * @param token The value that is being presented as a one time authentication token + * @returns `true` if `token` was valid, false otherwise + */ + authenticate(clusterId: ClusterId, tabId: string, token: string): boolean { + const clusterTokens = this.tokens.get(clusterId); + + if (!clusterTokens) { + return false; + } + + const authToken = clusterTokens.get(tabId); + + // need both conditions to prevent `undefined === undefined` being true here + if (typeof authToken === "string" && authToken === token) { + // remove the token because it is a single use token + clusterTokens.delete(tabId); + + return true; + } + + return false; + } +} export function shellApiRequest({ req, socket, head }: ProxyApiRequestArgs): void { const cluster = ClusterManager.getInstance().getClusterForRequest(req); + const { query: { node, shellToken, id: tabId }} = new URLParse(req.url, true); - if ( - socket.remoteAddress !== "127.0.0.1" - || !req.headers["user-agent"]?.includes(` ${appName}/${appSemVer} `) - || !req.headers.origin?.endsWith(`.localhost:${LensProxy.getInstance().port}`) - || !cluster - ) { + if (!cluster || !ShellRequestAuthenticator.getInstance().authenticate(cluster.id, tabId, shellToken)) { socket.write("Invalid shell request"); return void socket.end(); @@ -45,12 +86,11 @@ export function shellApiRequest({ req, socket, head }: ProxyApiRequestArgs): voi const ws = new WebSocketServer({ noServer: true }); ws.handleUpgrade(req, socket, head, (webSocket) => { - const nodeParam = url.parse(req.url, true).query["node"]?.toString(); - const shell = nodeParam - ? new NodeShellSession(webSocket, cluster, nodeParam) + const shell = node + ? new NodeShellSession(webSocket, cluster, node) : new LocalShellSession(webSocket, cluster); shell.open() - .catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeParam ? "node" : "local"} shell`, error)); + .catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${node ? "node" : "local"} shell`, error)); }); } diff --git a/src/renderer/api/terminal-api.ts b/src/renderer/api/terminal-api.ts index 1a66b25f46..f2c79cbb88 100644 --- a/src/renderer/api/terminal-api.ts +++ b/src/renderer/api/terminal-api.ts @@ -19,13 +19,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { boundMethod, base64, EventEmitter } from "../utils"; +import { boundMethod, base64, EventEmitter, getHostedClusterId } from "../utils"; import { WebSocketApi } from "./websocket-api"; import isEqual from "lodash/isEqual"; import { isDevelopment } from "../../common/vars"; import url from "url"; import { makeObservable, observable } from "mobx"; -import type { ParsedUrlQueryInput } from "querystring"; +import { ipcRenderer } from "electron"; export enum TerminalChannels { STDIN = 0, @@ -50,7 +50,7 @@ enum TerminalColor { export type TerminalApiQuery = Record & { id: string; node?: string; - type?: string | "node"; + type?: string; }; export class TerminalApi extends WebSocketApi { @@ -60,7 +60,13 @@ export class TerminalApi extends WebSocketApi { @observable public isReady = false; public readonly url: string; - constructor(protected options: TerminalApiQuery) { + public static async new(options: TerminalApiQuery): Promise { + const shellToken = await ipcRenderer.invoke("cluster:shell-api", getHostedClusterId(), options.id); + + return new TerminalApi({ ...options, shellToken }); + } + + private constructor(query: TerminalApiQuery) { super({ logging: isDevelopment, flushOnOpen: false, @@ -69,13 +75,9 @@ export class TerminalApi extends WebSocketApi { makeObservable(this); const { hostname, protocol, port } = location; - const query: ParsedUrlQueryInput = { - id: options.id, - }; - if (options.node) { - query.node = options.node; - query.type = options.type || "node"; + if (query.node) { + query.type ||= "node"; } this.url = url.format({ diff --git a/src/renderer/components/dock/terminal.store.ts b/src/renderer/components/dock/terminal.store.ts index 0f4ec7e8ba..c5d3182a8e 100644 --- a/src/renderer/components/dock/terminal.store.ts +++ b/src/renderer/components/dock/terminal.store.ts @@ -65,12 +65,12 @@ export class TerminalStore extends Singleton { }); } - connect(tabId: TabId) { + async connect(tabId: TabId) { if (this.isConnected(tabId)) { return; } const tab: ITerminalTab = dockStore.getTabById(tabId); - const api = new TerminalApi({ + const api = await TerminalApi.new({ id: tabId, node: tab.node, });