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