mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce initial authorization attempt for SHELLs and KUBECTL
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
2e6ced456d
commit
7708e662e1
@ -6,4 +6,4 @@
|
|||||||
/**
|
/**
|
||||||
* This is the header name that we use for request authentication
|
* This is the header name that we use for request authentication
|
||||||
*/
|
*/
|
||||||
export const lensAuthenticationHeader = "LENS-AUTHENTICATION";
|
export const lensAuthenticationHeader = "Authorization";
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* 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 { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||||
@ -30,6 +30,7 @@ import removePathInjectable from "../../common/fs/remove.injectable";
|
|||||||
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable";
|
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable";
|
||||||
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable";
|
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable";
|
||||||
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
|
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
|
||||||
|
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
|
||||||
|
|
||||||
const clusterServerUrl = "https://192.168.64.3:8443";
|
const clusterServerUrl = "https://192.168.64.3:8443";
|
||||||
|
|
||||||
@ -90,6 +91,8 @@ describe("kubeconfig manager tests", () => {
|
|||||||
ensureServer: ensureServerMock,
|
ensureServer: ensureServerMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
di.inject(lensProxyPortInjectable).set(9191);
|
||||||
|
|
||||||
const createCluster = di.inject(createClusterInjectionToken);
|
const createCluster = di.inject(createClusterInjectionToken);
|
||||||
|
|
||||||
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
||||||
@ -102,8 +105,6 @@ describe("kubeconfig manager tests", () => {
|
|||||||
clusterServerUrl,
|
clusterServerUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
|
|
||||||
|
|
||||||
kubeConfManager = createKubeconfigManager(clusterFake);
|
kubeConfManager = createKubeconfigManager(clusterFake);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable
|
|||||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||||
import writeFileInjectable from "../../common/fs/write-file.injectable";
|
import writeFileInjectable from "../../common/fs/write-file.injectable";
|
||||||
import removePathInjectable from "../../common/fs/remove.injectable";
|
import removePathInjectable from "../../common/fs/remove.injectable";
|
||||||
|
import authHeaderValueInjectable from "../lens-proxy/auth-header-value.injectable";
|
||||||
|
|
||||||
export interface KubeConfigManagerInstantiationParameter {
|
export interface KubeConfigManagerInstantiationParameter {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -29,6 +30,7 @@ const createKubeconfigManagerInjectable = getInjectable({
|
|||||||
directoryForTemp: di.inject(directoryForTempInjectable),
|
directoryForTemp: di.inject(directoryForTempInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
lensProxyPort: di.inject(lensProxyPortInjectable),
|
lensProxyPort: di.inject(lensProxyPortInjectable),
|
||||||
|
authHeaderValue: di.inject(authHeaderValueInjectable),
|
||||||
joinPaths: di.inject(joinPathsInjectable),
|
joinPaths: di.inject(joinPathsInjectable),
|
||||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||||
removePath: di.inject(removePathInjectable),
|
removePath: di.inject(removePathInjectable),
|
||||||
|
|||||||
@ -15,11 +15,13 @@ import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"
|
|||||||
import type { PathExists } from "../../common/fs/path-exists.injectable";
|
import type { PathExists } from "../../common/fs/path-exists.injectable";
|
||||||
import type { RemovePath } from "../../common/fs/remove.injectable";
|
import type { RemovePath } from "../../common/fs/remove.injectable";
|
||||||
import type { WriteFile } from "../../common/fs/write-file.injectable";
|
import type { WriteFile } from "../../common/fs/write-file.injectable";
|
||||||
|
import { lensAuthenticationHeader } from "../../common/vars/auth-header";
|
||||||
|
|
||||||
export interface KubeconfigManagerDependencies {
|
export interface KubeconfigManagerDependencies {
|
||||||
readonly directoryForTemp: string;
|
readonly directoryForTemp: string;
|
||||||
readonly logger: Logger;
|
readonly logger: Logger;
|
||||||
readonly lensProxyPort: { get: () => number };
|
readonly lensProxyPort: { get: () => number };
|
||||||
|
readonly authHeaderValue: string;
|
||||||
joinPaths: JoinPaths;
|
joinPaths: JoinPaths;
|
||||||
getDirnameOfPath: GetDirnameOfPath;
|
getDirnameOfPath: GetDirnameOfPath;
|
||||||
pathExists: PathExists;
|
pathExists: PathExists;
|
||||||
@ -47,7 +49,7 @@ export class KubeconfigManager {
|
|||||||
* @returns The path to the temporary kubeconfig
|
* @returns The path to the temporary kubeconfig
|
||||||
*/
|
*/
|
||||||
async getPath(): Promise<string> {
|
async getPath(): Promise<string> {
|
||||||
if (this.tempFilePath === null || !(await this.dependencies.pathExists(this.tempFilePath))) {
|
if (this.tempFilePath === null) {
|
||||||
return await this.ensureFile();
|
return await this.ensureFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,10 +87,6 @@ export class KubeconfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get resolveProxyUrl() {
|
|
||||||
return `http://127.0.0.1:${this.dependencies.lensProxyPort.get()}/${this.cluster.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
||||||
* This way any user of the config does not need to know anything about the auth etc. details.
|
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||||
@ -101,16 +99,23 @@ export class KubeconfigManager {
|
|||||||
`kubeconfig-${id}`,
|
`kubeconfig-${id}`,
|
||||||
);
|
);
|
||||||
const kubeConfig = await cluster.getKubeconfig();
|
const kubeConfig = await cluster.getKubeconfig();
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
[lensAuthenticationHeader]: this.dependencies.authHeaderValue,
|
||||||
|
});
|
||||||
|
|
||||||
const proxyConfig: PartialDeep<KubeConfig> = {
|
const proxyConfig: PartialDeep<KubeConfig> = {
|
||||||
currentContext: contextName,
|
currentContext: contextName,
|
||||||
clusters: [
|
clusters: [
|
||||||
{
|
{
|
||||||
name: contextName,
|
name: contextName,
|
||||||
server: this.resolveProxyUrl,
|
server: `http://127.0.0.1:${this.dependencies.lensProxyPort.get()}/${this.cluster.id}?${searchParams}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
users: [
|
users: [
|
||||||
{ name: "proxy" },
|
{
|
||||||
|
name: "proxy",
|
||||||
|
token: this.dependencies.authHeaderValue,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
contexts: [
|
contexts: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { lensAuthenticationHeaderValueInjectionToken } from "../../common/auth/h
|
|||||||
|
|
||||||
const authHeaderValueInjectable = getInjectable({
|
const authHeaderValueInjectable = getInjectable({
|
||||||
id: "auth-header-value",
|
id: "auth-header-value",
|
||||||
instantiate: () => uuid.v4(),
|
instantiate: () => `Bearer ${uuid.v4()}`,
|
||||||
injectionToken: lensAuthenticationHeaderValueInjectionToken,
|
injectionToken: lensAuthenticationHeaderValueInjectionToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import type { RouteRequest } from "../router/route-request.injectable";
|
|||||||
import { lensAuthenticationHeader } from "../../common/vars/auth-header";
|
import { lensAuthenticationHeader } from "../../common/vars/auth-header";
|
||||||
import { contentTypes } from "../router/router-content-types";
|
import { contentTypes } from "../router/router-content-types";
|
||||||
import { writeServerResponseFor } from "../router/write-server-response";
|
import { writeServerResponseFor } from "../router/write-server-response";
|
||||||
|
import { URL } from "url";
|
||||||
|
|
||||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
||||||
|
|
||||||
@ -78,10 +79,10 @@ export class LensProxy {
|
|||||||
|
|
||||||
this.proxyServer
|
this.proxyServer
|
||||||
.on("upgrade", (req: ServerIncomingMessage, socket: net.Socket, head: Buffer) => {
|
.on("upgrade", (req: ServerIncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||||
const cluster = dependencies.getClusterForRequest(req);
|
const cluster = this.dependencies.getClusterForRequest(req);
|
||||||
const authHeader = req.headers[lensAuthenticationHeader.toLowerCase()];
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
|
||||||
if (authHeader !== this.dependencies.authHeaderValue) {
|
if (url.searchParams.get(lensAuthenticationHeader) !== this.dependencies.authHeaderValue) {
|
||||||
this.dependencies.logger.warn(`[LENS-PROXY]: Request from url=${req.url} missing authentication`);
|
this.dependencies.logger.warn(`[LENS-PROXY]: Request from url=${req.url} missing authentication`);
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
|
||||||
@ -92,11 +93,21 @@ export class LensProxy {
|
|||||||
this.dependencies.logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
this.dependencies.logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
} else {
|
} else {
|
||||||
const isInternal = req.url.startsWith(`${apiPrefix}?`);
|
void (async () => {
|
||||||
const reqHandler = isInternal ? dependencies.shellApiRequest : dependencies.kubeApiUpgradeRequest;
|
try {
|
||||||
|
if (url.pathname === apiPrefix) {
|
||||||
|
await dependencies.shellApiRequest({ req, socket, head, cluster });
|
||||||
|
} else if (url.pathname.startsWith(`${apiKubePrefix}/`)) {
|
||||||
|
req.url = req.url.slice(apiKubePrefix.length);
|
||||||
|
|
||||||
(async () => reqHandler({ req, socket, head, cluster }))()
|
await dependencies.kubeApiUpgradeRequest({ req, socket, head, cluster });
|
||||||
.catch(error => this.dependencies.logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
|
} else {
|
||||||
|
this.dependencies.logger.warn(`[LENS-PROXY]: unknown upgrade request, url=${req.url}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.dependencies.logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -224,7 +235,6 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise<httpProxy.ServerOptions | undefined> {
|
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise<httpProxy.ServerOptions | undefined> {
|
||||||
if (req.url?.startsWith(apiKubePrefix)) {
|
if (req.url?.startsWith(apiKubePrefix)) {
|
||||||
delete req.headers.authorization;
|
|
||||||
req.url = req.url.replace(apiKubePrefix, "");
|
req.url = req.url.replace(apiKubePrefix, "");
|
||||||
|
|
||||||
return contextHandler.getApiTarget(isLongRunningRequest(req.url));
|
return contextHandler.getApiTarget(isLongRunningRequest(req.url));
|
||||||
|
|||||||
@ -7,13 +7,12 @@ import { chunk } from "lodash";
|
|||||||
import type { ConnectionOptions } from "tls";
|
import type { ConnectionOptions } from "tls";
|
||||||
import { connect } from "tls";
|
import { connect } from "tls";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import { apiKubePrefix } from "../../../common/vars";
|
|
||||||
import type { ProxyApiRequestArgs } from "./types";
|
import type { ProxyApiRequestArgs } from "./types";
|
||||||
|
|
||||||
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
||||||
|
|
||||||
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url;
|
||||||
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
|
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
|
||||||
const apiUrl = url.parse(cluster.apiUrl);
|
const apiUrl = url.parse(cluster.apiUrl);
|
||||||
const pUrl = url.parse(proxyUrl);
|
const pUrl = url.parse(proxyUrl);
|
||||||
|
|||||||
@ -16,7 +16,15 @@ describe("WebsocketApi tests", () => {
|
|||||||
let api: TestWebSocketApi;
|
let api: TestWebSocketApi;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
api = new TestWebSocketApi({});
|
api = new TestWebSocketApi({
|
||||||
|
authHeaderValue: "some-value",
|
||||||
|
defaultParams: {
|
||||||
|
flushOnOpen: true,
|
||||||
|
logging: false,
|
||||||
|
pingMessage: "{}",
|
||||||
|
reconnectDelay: 10,
|
||||||
|
},
|
||||||
|
}, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("before connection", () => {
|
describe("before connection", () => {
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
import authHeaderValueInjectable from "../auth/auth-header.injectable";
|
||||||
import hostedClusterIdInjectable from "../cluster-frame-context/hosted-cluster-id.injectable";
|
import hostedClusterIdInjectable from "../cluster-frame-context/hosted-cluster-id.injectable";
|
||||||
|
import defaultWebsocketApiParamsInjectable from "./default-websocket-params.injectable";
|
||||||
import type { TerminalApiQuery } from "./terminal-api";
|
import type { TerminalApiQuery } from "./terminal-api";
|
||||||
import { TerminalApi } from "./terminal-api";
|
import { TerminalApi } from "./terminal-api";
|
||||||
|
|
||||||
@ -14,12 +16,16 @@ const createTerminalApiInjectable = getInjectable({
|
|||||||
id: "create-terminal-api",
|
id: "create-terminal-api",
|
||||||
instantiate: (di): CreateTerminalApi => {
|
instantiate: (di): CreateTerminalApi => {
|
||||||
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
||||||
|
const authHeaderValue = di.inject(authHeaderValueInjectable);
|
||||||
|
const defaultParams = di.inject(defaultWebsocketApiParamsInjectable);
|
||||||
|
|
||||||
return (query) => {
|
return (query) => {
|
||||||
assert(hostedClusterId, "Can only create terminal APIs within a cluster frame");
|
assert(hostedClusterId, "Can only create terminal APIs within a cluster frame");
|
||||||
|
|
||||||
return new TerminalApi({
|
return new TerminalApi({
|
||||||
hostedClusterId,
|
hostedClusterId,
|
||||||
|
authHeaderValue,
|
||||||
|
defaultParams,
|
||||||
}, query);
|
}, query);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
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";
|
||||||
|
import type { DefaultWebsocketApiParams } from "./websocket-api";
|
||||||
|
|
||||||
|
|
||||||
|
const defaultWebsocketApiParamsInjectable = getInjectable({
|
||||||
|
id: "default-websocket-api-params",
|
||||||
|
instantiate: (di): DefaultWebsocketApiParams => ({
|
||||||
|
logging: di.inject(isDevelopmentInjectable),
|
||||||
|
reconnectDelay: 10,
|
||||||
|
flushOnOpen: true,
|
||||||
|
pingMessage: JSON.stringify({ type: TerminalChannels.PING } as TerminalMessage),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defaultWebsocketApiParamsInjectable;
|
||||||
@ -3,15 +3,16 @@
|
|||||||
* 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 type { WebSocketEvents } from "./websocket-api";
|
import type { WebSocketApiDependencies, WebSocketEvents } from "./websocket-api";
|
||||||
import { WebSocketApi } from "./websocket-api";
|
import { WebSocketApi } from "./websocket-api";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import url from "url";
|
import { URLSearchParams } from "url";
|
||||||
import { makeObservable, observable } from "mobx";
|
import { makeObservable, observable } from "mobx";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import logger from "../../common/logger";
|
import logger from "../../common/logger";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
|
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
|
||||||
|
import { object } from "../utils";
|
||||||
|
|
||||||
enum TerminalColor {
|
enum TerminalColor {
|
||||||
RED = "\u001b[31m",
|
RED = "\u001b[31m",
|
||||||
@ -25,7 +26,7 @@ enum TerminalColor {
|
|||||||
NO_COLOR = "\u001b[0m",
|
NO_COLOR = "\u001b[0m",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminalApiQuery extends Record<string, string | undefined> {
|
export interface TerminalApiQuery extends Partial<Record<string, string>> {
|
||||||
id: string;
|
id: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -36,7 +37,7 @@ export interface TerminalEvents extends WebSocketEvents {
|
|||||||
connected: () => void;
|
connected: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminalApiDependencies {
|
export interface TerminalApiDependencies extends WebSocketApiDependencies {
|
||||||
readonly hostedClusterId: string;
|
readonly hostedClusterId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
|||||||
@observable public isReady = false;
|
@observable public isReady = false;
|
||||||
|
|
||||||
constructor(protected readonly dependencies: TerminalApiDependencies, protected readonly query: TerminalApiQuery) {
|
constructor(protected readonly dependencies: TerminalApiDependencies, protected readonly query: TerminalApiQuery) {
|
||||||
super({
|
super(dependencies, {
|
||||||
flushOnOpen: false,
|
flushOnOpen: false,
|
||||||
pingInterval: 30,
|
pingInterval: 30,
|
||||||
});
|
});
|
||||||
@ -73,17 +74,12 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { hostname, protocol, port } = location;
|
const { hostname, protocol, port } = location;
|
||||||
const socketUrl = url.format({
|
const wsProtocol = protocol.includes("https") ? "wss" : "ws";
|
||||||
protocol: protocol.includes("https") ? "wss" : "ws",
|
const searchParams = new URLSearchParams([
|
||||||
hostname,
|
...object.entries(this.query),
|
||||||
port,
|
["shellToken", Buffer.from(authTokenArray).toString("base64")],
|
||||||
pathname: "/api",
|
]);
|
||||||
query: {
|
const socketUrl = `${wsProtocol}://${hostname}:${port}/api?${searchParams}`;
|
||||||
...this.query,
|
|
||||||
shellToken: Buffer.from(authTokenArray).toString("base64"),
|
|
||||||
},
|
|
||||||
slashes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onReady = once((data?: string) => {
|
const onReady = once((data?: string) => {
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
@ -128,9 +124,9 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _onMessage({ data, ...evt }: MessageEvent<string>): void {
|
protected _onMessage({ data, ...evt }: MessageEvent): void {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(data) as TerminalMessage;
|
const message = JSON.parse(data as string) as TerminalMessage;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case TerminalChannels.STDOUT:
|
case TerminalChannels.STDOUT:
|
||||||
|
|||||||
@ -7,11 +7,10 @@ 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 { Arguments } from "typed-emitter";
|
||||||
import { isDevelopment } from "../../common/vars";
|
|
||||||
import type { Defaulted } from "../utils";
|
import type { Defaulted } from "../utils";
|
||||||
import { TerminalChannels, type TerminalMessage } from "../../common/terminal/channels";
|
import { lensAuthenticationHeader } from "../../common/vars/auth-header";
|
||||||
|
|
||||||
interface WebsocketApiParams {
|
export interface WebsocketApiParams {
|
||||||
/**
|
/**
|
||||||
* Flush pending commands on open socket
|
* Flush pending commands on open socket
|
||||||
*
|
*
|
||||||
@ -64,28 +63,32 @@ export interface WebSocketEvents {
|
|||||||
close: () => void;
|
close: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebSocketApiDependencies {
|
||||||
|
readonly authHeaderValue: string;
|
||||||
|
readonly defaultParams: DefaultWebsocketApiParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefaultWebsocketApiParamNames = "logging" | "reconnectDelay" | "flushOnOpen" | "pingMessage";
|
||||||
|
export type DefaultWebsocketApiParams = Pick<Defaulted<WebsocketApiParams, DefaultWebsocketApiParamNames>, DefaultWebsocketApiParamNames>;
|
||||||
|
|
||||||
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
|
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
|
||||||
protected socket: WebSocket | null = null;
|
protected socket: WebSocket | null = null;
|
||||||
protected pendingCommands: string[] = [];
|
protected readonly pendingCommands: string[] = [];
|
||||||
protected reconnectTimer?: number;
|
protected reconnectTimer?: number;
|
||||||
protected pingTimer?: number;
|
protected pingTimer?: number;
|
||||||
protected params: Defaulted<WebsocketApiParams, keyof typeof WebSocketApi["defaultParams"]>;
|
protected readonly params: Defaulted<WebsocketApiParams, "logging" | "reconnectDelay" | "flushOnOpen" | "pingMessage">;
|
||||||
|
|
||||||
@observable readyState = WebSocketApiState.PENDING;
|
@observable readyState = WebSocketApiState.PENDING;
|
||||||
|
|
||||||
private static readonly defaultParams = {
|
constructor(
|
||||||
logging: isDevelopment,
|
protected readonly dependencies: WebSocketApiDependencies,
|
||||||
reconnectDelay: 10,
|
rawParams: WebsocketApiParams,
|
||||||
flushOnOpen: true,
|
) {
|
||||||
pingMessage: JSON.stringify({ type: TerminalChannels.PING } as TerminalMessage),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(params: WebsocketApiParams) {
|
|
||||||
super();
|
super();
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.params = {
|
this.params = {
|
||||||
...WebSocketApi.defaultParams,
|
...this.dependencies.defaultParams,
|
||||||
...params,
|
...rawParams,
|
||||||
};
|
};
|
||||||
const { pingInterval } = this.params;
|
const { pingInterval } = this.params;
|
||||||
|
|
||||||
@ -102,6 +105,11 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
|||||||
// close previous connection first
|
// close previous connection first
|
||||||
this.socket?.close();
|
this.socket?.close();
|
||||||
|
|
||||||
|
const authParam = new URLSearchParams({ [lensAuthenticationHeader]: this.dependencies.authHeaderValue });
|
||||||
|
const addingParam = url.includes("?") ? "&" : "?";
|
||||||
|
|
||||||
|
url += `${addingParam}${authParam}`;
|
||||||
|
|
||||||
// start new connection
|
// start new connection
|
||||||
this.socket = new WebSocket(url);
|
this.socket = new WebSocket(url);
|
||||||
this.socket.addEventListener("open", ev => this._onOpen(ev));
|
this.socket.addEventListener("open", ev => this._onOpen(ev));
|
||||||
@ -129,7 +137,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
|||||||
if (!this.socket) return;
|
if (!this.socket) return;
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.pendingCommands = [];
|
this.pendingCommands.length = 0;
|
||||||
this.clearAllListeners();
|
this.clearAllListeners();
|
||||||
clearTimeout(this.reconnectTimer);
|
clearTimeout(this.reconnectTimer);
|
||||||
clearInterval(this.pingTimer);
|
clearInterval(this.pingTimer);
|
||||||
@ -153,7 +161,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
|||||||
protected flush() {
|
protected flush() {
|
||||||
const commands = this.pendingCommands;
|
const commands = this.pendingCommands;
|
||||||
|
|
||||||
this.pendingCommands = [];
|
this.pendingCommands.length = 0;
|
||||||
|
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
this.send(command);
|
this.send(command);
|
||||||
@ -168,7 +176,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _onMessage({ data }: MessageEvent): void {
|
protected _onMessage({ data }: MessageEvent): void {
|
||||||
this.emit("data", ...[data] as Arguments<Events["data"]>);
|
this.emit("data", ...[data] as string[] as Arguments<Events["data"]>);
|
||||||
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
|
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user