diff --git a/components/dashboard/src/service/service.tsx b/components/dashboard/src/service/service.tsx index ccae3c6c4c58cf..44097cd08accec 100644 --- a/components/dashboard/src/service/service.tsx +++ b/components/dashboard/src/service/service.tsx @@ -66,7 +66,6 @@ export function getIDEFrontendService(workspaceID: string, sessionId: string, se export class IDEFrontendService implements IDEFrontendDashboardService.IServer { private instanceID: string | undefined; - private ideUrl: URL | undefined; private user: User | undefined; private latestStatus?: IDEFrontendDashboardService.Status; @@ -82,10 +81,6 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer { ) { this.processServerInfo(); window.addEventListener("message", (event: MessageEvent) => { - if (event.origin !== this.ideUrl?.origin) { - return; - } - if (IDEFrontendDashboardService.isTrackEventData(event.data)) { this.trackEvent(event.data.msg); } @@ -95,6 +90,9 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer { if (IDEFrontendDashboardService.isSetStateEventData(event.data)) { this.onDidChangeEmitter.fire(event.data.state); } + if (IDEFrontendDashboardService.isOpenDesktopIDE(event.data)) { + this.openDesktopIDE(event.data.url); + } }); window.addEventListener("unload", () => { if (!this.instanceID) { @@ -116,7 +114,6 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer { const reconcile = () => { const status = this.getWorkspaceStatus(listener.info); this.latestStatus = status; - this.ideUrl = status.ideUrl ? new URL(status.ideUrl) : undefined; const oldInstanceID = this.instanceID; this.instanceID = status.instanceId; if (status.instanceId && oldInstanceID !== status.instanceId) { @@ -128,7 +125,7 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer { listener.onDidChange(reconcile); } - getWorkspaceStatus(workspace: WorkspaceInfo): IDEFrontendDashboardService.Status { + private getWorkspaceStatus(workspace: WorkspaceInfo): IDEFrontendDashboardService.Status { return { loggedUserId: this.user!.id, workspaceID: this.workspaceID, @@ -142,7 +139,7 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer { // implements - async auth() { + private async auth() { if (!this.instanceID) { return; } @@ -152,7 +149,7 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer { }); } - trackEvent(msg: RemoteTrackMessage): void { + private trackEvent(msg: RemoteTrackMessage): void { msg.properties = { ...msg.properties, sessionId: this.sessionId, @@ -163,32 +160,48 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer { this.service.server.trackEvent(msg); } - activeHeartbeat(): void { + private activeHeartbeat(): void { if (this.instanceID) { this.service.server.sendHeartBeat({ instanceId: this.instanceID }); } } - sendStatusUpdate(status: IDEFrontendDashboardService.Status): void { - if (!this.ideUrl) { - return; + openDesktopIDE(url: string): void { + let redirect = false; + try { + const desktopLink = new URL(url); + redirect = desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:"; + } catch (e) { + console.error("invalid desktop link:", e); + } + // redirect only if points to desktop application + // don't navigate browser to another page + if (redirect) { + window.location.href = url; + } else { + window.open(url, "_blank", "noopener"); } + } + + sendStatusUpdate(status: IDEFrontendDashboardService.Status): void { this.clientWindow.postMessage( { + version: 1, type: "ide-status-update", status, } as IDEFrontendDashboardService.StatusUpdateEventData, - this.ideUrl.origin, + "*", ); } relocate(url: string): void { - if (!this.ideUrl) { - return; - } this.clientWindow.postMessage( { type: "ide-relocate", url } as IDEFrontendDashboardService.RelocateEventData, - this.ideUrl.origin, + "*", ); } + + openBrowserIDE(): void { + this.clientWindow.postMessage({ type: "ide-open-browser" } as IDEFrontendDashboardService.OpenBrowserIDE, "*"); + } } diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx index 4e4011ba26acf1..21ed6cc54aed73 100644 --- a/components/dashboard/src/start/StartWorkspace.tsx +++ b/components/dashboard/src/start/StartWorkspace.tsx @@ -458,20 +458,7 @@ export default class StartWorkspace extends React.Component window.parent.postMessage({ type: "openBrowserIde" }, "*"), + onClick: () => { + // TODO(ak): delete after supervisor deploy + window.parent.postMessage({ type: "openBrowserIde" }, "*"); + // TODO(ak): end of delete + + this.ideFrontendService?.openBrowserIDE(); + }, }, { title: "Stop Workspace", diff --git a/components/gitpod-protocol/src/frontend-dashboard-service.ts b/components/gitpod-protocol/src/frontend-dashboard-service.ts index 1098431d3dd87d..02bc50de84c8c6 100644 --- a/components/gitpod-protocol/src/frontend-dashboard-service.ts +++ b/components/gitpod-protocol/src/frontend-dashboard-service.ts @@ -6,37 +6,34 @@ import { WorkspaceInstancePhase } from "./workspace-instance"; import { RemoteTrackMessage } from "./analytics"; -import { Event } from "./util/event"; +/** + * IDEFrontendDashboardService enables IDE related communications + * between the workspace to gitpod origins. Use it to communicate + * between IDE and dashboard iframes, never use + * `window.postMessage` directly. + * + * **Security Note**: never expose information about other workspaces + * or sensitive information for the current workspace, i.e. owner token. + */ export namespace IDEFrontendDashboardService { /** * IClient is the client side which is using in supervisor frontend */ - export interface IClient extends IClientOn, IClientSend {} - interface IClientOn { - onStatusUpdate: Event; - relocate(url: string): void; - } - interface IClientSend { + export interface IClient { trackEvent(msg: RemoteTrackMessage): void; activeHeartbeat(): void; setState(state: SetStateData): void; + openDesktopIDE(url: string): void; } /** * IServer is the server side which is using in dashboard loading screen */ - export interface IServer extends IServerOn, IServerSend { - auth(): Promise; - } - interface IServerOn { - onSetState: Event; - trackEvent(msg: RemoteTrackMessage): void; - activeHeartbeat(): void; - } - interface IServerSend { + export interface IServer { sendStatusUpdate(status: Status): void; relocate(url: string): void; + openBrowserIDE(): void; } export interface Status { @@ -64,6 +61,8 @@ export namespace IDEFrontendDashboardService { * interface for post message that send status update from dashboard to supervisor */ export interface StatusUpdateEventData { + // protocol version + version?: number; type: "ide-status-update"; status: Status; } @@ -87,6 +86,15 @@ export namespace IDEFrontendDashboardService { state: SetStateData; } + export interface OpenBrowserIDE { + type: "ide-open-browser"; + } + + export interface OpenDesktopIDE { + type: "ide-open-desktop"; + url: string; + } + export function isStatusUpdateEventData(obj: any): obj is StatusUpdateEventData { return obj != null && typeof obj === "object" && obj.type === "ide-status-update"; } @@ -106,4 +114,12 @@ export namespace IDEFrontendDashboardService { export function isSetStateEventData(obj: any): obj is SetStateEventData { return obj != null && typeof obj === "object" && obj.type === "ide-set-state"; } + + export function isOpenBrowserIDE(obj: any): obj is OpenBrowserIDE { + return obj != null && typeof obj === "object" && obj.type === "ide-open-browser"; + } + + export function isOpenDesktopIDE(obj: any): obj is OpenDesktopIDE { + return obj != null && typeof obj === "object" && obj.type === "ide-open-desktop"; + } } diff --git a/components/supervisor/frontend/src/regular.ts b/components/supervisor/frontend/src/regular.ts index a5200fec6a7bbc..6213d253738c3e 100644 --- a/components/supervisor/frontend/src/regular.ts +++ b/components/supervisor/frontend/src/regular.ts @@ -75,16 +75,12 @@ LoadingFrame.load().then(async (loading) => { const supervisorServiceClient = SupervisorServiceClient.get(); let hideDesktopIde = false; - const serverOrigin = startUrl.url.origin; - const hideDesktopIdeEventListener = (event: MessageEvent) => { - if (event.origin === serverOrigin && event.data.type == "openBrowserIde") { - window.removeEventListener("message", hideDesktopIdeEventListener); - hideDesktopIde = true; - toStop.push(ideService.start()); - } - }; - window.addEventListener("message", hideDesktopIdeEventListener, false); - toStop.push({ dispose: () => window.removeEventListener("message", hideDesktopIdeEventListener) }); + const hideDesktopIdeEventListener = frontendDashboardServiceClient.onOpenBrowserIDE(() => { + hideDesktopIdeEventListener.dispose(); + hideDesktopIde = true; + toStop.push(ideService.start()); + }); + toStop.push(hideDesktopIdeEventListener); //#region gitpod browser telemetry // TODO(ak) get rid of it @@ -140,7 +136,7 @@ LoadingFrame.load().then(async (loading) => { }); if (!desktopRedirected) { desktopRedirected = true; - loading.openDesktopLink(ideStatus.desktop.link); + frontendDashboardServiceClient.openDesktopIDE(ideStatus.desktop.link); } return loading.frame; } diff --git a/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts b/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts index 0ccb6165509aa7..e87945327c1c5d 100644 --- a/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts +++ b/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts @@ -15,33 +15,37 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi private readonly onDidChangeEmitter = new Emitter(); readonly onStatusUpdate = this.onDidChangeEmitter.event; + private readonly onOpenBrowserIDEEmitter = new Emitter(); + readonly onOpenBrowserIDE = this.onOpenBrowserIDEEmitter.event; + private resolveInit!: () => void; private initPromise = new Promise((resolve) => (this.resolveInit = resolve)); + private version?: number; + constructor(private serverWindow: Window) { window.addEventListener("message", (event: MessageEvent) => { if (event.origin !== serverUrl.url.origin) { return; } if (IDEFrontendDashboardService.isStatusUpdateEventData(event.data)) { + this.version = event.data.version; this.latestStatus = event.data.status; this.resolveInit(); this.onDidChangeEmitter.fire(this.latestStatus); } if (IDEFrontendDashboardService.isRelocateEventData(event.data)) { - this.relocate(event.data.url); + window.location.href = event.data.url; + } + if (IDEFrontendDashboardService.isOpenBrowserIDE(event.data)) { + this.onOpenBrowserIDEEmitter.fire(undefined); } }); } - initialize(): Promise { return this.initPromise; } - relocate(url: string): void { - window.location.href = url; - } - trackEvent(msg: RemoteTrackMessage): void { this.serverWindow.postMessage( { type: "ide-track-event", msg } as IDEFrontendDashboardService.TrackEventData, @@ -62,4 +66,32 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi serverUrl.url.origin, ); } + + openDesktopIDE(url: string): void { + if (this.version && this.version >= 1) { + // always perfrom redirect to dekstop IDE on gitpod origin + // to avoid confirmation popup on each workspace origin + this.serverWindow.postMessage( + { type: "ide-open-desktop", url } as IDEFrontendDashboardService.OpenDesktopIDE, + serverUrl.url.origin, + ); + return; + } + + // TODO(ak) remove after new dashboard is deployed + let redirect = false; + try { + const desktopLink = new URL(url); + redirect = desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:"; + } catch (e) { + console.error("invalid desktop link:", e); + } + // redirect only if points to desktop application + // don't navigate browser to another page + if (redirect) { + window.location.href = url; + } else { + window.open(url, "_blank", "noopener"); + } + } } diff --git a/components/supervisor/frontend/src/shared/loading-frame.ts b/components/supervisor/frontend/src/shared/loading-frame.ts index 74282ae983afe2..22686fbb8b50c5 100644 --- a/components/supervisor/frontend/src/shared/loading-frame.ts +++ b/components/supervisor/frontend/src/shared/loading-frame.ts @@ -10,7 +10,6 @@ import { startUrl } from "./urls"; export function load(): Promise<{ frame: HTMLIFrameElement; frontendDashboardServiceClient: FrontendDashboardServiceClient; - openDesktopLink: (link: string) => void; }> { return new Promise((resolve) => { const frame = document.createElement("iframe"); @@ -21,23 +20,7 @@ export function load(): Promise<{ frame.onload = () => { const frontendDashboardServiceClient = new FrontendDashboardServiceClient(frame.contentWindow!); - const openDesktopLink = (link: string) => { - let redirect = false; - try { - const desktopLink = new URL(link); - redirect = desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:"; - } catch (e) { - console.error("invalid desktop link:", e); - } - // redirect only if points to desktop application - // don't navigate browser to another page - if (redirect) { - window.location.href = link; - } else { - window.open(link, "_blank", "noopener"); - } - }; - resolve({ frame, frontendDashboardServiceClient, openDesktopLink }); + resolve({ frame, frontendDashboardServiceClient }); }; }); }