mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Cleanup
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
ed6f5226bd
commit
87dc14cbfb
17
src/features/terminal/common/shell-api-auth-channel.ts
Normal file
17
src/features/terminal/common/shell-api-auth-channel.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RequestChannel } from "../../../common/utils/channel/request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
export interface ShellApiAuthArgs {
|
||||||
|
clusterId: string;
|
||||||
|
tabId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShellApiAuthChannel = RequestChannel<ShellApiAuthArgs, Uint8Array>;
|
||||||
|
|
||||||
|
export const shellApiAuthChannel: ShellApiAuthChannel = {
|
||||||
|
id: "cluster-shell-api-auth",
|
||||||
|
};
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens";
|
||||||
|
import { shellApiAuthChannel } from "../common/shell-api-auth-channel";
|
||||||
|
import shellApiAuthenticatorInjectable from "./shell-api-authenticator.injectable";
|
||||||
|
|
||||||
|
const shellApiAuthRequestChannelListener = getRequestChannelListenerInjectable({
|
||||||
|
channel: shellApiAuthChannel,
|
||||||
|
handler: (di) => {
|
||||||
|
const shellApiAuthenticator = di.inject(shellApiAuthenticatorInjectable);
|
||||||
|
|
||||||
|
return ({ clusterId, tabId }) => shellApiAuthenticator.requestToken(clusterId, tabId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default shellApiAuthRequestChannelListener;
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ClusterId } from "../../../common/cluster-types";
|
||||||
|
import { getOrInsertMap, put } from "../../../common/utils";
|
||||||
|
import type { TabId } from "../../../renderer/components/dock/dock/store";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
const randomBytes = promisify(crypto.randomBytes);
|
||||||
|
|
||||||
|
export interface ShellApiAuthenticator {
|
||||||
|
authenticate: (clusterId: ClusterId, tabId: string, token: string | null) => boolean;
|
||||||
|
requestToken: (clusterId: ClusterId, tabId: TabId) => Promise<Uint8Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shellApiAuthenticatorInjectable = getInjectable({
|
||||||
|
id: "shell-api-authenticator",
|
||||||
|
instantiate: (): ShellApiAuthenticator => {
|
||||||
|
const tokens = new Map<ClusterId, Map<TabId, Uint8Array>>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticate: (clusterId, tabId, token) => {
|
||||||
|
const clusterTokens = tokens.get(clusterId);
|
||||||
|
|
||||||
|
if (!clusterTokens || !tabId || !token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authToken = clusterTokens.get(tabId);
|
||||||
|
const buf = Uint8Array.from(Buffer.from(token, "base64"));
|
||||||
|
|
||||||
|
if (authToken instanceof Uint8Array && authToken.length === buf.length && crypto.timingSafeEqual(authToken, buf)) {
|
||||||
|
// remove the token because it is a single use token
|
||||||
|
clusterTokens.delete(tabId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
requestToken: async (clusterId, tabId) => (
|
||||||
|
put(
|
||||||
|
getOrInsertMap(tokens, clusterId),
|
||||||
|
tabId,
|
||||||
|
Uint8Array.from(await randomBytes(128)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default shellApiAuthenticatorInjectable;
|
||||||
@ -3,38 +3,44 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import shellRequestAuthenticatorInjectable from "./shell-request-authenticator/shell-request-authenticator.injectable";
|
import { URL } from "url";
|
||||||
import openShellSessionInjectable from "../../shell-session/create-shell-session.injectable";
|
import { WebSocketServer } from "ws";
|
||||||
import type { LensProxyApiRequest } from "../lens-proxy";
|
|
||||||
import URLParse from "url-parse";
|
|
||||||
import { Server as WebSocketServer } from "ws";
|
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import shellApiAuthenticatorInjectable from "../../../features/terminal/main/shell-api-authenticator.injectable";
|
||||||
|
import clusterManagerInjectable from "../../cluster/manager.injectable";
|
||||||
|
import openShellSessionInjectable from "../../shell-session/create-shell-session.injectable";
|
||||||
import getClusterForRequestInjectable from "../get-cluster-for-request.injectable";
|
import getClusterForRequestInjectable from "../get-cluster-for-request.injectable";
|
||||||
|
import type { ProxyApiRequestArgs } from "./types";
|
||||||
|
|
||||||
const shellApiRequestInjectable = getInjectable({
|
const shellApiRequestInjectable = getInjectable({
|
||||||
id: "shell-api-request",
|
id: "shell-api-request",
|
||||||
|
|
||||||
instantiate: (di): LensProxyApiRequest => {
|
instantiate: (di) => {
|
||||||
const openShellSession = di.inject(openShellSessionInjectable);
|
const openShellSession = di.inject(openShellSessionInjectable);
|
||||||
const authenticateRequest = di.inject(shellRequestAuthenticatorInjectable).authenticate;
|
const clusterManager = di.inject(clusterManagerInjectable);
|
||||||
const getClusterForRequest = di.inject(getClusterForRequestInjectable);
|
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
const shellApiAuthenticator = di.inject(shellApiAuthenticatorInjectable);
|
||||||
|
const getClusterForRequest = di.inject(getClusterForRequestInjectable);
|
||||||
|
|
||||||
return ({ req, socket, head }) => {
|
return ({ req, socket, head }: ProxyApiRequestArgs): void => {
|
||||||
const cluster = getClusterForRequest(req);
|
const cluster = getClusterForRequest(req);
|
||||||
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
|
const { searchParams } = new URL(req.url);
|
||||||
|
const tabId = searchParams.get("id");
|
||||||
|
const nodeName = searchParams.get("node");
|
||||||
|
const shellToken = searchParams.get("shellToken");
|
||||||
|
|
||||||
if (!tabId || !cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
|
if (!tabId || !cluster || !shellApiAuthenticator.authenticate(cluster.id, tabId, shellToken)) {
|
||||||
socket.write("Invalid shell request");
|
socket.write("Invalid shell request");
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
const ws = new WebSocketServer({ noServer: true });
|
|
||||||
|
|
||||||
ws.handleUpgrade(req, socket, head, (websocket) => {
|
return void socket.end();
|
||||||
openShellSession({ websocket, cluster, tabId, nodeName })
|
|
||||||
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ws = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
|
ws.handleUpgrade(req, socket, head, (websocket) => {
|
||||||
|
openShellSession({ websocket, cluster, tabId, nodeName })
|
||||||
|
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { ShellRequestAuthenticator } from "./shell-request-authenticator";
|
|
||||||
|
|
||||||
const shellRequestAuthenticatorInjectable = getInjectable({
|
|
||||||
id: "shell-request-authenticator",
|
|
||||||
|
|
||||||
instantiate: () => {
|
|
||||||
const authenticator = new ShellRequestAuthenticator();
|
|
||||||
|
|
||||||
authenticator.init();
|
|
||||||
|
|
||||||
return authenticator;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default shellRequestAuthenticatorInjectable;
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getOrInsertMap } from "../../../../common/utils";
|
|
||||||
import type { ClusterId } from "../../../../common/cluster-types";
|
|
||||||
import { ipcMainHandle } from "../../../../common/ipc";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
const randomBytes = promisify(crypto.randomBytes);
|
|
||||||
|
|
||||||
export class ShellRequestAuthenticator {
|
|
||||||
private tokens = new Map<ClusterId, Map<string, Uint8Array>>();
|
|
||||||
|
|
||||||
init() {
|
|
||||||
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
|
|
||||||
const authToken = Uint8Array.from(await randomBytes(128));
|
|
||||||
const forCluster = getOrInsertMap(this.tokens, clusterId);
|
|
||||||
|
|
||||||
forCluster.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 | undefined): boolean => {
|
|
||||||
const clusterTokens = this.tokens.get(clusterId);
|
|
||||||
|
|
||||||
if (!clusterTokens || !tabId || !token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authToken = clusterTokens.get(tabId);
|
|
||||||
const buf = Uint8Array.from(Buffer.from(token, "base64"));
|
|
||||||
|
|
||||||
if (authToken instanceof Uint8Array && authToken.length === buf.length && crypto.timingSafeEqual(authToken, buf)) {
|
|
||||||
// remove the token because it is a single use token
|
|
||||||
clusterTokens.delete(tabId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -12,7 +12,7 @@ export interface OpenShellSessionArgs {
|
|||||||
websocket: WebSocket;
|
websocket: WebSocket;
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
nodeName?: string;
|
nodeName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OpenShellSession = (args: OpenShellSessionArgs) => Promise<void>;
|
export type OpenShellSession = (args: OpenShellSessionArgs) => Promise<void>;
|
||||||
|
|||||||
22
src/renderer/api/default-websocket-params.injectable.ts
Normal file
22
src/renderer/api/default-websocket-params.injectable.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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 { TerminalMessage } from "../../common/terminal/channels";
|
||||||
|
import { TerminalChannels } from "../../common/terminal/channels";
|
||||||
|
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
||||||
|
|
||||||
|
export type DefaultWebsocketApiParams = ReturnType<(typeof defaultWebsocketApiParamsInjectable)["instantiate"]>;
|
||||||
|
|
||||||
|
const defaultWebsocketApiParamsInjectable = getInjectable({
|
||||||
|
id: "default-websocket-api-params",
|
||||||
|
instantiate: (di) => ({
|
||||||
|
logging: di.inject(isDevelopmentInjectable),
|
||||||
|
reconnectDelay: 10,
|
||||||
|
flushOnOpen: true,
|
||||||
|
pingMessage: JSON.stringify({ type: TerminalChannels.PING } as TerminalMessage),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defaultWebsocketApiParamsInjectable;
|
||||||
@ -6,9 +6,8 @@
|
|||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEventEmitter from "typed-emitter";
|
import type TypedEventEmitter from "typed-emitter";
|
||||||
import type { Arguments } from "typed-emitter";
|
|
||||||
import type { Defaulted } from "../utils";
|
import type { Defaulted } from "../utils";
|
||||||
import type { DefaultWebsocketApiParams } from "./default-websocket-api-params.injectable";
|
import type { DefaultWebsocketApiParams } from "./default-websocket-params.injectable";
|
||||||
|
|
||||||
interface WebsocketApiParams {
|
interface WebsocketApiParams {
|
||||||
/**
|
/**
|
||||||
@ -72,7 +71,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
|||||||
protected pendingCommands: string[] = [];
|
protected pendingCommands: string[] = [];
|
||||||
protected reconnectTimer?: number;
|
protected reconnectTimer?: number;
|
||||||
protected pingTimer?: number;
|
protected pingTimer?: number;
|
||||||
protected params: Defaulted<WebsocketApiParams, keyof DefaultWebsocketApiParams>;
|
protected readonly params: Defaulted<WebsocketApiParams, keyof DefaultWebsocketApiParams>;
|
||||||
|
|
||||||
@observable readyState = WebSocketApiState.PENDING;
|
@observable readyState = WebSocketApiState.PENDING;
|
||||||
|
|
||||||
@ -157,14 +156,14 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _onOpen(evt: Event) {
|
protected _onOpen(evt: Event) {
|
||||||
this.emit("open", ...[] as Arguments<Events["open"]>);
|
(this as TypedEventEmitter<WebSocketEvents>).emit("open");
|
||||||
if (this.params.flushOnOpen) this.flush();
|
if (this.params.flushOnOpen) this.flush();
|
||||||
this.readyState = WebSocketApiState.OPEN;
|
this.readyState = WebSocketApiState.OPEN;
|
||||||
this.writeLog("%cOPEN", "color:green;font-weight:bold;", evt);
|
this.writeLog("%cOPEN", "color:green;font-weight:bold;", evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _onMessage({ data }: MessageEvent): void {
|
protected _onMessage({ data }: MessageEvent): void {
|
||||||
this.emit("data", ...[data] as Arguments<Events["data"]>);
|
(this as TypedEventEmitter<WebSocketEvents>).emit("data", data);
|
||||||
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
|
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +187,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.readyState = WebSocketApiState.CLOSED;
|
this.readyState = WebSocketApiState.CLOSED;
|
||||||
this.emit("close", ...[] as Arguments<Events["close"]>);
|
(this as TypedEventEmitter<WebSocketEvents>).emit("close");
|
||||||
}
|
}
|
||||||
this.writeLog("%cCLOSE", `color:${error ? "red" : "black"};font-weight:bold;`, evt);
|
this.writeLog("%cCLOSE", `color:${error ? "red" : "black"};font-weight:bold;`, evt);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user