Path: blob/main/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts
2501 views
/**1* Copyright (c) 2023 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import * as crypto from "crypto";7import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/frontend-dashboard-service";8import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";9import { Emitter } from "@gitpod/gitpod-protocol/lib/util/event";10import { workspaceUrl, serverUrl } from "./urls";11import { metricsReporter } from "../ide/ide-metrics-service-client";1213export class FrontendDashboardServiceClient implements IDEFrontendDashboardService.IClient {14public latestInfo!: IDEFrontendDashboardService.Info;15private credentialsToken?: Uint8Array;1617private readonly onDidChangeEmitter = new Emitter<IDEFrontendDashboardService.Info>();18readonly onInfoUpdate = this.onDidChangeEmitter.event;1920private readonly onOpenBrowserIDEEmitter = new Emitter<void>();21readonly onOpenBrowserIDE = this.onOpenBrowserIDEEmitter.event;2223private readonly onWillRedirectEmitter = new Emitter<void>();24readonly onWillRedirect = this.onWillRedirectEmitter.event;2526private resolveInit!: () => void;27private initPromise = new Promise<void>((resolve) => (this.resolveInit = resolve));28private featureFlags: Partial<IDEFrontendDashboardService.FeatureFlagsUpdateEventData["flags"]> = {};2930private version?: number;3132constructor(private serverWindow: Window) {33window.addEventListener("message", (event: MessageEvent) => {34if (event.origin !== serverUrl.url.origin) {35return;36}37if (IDEFrontendDashboardService.isInfoUpdateEventData(event.data)) {38metricsReporter.updateCommonErrorDetails({39userId: event.data.info.loggedUserId,40ownerId: event.data.info.ownerId,41workspaceId: event.data.info.workspaceID,42instanceId: event.data.info.instanceId,43instancePhase: event.data.info.statusPhase,44});45this.version = event.data.version;46this.latestInfo = event.data.info;47if (event.data.info.credentialsToken?.length > 0) {48this.credentialsToken = Uint8Array.from(atob(event.data.info.credentialsToken), (c) =>49c.charCodeAt(0),50);51}52this.resolveInit();53this.onDidChangeEmitter.fire(this.latestInfo);54}55if (IDEFrontendDashboardService.isRelocateEventData(event.data)) {56this.onWillRedirectEmitter.fire();57window.location.href = event.data.url;58}59if (IDEFrontendDashboardService.isOpenBrowserIDE(event.data)) {60this.onOpenBrowserIDEEmitter.fire(undefined);61}62if (IDEFrontendDashboardService.isFeatureFlagsUpdateEventData(event.data)) {63this.featureFlags = event.data.flags;64}65});66this.requestFreshFeatureFlags();67}68initialize(): Promise<void> {69return this.initPromise;70}7172decrypt(str: string): string {73if (!this.credentialsToken) {74throw new Error("no credentials token available");75}76const obj = JSON.parse(str);77if (!isSerializedEncryptedData(obj)) {78throw new Error("incorrect encrypted data");79}80const data = {81...obj,82iv: Buffer.from(obj.iv, "base64"),83tag: Buffer.from(obj.tag, "base64"),84};85const decipher = crypto.createDecipheriv("aes-256-gcm", this.credentialsToken, data.iv);86decipher.setAuthTag(data.tag);87const decrypted = decipher.update(data.encrypted, "hex", "utf8");88return decrypted + decipher.final("utf8");89}9091encrypt(content: string): string {92if (!this.credentialsToken) {93throw new Error("no credentials token available");94}95const iv = crypto.randomBytes(12);96const cipher = crypto.createCipheriv("aes-256-gcm", this.credentialsToken, iv);97let encrypted = cipher.update(content, "utf8", "hex");98encrypted += cipher.final("hex");99const tag = cipher.getAuthTag();100return JSON.stringify({101iv: iv.toString("base64"),102tag: tag.toString("base64"),103encrypted,104});105}106107isEncryptedData(content: string): boolean {108try {109const obj = JSON.parse(content);110return isSerializedEncryptedData(obj);111} catch (e) {112return false;113}114}115116trackEvent(msg: RemoteTrackMessage): void {117const debugWorkspace = workspaceUrl.debugWorkspace;118msg.properties = { ...msg.properties, debugWorkspace };119this.serverWindow.postMessage(120{ type: "ide-track-event", msg } as IDEFrontendDashboardService.TrackEventData,121serverUrl.url.origin,122);123}124125activeHeartbeat(): void {126this.serverWindow.postMessage(127{ type: "ide-heartbeat" } as IDEFrontendDashboardService.HeartbeatEventData,128serverUrl.url.origin,129);130}131132setState(state: IDEFrontendDashboardService.SetStateData): void {133this.serverWindow.postMessage(134{ type: "ide-set-state", state } as IDEFrontendDashboardService.SetStateData,135serverUrl.url.origin,136);137}138139// always perfrom redirect to dekstop IDE on gitpod origin140// to avoid confirmation popup on each workspace origin141openDesktopIDE(url: string): void {142this.serverWindow.postMessage(143{ type: "ide-open-desktop", url } as IDEFrontendDashboardService.OpenDesktopIDE,144serverUrl.url.origin,145);146}147148requestFreshFeatureFlags(): void {149window.postMessage(150{ type: "ide-feature-flag-request" } as IDEFrontendDashboardService.FeatureFlagsRequestEventData,151serverUrl.url.origin,152);153}154155isCheckReadyRetryEnabled(): boolean {156return !!this.featureFlags.supervisor_check_ready_retry;157}158}159160function isSerializedEncryptedData(obj: any): obj is { iv: string; encrypted: string; tag: string } {161return (162obj != null &&163typeof obj === "object" &&164typeof obj.iv === "string" &&165typeof obj.encrypted === "string" &&166typeof obj.tag === "string"167);168}169170171