From ce08ce05f2a3df62a0847d0cdf37f99e29a9059b Mon Sep 17 00:00:00 2001 From: Sourabh Jain Date: Fri, 21 Feb 2025 21:09:44 +0530 Subject: [PATCH] mongo is working --- package-lock.json | 20 +- package.json | 2 +- src/Explorer/Tabs/XTermComponent.tsx | 624 +++++++++++++++------------ src/Utils/AuthorizationUtils.ts | 9 +- 4 files changed, 349 insertions(+), 306 deletions(-) diff --git a/package-lock.json b/package-lock.json index 176d7d5cf..75dcb7203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "@xmldom/xmldom": "0.7.13", + "@xterm/xterm": "5.5.0", "allotment": "1.20.2", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", @@ -113,7 +114,6 @@ "tinykeys": "2.1.0", "underscore": "1.12.1", "utility-types": "3.10.0", - "xterm-for-react": "1.0.4", "zustand": "3.5.0" }, "devDependencies": { @@ -13238,6 +13238,11 @@ "node": ">=10.0.0" } }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "license": "BSD-3-Clause" @@ -36550,19 +36555,6 @@ "xterm": "^4.0.0" } }, - "node_modules/xterm-for-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/xterm-for-react/-/xterm-for-react-1.0.4.tgz", - "integrity": "sha512-DCkLR9ZXeW907YyyaCTk/3Ol34VRHfCnf3MAPOkj3dUNA85sDqHvTXN8efw4g7bx7gWdJQRsEpGt2tJOXKG3EQ==", - "dependencies": { - "prop-types": "^15.7.2", - "xterm": "^4.5.0" - }, - "peerDependencies": { - "react": "^16.0.0", - "react-dom": "^16.0.0" - } - }, "node_modules/y18n": { "version": "4.0.3", "license": "ISC" diff --git a/package.json b/package.json index 1882b40e2..77409d3ae 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "@xmldom/xmldom": "0.7.13", - "xterm-for-react":"1.0.4", + "@xterm/xterm": "5.5.0", "allotment": "1.20.2", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", diff --git a/src/Explorer/Tabs/XTermComponent.tsx b/src/Explorer/Tabs/XTermComponent.tsx index d59d3d7ad..787aee9e7 100644 --- a/src/Explorer/Tabs/XTermComponent.tsx +++ b/src/Explorer/Tabs/XTermComponent.tsx @@ -1,28 +1,55 @@ import React, { useEffect, useRef } from "react"; import { v4 as uuidv4 } from 'uuid'; -import { XTerm } from "xterm-for-react"; -import { useAADAuth } from "../../hooks/useAADAuth"; +import { Terminal } from "xterm"; +import "xterm/css/xterm.css"; import { userContext } from "../../UserContext"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; const XTermComponent: React.FC = () => { - const xtermRef = useRef(null); - const intervalsToClearRef = useRef([]); + const terminalRef = useRef(null); // Reference for terminal container + const xtermRef = useRef(null); // Reference for XTerm instance + const socketRef = useRef(null); // Reference for WebSocket + const intervalsToClearRef = useRef([]); - useEffect(() => { - if (xtermRef.current) { - xtermRef.current.terminal.writeln("Hello, World!"); - } + useEffect(() => { + // Initialize XTerm instance + const term = new Terminal({ + cursorBlink: true, + fontSize: 14, + theme: { background: "#1d1f21", foreground: "#c5c8c6" }, + }); - const authorizationHeader = getAuthorizationHeader() - startCloudShellterminal(xtermRef.current.terminal, intervalsToClearRef, authorizationHeader.token); + // Attach terminal to the DOM + if (terminalRef.current) { + term.open(terminalRef.current); + xtermRef.current = term; + } + + term.writeln("Hello, World!"); + + const authorizationHeader = getAuthorizationHeader() + socketRef.current = startCloudShellterminal(term, intervalsToClearRef, authorizationHeader.token); - }, []); + term.onData((data) => { + if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { + socketRef.current.send(data); + } + }); - return ; + // Cleanup function to close WebSocket and dispose terminal + return () => { + if (socketRef.current) { + socketRef.current.close(); // Close WebSocket connection + } + term.dispose(); // Clean up XTerm instance + }; + + }, []); + + return
; }; -const startCloudShellterminal = async (xterminal: any, intervalsToClearRef: any, authorizationToken: any) => { +const startCloudShellterminal = async (xterminal: Terminal, intervalsToClearRef: any, authorizationToken: any) => { // const allowedParentFrameAuthorities = ["localhost:1234", "localhost:3000", "portal.azure.com", "portal.azure.us", "rc.portal.azure.com", "ms.portal.azure.com", "canary.portal.azure.com", "canary-ms.portal.azure.com", "docs.microsoft.com", "review.docs.microsoft.com", "ppe.docs.microsoft.com", "ux.console.azure.us", "admin-local.teams.microsoft.net", "admin-ignite.microsoft.com", "wusportalprv.office.com", "portal-sdf.office.com", "ncuportalprv.office.com", "admin.microsoft.com", "portal.microsoft.com", "portal.office.com", "admin.microsoft365.com", "admin-sdf.exchange.microsoft.com", "admin.exchange.microsoft.com", "cloudconsole-ux-prod-usnatwest.appservice.eaglex.ic.gov", "cloudconsole-ux-prod-usnateast.appservice.eaglex.ic.gov", "portal.azure.eaglex.ic.gov", "cloudconsole-ux-prod-ussecwest.appservice.microsoft.scloud", "cloudconsole-ux-prod-usseceast.appservice.microsoft.scloud", "portal.azure.microsoft.scloud", "admin-local.teams.microsoft.net", "admin-dev.teams.microsoft.net", "admin-int.teams.microsoft.net", "admin.teams.microsoft.com", "preview.portal.azure.com", "learn.microsoft.com", "review.learn.microsoft.com", "ppe.learn.microsoft.com", "dev.learn.microsoft.com"]; // const trustedParentOrigin = getTrustedParentOrigin(); @@ -59,28 +86,33 @@ const startCloudShellterminal = async (xterminal: any, intervalsToClearRef: any, } const region = await getUserRegion(authorizationToken, userContext.subscriptionId).then((res) => { - const reqId = (res.headers as any).get("x-ms-routing-request-id"); - const location = reqId?.split(":")?.[0]?.toLowerCase() ?? ""; - const validRegions = new Set(["westus", "southcentralus", "eastus", "northeurope", "westeurope", "centralindia", "southeastasia", "westcentralus", "eastus2euap", "centraluseuap"]); - if (validRegions.has(location.toLowerCase())) { - return location; - } - if (location === "centralus") { - return "centraluseuap"; - } - if (location === "eastus2") { - return "eastus2euap"; - } - return "westus"; - }).catch((err) => { - xterminal.writeln(''); - xterminal.writeln('Unable to get user region.'); - return "westus"; + // const reqId = (res.headers as any).get("x-ms-routing-request-id"); + // const location = reqId?.split(":")?.[0]?.toLowerCase() ?? ""; + // const validRegions = new Set(["westus", "southcentralus", "eastus", "northeurope", "westeurope", "centralindia", "southeastasia", "westcentralus", "eastus2euap", "centraluseuap"]); + // if (validRegions.has(location.toLowerCase())) { + // return location; + // } + // if (location === "centralus") { + // return "centraluseuap"; + // } + // if (location === "eastus2") { + // return "eastus2euap"; + // } + // return "westus"; + // }).catch((err) => { + // xterminal.writeln(''); + // xterminal.writeln('Unable to get user region.'); + // return "westus"; + return "westus"; }); + + //const cloudshellToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false, "b677c290-cf4b-4a8e-a60e-91ba650a4abe"); + xterminal.writeln('Requested Region ' + region); + const cloudshellToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImltaTBZMnowZFlLeEJ0dEFxS19UdDVoWUJUayIsImtpZCI6ImltaTBZMnowZFlLeEJ0dEFxS19UdDVoWUJUayJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNzQwMDQ2ODAzLCJuYmYiOjE3NDAwNDY4MDMsImV4cCI6MTc0MDA1MjQ4OSwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2U4MGZmZGE4LTlmZDUtNDQ4ZC05M2VhLWY5YzgyM2ZjN2RkOC9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVpRQWEvOFpBQUFBVWd4YU5ram1kVWZ5VlhmR3kwODJzaEFkUEFQYkd6NW1TNDBjNW0zM3hjQTNCYmpOSTVjNTArVloxMFNCbzNGNjV2Rml6a2J4Z2VuNXk1dXQvWGVVZmUvNHMyb1lSdkczTnR4V2NJK09samI2aHRBQzNuSk5uQ1JINnNnUHNqd2VBZFkxcXZTTTFnMUtZVmZ5MG11Nm5aL0NYQWhCSkpoNWNLLzRNS0F5TzZvc2NDZjN0Q2N3dS9ZcXd6ZzIwbG9UIiwiYW1yIjpbInJzYSIsIm1mYSJdLCJhcHBpZCI6ImI2NzdjMjkwLWNmNGItNGE4ZS1hNjBlLTkxYmE2NTBhNGFiZSIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiZTM4YzBiOTgtMzQ5OS00YWQzLTkwN2EtYjc2NzJjNzdkZTQ3IiwiZmFtaWx5X25hbWUiOiJKYWluIiwiZ2l2ZW5fbmFtZSI6IlNvdXJhYmgiLCJpZHR5cCI6InVzZXIiLCJpcGFkZHIiOiIyNDA0OmY4MDE6ODAyODozOjhjZTU6MTk2ZDpjNWE3OmQ3YTYiLCJuYW1lIjoiU291cmFiaCBKYWluIiwib2lkIjoiZTgwZmZkYTgtOWZkNS00NDhkLTkzZWEtZjljODIzZmM3ZGQ4Iiwib25wcmVtX3NpZCI6IlMtMS01LTIxLTIxNDY3NzMwODUtOTAzMzYzMjg1LTcxOTM0NDcwNy0yNzA3MDY2IiwicHVpZCI6IjEwMDMyMDAxMUE2OTQ1RjAiLCJyaCI6IjEuQVJvQXY0ajVjdkdHcjBHUnF5MTgwQkhiUjBaSWYza0F1dGRQdWtQYXdmajJNQk1hQU9nYUFBLiIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInNpZCI6IjAwMjAxMzA5LTJhNjAtY2M1Yy1iNTMxLTNiMGQwNWFkMWY3NSIsInN1YiI6Ijh4c0R4U0tqcmcycXdXaTNYM0pmLXkxUkNXUjZ2UDBEZ0pFbEtoTW05bTAiLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InNvdXJhYmhqYWluQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzb3VyYWJoamFpbkBtaWNyb3NvZnQuY29tIiwidXRpIjoiN3BZYmN4MDRja3liNHZlYy1tMUdBQSIsInZlciI6IjEuMCIsIndpZHMiOlsiYjc5ZmJmNGQtM2VmOS00Njg5LTgxNDMtNzZiMTk0ZTg1NTA5Il0sInhtc19pZHJlbCI6IjI4IDEiLCJ4bXNfdGNkdCI6MTI4OTI0MTU0N30.A3eOAHuSDbA3w4n5r4xaMzpchoMuQMzAy7g7pyWGpY-zHsbUykUDYgbSOpAytMDzkcL9pbVCPlB8OxNnFOtgUn0lBRxmInCf-xWp38WoxSy_kqJ59i6PSmjSyNRVxHP70b3dNO3ZT6rkdvWWghaImTV-thQoSQyO7jYJrgEwhu8wNUV_uEQ67IGTKdylo0TupIxYW6VxpfMWfkVGaPRuZHnjQe14PwisZIJ9KJnTkgsszrv_fefbUkiE4dcG9PaWmIfSs7vLAsszNp2IozTo5VReZCztmxdTY1bNSRd2AKYb3wgywOTbB5DDzUxLLr2VofK946_eN8bHAm6uouiNOw"; try { // do not use the subscription from the preferred settings use the one from the context - await putEphemeralUserSettings(userContext.subscriptionId, region, authorizationToken); + await putEphemeralUserSettings(userContext.subscriptionId, region, `${cloudshellToken}`); } catch (err) { xterminal.writeln(''); xterminal.writeln('Unable to update user settings to ephemeral session.'); @@ -90,7 +122,7 @@ const startCloudShellterminal = async (xterminal: any, intervalsToClearRef: any, // verify user settings after they have been updated to ephemeral try { - const userSettings = await getUserSettings(authorizationToken); + const userSettings = await getUserSettings(cloudshellToken); const isValidUserSettings = validateUserSettings(userSettings); if (!isValidUserSettings) { throw new Error("Invalid user settings detected for ephemeral session."); @@ -105,7 +137,7 @@ const startCloudShellterminal = async (xterminal: any, intervalsToClearRef: any, // trigger callback to provision console internal let provisionConsoleResponse; try { - provisionConsoleResponse = await provisionConsole(userContext.subscriptionId, authorizationToken, region); + provisionConsoleResponse = await provisionConsole(userContext.subscriptionId, cloudshellToken, region); // statusPaneUpdateCommands.setTerminalUri(provisionConsoleResponse.properties.uri); } catch (err) { xterminal.writeln(''); @@ -125,7 +157,7 @@ const startCloudShellterminal = async (xterminal: any, intervalsToClearRef: any, // connect the terminal let connectTerminalResponse; try { - connectTerminalResponse = await connectTerminal(provisionConsoleResponse.properties.uri, authorizationToken, { rows: xterminal.rows, cols: xterminal.cols }); + connectTerminalResponse = await connectTerminal(provisionConsoleResponse.properties.uri, cloudshellToken, { rows: xterminal.rows, cols: xterminal.cols }); } catch (err) { xterminal.writeln(''); xterminal.writeln('Unable to connect terminal.'); @@ -147,12 +179,12 @@ const startCloudShellterminal = async (xterminal: any, intervalsToClearRef: any, socketUri = 'wss://' + targetUriBodyArr[0] + '/$hc/' + targetUriBodyArr[1] + '/terminals/' + termId; } - // provision appropriate first party permissions to cloudshell instance - await postTokens(provisionConsoleResponse.properties.uri, authorizationToken).catch((err) => { - xterminal.writeln('Unable to provision first party permissions to cloudshell instance.'); - intervalsToClear.forEach((val) => window.clearInterval(+val)); - throw err; - }); + // // provision appropriate first party permissions to cloudshell instance + // await postTokens(provisionConsoleResponse.properties.uri, authorizationToken).catch((err) => { + // xterminal.writeln('Unable to provision first party permissions to cloudshell instance.'); + // intervalsToClear.forEach((val) => window.clearInterval(+val)); + // throw err; + // }); const socket = new WebSocket(socketUri); @@ -160,16 +192,21 @@ const startCloudShellterminal = async (xterminal: any, intervalsToClearRef: any, // authorize the session try { - const authorizeResponse = await authorizeSession(provisionConsoleResponse.properties.uri, authorizationToken); + const authorizeResponse = await authorizeSession(provisionConsoleResponse.properties.uri, cloudshellToken); const cookieToken = authorizeResponse.token; const a = document.createElement("img"); - a.src = targetUri + "?token=" + encodeURIComponent(cookieToken); + a.src = targetUri + "&token=" + encodeURIComponent(cookieToken); } catch (err) { xterminal.writeln('Unable to authroize the session'); intervalsToClear.forEach((val) => window.clearInterval(+val)); socket.close(); throw err; } + + xterminal.writeln("Connected to cloudshell."); + xterminal.focus(); + + return socket; } export const validateUserSettings = (userSettings: Settings) => { @@ -183,260 +220,267 @@ export const validateUserSettings = (userSettings: Settings) => { export const enum OsType { Linux = "linux", Windows = "windows" - } - - export const enum ShellType { +} + +export const enum ShellType { Bash = "bash", PowerShellCore = "pwsh" - } +} - export const enum NetworkType { +export const enum NetworkType { Default = "Default", Isolated = "Isolated" - } - - export const enum SessionType { +} + +export const enum SessionType { Mounted = "Mounted", Ephemeral = "Ephemeral" - } - - // https://stackoverflow.com/q/38598280 (Is it possible to wrap a function and retain its types?) - export const trackedApiCall = , U>(apiCall: (...args: T) => Promise, name: string) => { - return async (...args: T): Promise => { - const startTime = Date.now(); - const result = await apiCall(...args); - const endTime = Date.now(); - return result; - }; - }; - - export const getUserRegion = trackedApiCall(async (authToken: string, subscriptionId: string) => { - const locale = getLocale(); - const locationUri = getArmUri("management.azure.com")(`/subscriptions/${subscriptionId}/locations?api-version=2022-12-01`).toString(); - return await fetch(locationUri, { - method: "get", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': authToken, - 'Accept-Language': locale, - 'x-ms-correlation-request-id': uuidv4(), - } - }); - }, "getUserRegion"); - - export type Settings = { - location: string; - sessionType: SessionType; - osType: OsType; - }; - - export const getUserSettings = trackedApiCall(async (authToken: string): Promise => { - // figure out how to set the Accept-Language dynamically - const armUri = getArmUri("management.azure.com")(`/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2023-02-01-preview`).toString();; - const locale = getLocale(); - const resp = await fetch(armUri, { - method: "get", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': authToken, - 'Accept-Language': locale, - 'x-ms-correlation-request-id': uuidv4(), - } - }); - - const json = await resp?.json() as any; - return { - location: json?.properties?.preferredLocation, - sessionType: json?.properties?.sessionType, - osType: json?.properties?.preferredOsType - }; - }, "getUserSettings"); - - export const verifyCloudshellProviderRegistration = async(subscriptionId: string, authToken: string) => { - const targetUri = getArmUri("management.azure.com")(`/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell?api-version=2022-12-01`).toString(); - const locale = getLocale(); - return await fetch(targetUri, { - method: "get", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': authToken, - 'Accept-Language': locale, - } - }); - }; - - export const registerCloudShellProvider = async (subscriptionId: string, authToken: string) => { - const targetUri = getArmUri("management.azure.com")(`/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell/register?api-version=2022-12-01`).toString(); - return await fetch(targetUri, { - method: "post", - headers: { - 'Content-Length': "0", - 'Content-Type': 'application/json', - 'Authorization': authToken - } - }); - }; - - // TODO: update accept language header - export const putEphemeralUserSettings = trackedApiCall(async (userSubscriptionId: string, userRegion: string, authorizationToken: string) => { - const ephemeralSettings = { - properties: { - preferredOsType: OsType.Linux, - preferredShellType: ShellType.Bash, - preferredLocation: userRegion, - networkType: NetworkType.Default, - sessionType: SessionType.Ephemeral, - userSubscription: userSubscriptionId, - } - }; - - const armUri = getArmUri("management.azure.com")(`/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2023-02-01-preview`).toString(); - await fetch(armUri, { - method: "put", - body: JSON.stringify(ephemeralSettings), - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': authorizationToken, - 'Accept-Language': getLocale(), - } - }); - }, "putEphemeralUserSettings"); - - type provisionConsoleResponse = { - properties: { - osType: OsType; - provisioningState: string; - uri: string; - }; - }; - - export const provisionConsole = trackedApiCall(async (subscriptionId: string, authorizationToken: string, location: string): Promise => { - const armUri = getArmUri("management.azure.com")(`providers/Microsoft.Portal/consoles/default?api-version=2023-02-01-preview&feature.azureconsole.sessiontype=mounted&feature.azureconsole.usersubscription=${subscriptionId}`).toString(); - - const data = { - properties: { - osType: OsType.Linux - } - }; - const resp = await fetch(armUri, { - method: "put", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': authorizationToken, - 'x-ms-console-preferred-location': location, - 'x-ms-correlation-request-id': uuidv4(), - 'Accept-Language': getLocale() - }, - body: JSON.stringify(data) - }); - return resp.json(); - }, "provisionConsole"); - - export type ConnectTerminalResponse = { - id: string; - idleTimeout: string; - rootDirectory: string; - socketUri: string; - tokenUpdated: boolean; - }; - - export const postTokens = trackedApiCall(async (consoleUri: string, authorizationToken: string) => { - const targetUri = consoleUri + '/accessToken'; - let aadAuth = useAADAuth(); - let token = aadAuth.armToken; - - await fetch(targetUri, { - method: "post", - headers: { - 'Accept': 'application/json', - 'Authorization': authorizationToken, - 'x-ms-client-request-id': uuidv4(), - 'Accept-Language': getLocale() - }, - body: JSON.stringify({ token }) - }); - }, "postTokens"); - - export const connectTerminal = trackedApiCall(async (consoleUri: string, authorizationToken: string, size: { rows: number, cols: number }): Promise => { - const targetUri = consoleUri + `/terminals?cols=${size.cols}&rows=${size.rows}&version=2019-01-01&shell=bash`; - const resp = await fetch(targetUri, { - method: "post", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Content-Length': '2', - 'Authorization': authorizationToken, - 'x-ms-client-request-id': uuidv4(), - 'Accept-Language': getLocale(), - }, - body: "{}" // empty body is necessary - }); - return resp.json(); - }, "connectTerminal"); - - export type Authroization = { - token: string; - }; - - export const authorizeSession = trackedApiCall(async (consoleUri: string, accessToken: string): Promise => { - const targetUri = consoleUri + "/authorize"; - const resp = await fetch(targetUri, { - method: "post", - headers: { - 'Accept': 'application/json', - 'Authorization': accessToken, - 'Accept-Language': getLocale(), - "Content-Type": 'application/json' - } - }); - return resp.json(); - }, "authorizeSession"); - - - export const getArmUri = (origin: string): (relativePath: string) => string => { - let originNoTrailingSlash = origin; - if (origin.endsWith("/")) { - originNoTrailingSlash = originNoTrailingSlash.slice(0, originNoTrailingSlash.length - 1); - } else { - origin += "/"; - } - - return (relativePath: string) => { - if (!relativePath) { - throw new Error(`relativePath is required: ${relativePath}`); - } - - return `https://${relativePath.charAt(0) === "/" ? originNoTrailingSlash : origin}${relativePath}`; +} + +// https://stackoverflow.com/q/38598280 (Is it possible to wrap a function and retain its types?) +export const trackedApiCall = , U>(apiCall: (...args: T) => Promise, name: string) => { + return async (...args: T): Promise => { + const startTime = Date.now(); + const result = await apiCall(...args); + const endTime = Date.now(); + return result; }; - } - - export const getLocale = () => { +}; + +export const getUserRegion = trackedApiCall(async (authToken: string, subscriptionId: string) => { + const locale = getLocale(); + const locationUri = getArmUri("management.azure.com")(`/subscriptions/${subscriptionId}/locations?api-version=2022-12-01`).toString(); + return await fetch(locationUri, { + method: "get", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken, + 'Accept-Language': locale, + 'x-ms-correlation-request-id': uuidv4(), + } + }); +}, "getUserRegion"); + +export type Settings = { + location: string; + sessionType: SessionType; + osType: OsType; +}; + +export const getUserSettings = trackedApiCall(async (authToken: string): Promise => { + // figure out how to set the Accept-Language dynamically + const armUri = getArmUri("management.azure.com")(`/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2023-02-01-preview`).toString();; + const locale = getLocale(); + const resp = await fetch(armUri, { + method: "get", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken, + 'Accept-Language': locale, + 'x-ms-correlation-request-id': uuidv4(), + } + }); + + const json = await resp?.json() as any; + return { + location: json?.properties?.preferredLocation, + sessionType: json?.properties?.sessionType, + osType: json?.properties?.preferredOsType + }; +}, "getUserSettings"); + +export const verifyCloudshellProviderRegistration = async(subscriptionId: string, authToken: string) => { + const targetUri = getArmUri("management.azure.com")(`/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell?api-version=2022-12-01`).toString(); + const locale = getLocale(); + return await fetch(targetUri, { + method: "get", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken, + 'Accept-Language': locale, + } + }); +}; + +export const registerCloudShellProvider = async (subscriptionId: string, authToken: string) => { + const targetUri = getArmUri("management.azure.com")(`/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell/register?api-version=2022-12-01`).toString(); + return await fetch(targetUri, { + method: "post", + headers: { + 'Content-Length': "0", + 'Content-Type': 'application/json', + 'Authorization': authToken + } + }); +}; + +// TODO: update accept language header +export const putEphemeralUserSettings = trackedApiCall(async (userSubscriptionId: string, userRegion: string, authorizationToken: string) => { + const ephemeralSettings = { + properties: { + preferredOsType: OsType.Linux, + preferredShellType: ShellType.Bash, + preferredLocation: userRegion, + networkType: NetworkType.Default, + sessionType: SessionType.Ephemeral, + userSubscription: userSubscriptionId, + } + }; + + const armUri = getArmUri("management.azure.com")(`/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2023-02-01-preview`).toString(); + await fetch(armUri, { + method: "put", + body: JSON.stringify(ephemeralSettings), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authorizationToken, + 'Accept-Language': getLocale(), + } + }); +}, "putEphemeralUserSettings"); + +type provisionConsoleResponse = { + properties: { + osType: OsType; + provisioningState: string; + uri: string; + }; +}; + +export const provisionConsole = trackedApiCall(async (subscriptionId: string, authorizationToken: string, location: string): Promise => { + const armUri = getArmUri("management.azure.com")(`providers/Microsoft.Portal/consoles/default?api-version=2023-02-01-preview&feature.azureconsole.sessiontype=mounted&feature.azureconsole.usersubscription=${subscriptionId}`).toString(); + + const data = { + properties: { + osType: OsType.Linux + } + }; + const resp = await fetch(armUri, { + method: "put", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authorizationToken, + 'x-ms-console-preferred-location': location, + 'x-ms-correlation-request-id': uuidv4(), + 'Accept-Language': getLocale() + }, + body: JSON.stringify(data) + }); + return resp.json(); +}, "provisionConsole"); + +export type ConnectTerminalResponse = { + id: string; + idleTimeout: string; + rootDirectory: string; + socketUri: string; + tokenUpdated: boolean; +}; + +export const postTokens = trackedApiCall(async (consoleUri: string, authorizationToken: string) => { + const targetUri = consoleUri + '/accessToken'; + let token = aadAuth.armToken; + + await fetch(targetUri, { + method: "post", + headers: { + 'Accept': 'application/json', + 'Authorization': authorizationToken, + 'x-ms-client-request-id': uuidv4(), + 'Accept-Language': getLocale() + }, + body: JSON.stringify({ token }) + }); +}, "postTokens"); + +export const connectTerminal = trackedApiCall(async (consoleUri: string, authorizationToken: string, size: { rows: number, cols: number }): Promise => { + const targetUri = consoleUri + `/terminals?cols=${size.cols}&rows=${size.rows}&version=2019-01-01&shell=bash`; + const resp = await fetch(targetUri, { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Content-Length': '2', + 'Authorization': authorizationToken, + 'x-ms-client-request-id': uuidv4(), + 'Accept-Language': getLocale(), + }, + body: "{}" // empty body is necessary + }); + return resp.json(); +}, "connectTerminal"); + +export type Authorization = { + token: string; +}; + +export const authorizeSession = trackedApiCall(async (consoleUri: string, accessToken: string): Promise => { + const targetUri = consoleUri + "/authorize"; + const resp = await fetch(targetUri, { + method: "post", + headers: { + 'Accept': 'application/json', + 'Authorization': accessToken, + 'Accept-Language': getLocale(), + "Content-Type": 'application/json' + }, + body: "{}" // empty body is necessary + }); + return resp.json(); +}, "authorizeSession"); + + +export const getArmUri = (origin: string): (relativePath: string) => string => { +let originNoTrailingSlash = origin; +if (origin.endsWith("/")) { + originNoTrailingSlash = originNoTrailingSlash.slice(0, originNoTrailingSlash.length - 1); +} else { + origin += "/"; +} + +return (relativePath: string) => { + if (!relativePath) { + throw new Error(`relativePath is required: ${relativePath}`); + } + + return `https://${relativePath.charAt(0) === "/" ? originNoTrailingSlash : origin}${relativePath}`; +}; +} + +export const getLocale = () => { const langLocale = navigator.language; return (langLocale && langLocale.length === 2 ? langLocale[1] : 'en-us'); - }; - - export const getTrustedParentOrigin = () => { +}; + +export const getTrustedParentOrigin = () => { const searchParams = new URLSearchParams(window.location.search); return searchParams.get("trustedAuthority") || ''; - } - - let keepAliveID: NodeJS.Timeout = null; - let pingCount = 0; - - export const configureSocket = (socket: WebSocket, uri: string, terminal: any, intervals: NodeJS.Timer[], socketRetryCount: number) => { +} + +let keepAliveID: NodeJS.Timeout = null; +let pingCount = 0; + +export const configureSocket = (socket: WebSocket, uri: string, terminal: any, intervals: NodeJS.Timer[], socketRetryCount: number) => { let jsonData = ''; socket.onopen = () => { + terminal.writeln("Socket Opened"); const initializeCommand = - `rm -rf ie.log && rm -rf ie && rm -rf scenarios/ && \n` + - `echo Welcome to this quick start shell. This Cloud Shell terminal will be used to execute commands as part of the scenario. Follow the instructions on the left to get started\n`; - + `curl -s https://ipinfo.io \n` + + `curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz \n` + + `tar -xvzf mongosh-2.3.8-linux-x64.tgz \n` + + `mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/ \n` + + `echo 'export PATH=$PATH:$HOME/mongosh/bin' >> ~/.bashrc \n` + + `source ~/.bashrc \n` + + `mongosh --version \n`; + + terminal.writeln(initializeCommand); socket.send(initializeCommand); - + const keepSocketAlive = (socket: WebSocket) => { if (socket.readyState === WebSocket.OPEN) { if ((pingCount / 60) >= 20) { @@ -450,8 +494,9 @@ export const enum OsType { }; keepSocketAlive(socket); }; - + socket.onclose = () => { + terminal.writeln("Socket Closed"); if (keepAliveID) { clearTimeout(keepAliveID); pingCount = 0; @@ -459,10 +504,10 @@ export const enum OsType { intervals.forEach((val) => { window.clearInterval(+val); }); - + terminal.writeln("Session terminated. Please refresh the page to start a new session."); }; - + socket.onerror = () => { terminal.writeln("terminal reconnected"); if (socketRetryCount < 10 && socket.readyState !== WebSocket.CLOSED) { @@ -474,11 +519,12 @@ export const enum OsType { socket.close(); } }; - + socket.onmessage = (event: MessageEvent) => { + terminal.writeln("Socket onMessage"); // if we are sending and receiving messages the terminal is not idle set ping count to 0 pingCount = 0; - + // check if we are dealing with array buffer or string let eventData = ''; if (typeof event.data === "object") { @@ -491,8 +537,10 @@ export const enum OsType { } if (typeof event.data === 'string') { eventData = event.data; + + terminal.write(eventData); } - + // process as one line or process as multiline if (eventData.includes("ie_us") && eventData.includes("ie_ue")) { // process as one line @@ -511,6 +559,8 @@ export const enum OsType { jsonData += eventData; } }; + return socket; - }; +}; + export default XTermComponent; diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index d2ef4e8ff..6e844ed72 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -46,14 +46,14 @@ export function decryptJWTToken(token: string) { return JSON.parse(tokenPayload); } -export async function getMsalInstance() { +export async function getMsalInstance(clientId: string = "203f1145-856a-4232-83d4-a43568fba23d"){ const msalConfig: msal.Configuration = { cache: { cacheLocation: "localStorage", }, auth: { authority: `${configContext.AAD_ENDPOINT}organizations`, - clientId: "203f1145-856a-4232-83d4-a43568fba23d", + clientId: clientId, }, }; @@ -68,7 +68,8 @@ export async function getMsalInstance() { export async function acquireMsalTokenForAccount( account: DatabaseAccount, silent: boolean = false, - user_hint?: string, + clientId: string = "203f1145-856a-4232-83d4-a43568fba23d", + user_hint?: string ) { if (userContext.databaseAccount.properties?.documentEndpoint === undefined) { throw new Error("Database account has no document endpoint defined"); @@ -77,7 +78,7 @@ export async function acquireMsalTokenForAccount( /\/+$/, "/.default", ); - const msalInstance = await getMsalInstance(); + const msalInstance = await getMsalInstance(clientId); const knownAccounts = msalInstance.getAllAccounts(); // If user_hint is provided, we will try to use it to find the account. // If no account is found, we will use the current active account or first account in the list.