This commit is contained in:
Alex Weininger 2023-05-15 11:55:15 -07:00 коммит произвёл GitHub
Родитель a7a365ca5d
Коммит 14b9d28014
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 214 добавлений и 192 удалений

Просмотреть файл

@ -7,11 +7,13 @@ import { callWithTelemetryAndErrorHandlingSync, IActionContext, IParsedError, pa
import * as cp from 'child_process';
import * as FormData from 'form-data';
import { ReadStream } from 'fs';
import * as http from 'http';
import { ClientRequest } from 'http';
import { DeviceTokenCredentials } from 'ms-rest-azure';
import { Socket } from 'net';
import { Response } from 'node-fetch';
import * as path from 'path';
import * as request from 'request-promise';
import * as semver from 'semver';
import { parse, UrlWithStringQuery } from 'url';
import { v4 as uuid } from 'uuid';
@ -24,9 +26,11 @@ import { tokenFromRefreshToken } from '../login/adal/tokens';
import { getAuthLibrary } from '../login/getAuthLibrary';
import { localize } from '../utils/localize';
import { logErrorMessage } from '../utils/logErrorMessage';
import { HttpLogger } from '../utils/logging/HttpLogger';
import { fetchWithLogging } from '../utils/logging/nodeFetch/nodeFetch';
import { RequestNormalizer } from '../utils/logging/request/RequestNormalizer';
import { Deferred } from '../utils/promiseUtils';
import { AccessTokens, connectTerminal, ConsoleUris, Errors, getUserSettings, provisionConsole, resetConsole, Size, UserSettings } from './cloudConsoleLauncher';
import { delay } from '../utils/timeUtils';
import { CloudShellInternal } from './CloudShellInternal';
import { createServer, Queue, readJSON, Server } from './ipc';
@ -645,3 +649,209 @@ async function exec(command: string): Promise<ExecResult> {
});
});
}
const consoleApiVersion = '2017-08-01-preview';
export enum Errors {
DeploymentOsTypeConflict = 'DeploymentOsTypeConflict'
}
function getConsoleUri(armEndpoint: string) {
return `${armEndpoint}/providers/Microsoft.Portal/consoles/default?api-version=${consoleApiVersion}`;
}
export interface UserSettings {
preferredLocation: string;
preferredOsType: string; // The last OS chosen in the portal.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
storageProfile: any;
}
export interface AccessTokens {
resource: string;
graph: string;
keyVault?: string;
}
export interface ConsoleUris {
consoleUri: string;
terminalUri: string;
socketUri: string;
}
export interface Size {
cols: number;
rows: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function requestWithLogging(requestOptions: request.Options): Promise<any> {
try {
const requestLogger = new HttpLogger(ext.outputChannel, 'CloudConsoleLauncher', new RequestNormalizer());
requestLogger.logRequest(requestOptions);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response: http.IncomingMessage & { body: unknown } = await request(requestOptions);
requestLogger.logResponse({ response, request: requestOptions });
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response;
} catch (e) {
const error = parseError(e);
ext.outputChannel.error({...error, name: 'Request Error: CloudConsoleLauncher'});
}
}
export async function getUserSettings(accessToken: string, armEndpoint: string): Promise<UserSettings | undefined> {
const targetUri = `${armEndpoint}/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=${consoleApiVersion}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await requestWithLogging({
uri: targetUri,
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
simple: false,
resolveWithFullResponse: true,
json: true,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (response.statusCode < 200 || response.statusCode > 299) {
// if (response.body && response.body.error && response.body.error.message) {
// console.log(`${response.body.error.message} (${response.statusCode})`);
// } else {
// console.log(response.statusCode, response.headers, response.body);
// }
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return response.body && response.body.properties;
}
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
export async function provisionConsole(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string): Promise<string> {
let response = await createTerminal(accessToken, armEndpoint, userSettings, osType, true);
for (let i = 0; i < 10; i++ , response = await createTerminal(accessToken, armEndpoint, userSettings, osType, false)) {
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode === 409 && response.body && response.body.error && response.body.error.code === Errors.DeploymentOsTypeConflict) {
throw new Error(Errors.DeploymentOsTypeConflict);
} else if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
const consoleResource = response.body;
if (consoleResource.properties.provisioningState === 'Succeeded') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return consoleResource.properties.uri;
} else if (consoleResource.properties.provisioningState === 'Failed') {
break;
}
}
throw new Error(`Sorry, your Cloud Shell failed to provision. Please retry later. Request correlation id: ${response.headers['x-ms-routing-request-id']}`);
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
async function createTerminal(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string, initial: boolean) {
return requestWithLogging({
uri: getConsoleUri(armEndpoint),
method: initial ? 'PUT' : 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'x-ms-console-preferred-location': userSettings.preferredLocation
},
simple: false,
resolveWithFullResponse: true,
json: true,
body: initial ? {
properties: {
osType
}
} : undefined
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function resetConsole(accessToken: string, armEndpoint: string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await requestWithLogging({
uri: getConsoleUri(armEndpoint),
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
simple: false,
resolveWithFullResponse: true,
json: true
});
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
export async function connectTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size, progress: (i: number) => void): Promise<ConsoleUris> {
for (let i = 0; i < 10; i++) {
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
const response = await initializeTerminal(accessTokens, consoleUri, shellType, initialSize);
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode !== 503 && response.statusCode !== 504 && response.body && response.body.error) {
if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
await delay(1000 * (i + 1));
progress(i + 1);
continue;
}
const { id, socketUri } = response.body;
const terminalUri = `${consoleUri}/terminals/${id}`;
return {
consoleUri,
terminalUri,
socketUri
};
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
}
throw new Error('Failed to connect to the terminal.');
}
async function initializeTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size) {
return requestWithLogging({
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
uri: consoleUri + '/terminals?cols=' + initialSize.cols + '&rows=' + initialSize.rows + '&shell=' + shellType,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${accessTokens.resource}`
},
simple: false,
resolveWithFullResponse: true,
json: true,
body: {
tokens: accessTokens.keyVault ? [accessTokens.graph, accessTokens.keyVault] : [accessTokens.graph]
}
});
}

Просмотреть файл

@ -3,33 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// this file is run as a child process from the extension host and communicates via IPC
import * as http from 'http';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as request from 'request-promise';
import * as WS from 'ws';
import { ext } from '../extensionVariables';
import { HttpLogger } from '../utils/logging/HttpLogger';
import { RequestNormalizer } from '../utils/logging/request/RequestNormalizer';
import { delay } from '../utils/timeUtils';
import { readJSON, sendData } from './ipc';
const consoleApiVersion = '2017-08-01-preview';
export enum Errors {
DeploymentOsTypeConflict = 'DeploymentOsTypeConflict'
}
function getConsoleUri(armEndpoint: string) {
return `${armEndpoint}/providers/Microsoft.Portal/consoles/default?api-version=${consoleApiVersion}`;
}
export interface UserSettings {
preferredLocation: string;
preferredOsType: string; // The last OS chosen in the portal.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
storageProfile: any;
}
export interface AccessTokens {
resource: string;
graph: string;
@ -47,172 +29,6 @@ export interface Size {
rows: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function requestWithLogging(requestOptions: request.Options): Promise<any> {
const requestLogger = new HttpLogger(ext.outputChannel, 'CloudConsoleLauncher', new RequestNormalizer());
requestLogger.logRequest(requestOptions);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response: http.IncomingMessage & { body: unknown } = await request(requestOptions);
requestLogger.logResponse({ response, request: requestOptions });
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response;
}
export async function getUserSettings(accessToken: string, armEndpoint: string): Promise<UserSettings | undefined> {
const targetUri = `${armEndpoint}/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=${consoleApiVersion}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await requestWithLogging({
uri: targetUri,
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
simple: false,
resolveWithFullResponse: true,
json: true,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (response.statusCode < 200 || response.statusCode > 299) {
// if (response.body && response.body.error && response.body.error.message) {
// console.log(`${response.body.error.message} (${response.statusCode})`);
// } else {
// console.log(response.statusCode, response.headers, response.body);
// }
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return response.body && response.body.properties;
}
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
export async function provisionConsole(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string): Promise<string> {
let response = await createTerminal(accessToken, armEndpoint, userSettings, osType, true);
for (let i = 0; i < 10; i++ , response = await createTerminal(accessToken, armEndpoint, userSettings, osType, false)) {
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode === 409 && response.body && response.body.error && response.body.error.code === Errors.DeploymentOsTypeConflict) {
throw new Error(Errors.DeploymentOsTypeConflict);
} else if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
const consoleResource = response.body;
if (consoleResource.properties.provisioningState === 'Succeeded') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return consoleResource.properties.uri;
} else if (consoleResource.properties.provisioningState === 'Failed') {
break;
}
}
throw new Error(`Sorry, your Cloud Shell failed to provision. Please retry later. Request correlation id: ${response.headers['x-ms-routing-request-id']}`);
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
async function createTerminal(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string, initial: boolean) {
return requestWithLogging({
uri: getConsoleUri(armEndpoint),
method: initial ? 'PUT' : 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'x-ms-console-preferred-location': userSettings.preferredLocation
},
simple: false,
resolveWithFullResponse: true,
json: true,
body: initial ? {
properties: {
osType
}
} : undefined
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function resetConsole(accessToken: string, armEndpoint: string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await requestWithLogging({
uri: getConsoleUri(armEndpoint),
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
simple: false,
resolveWithFullResponse: true,
json: true
});
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
export async function connectTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size, progress: (i: number) => void): Promise<ConsoleUris> {
for (let i = 0; i < 10; i++) {
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
const response = await initializeTerminal(accessTokens, consoleUri, shellType, initialSize);
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode !== 503 && response.statusCode !== 504 && response.body && response.body.error) {
if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
await delay(1000 * (i + 1));
progress(i + 1);
continue;
}
const { id, socketUri } = response.body;
const terminalUri = `${consoleUri}/terminals/${id}`;
return {
consoleUri,
terminalUri,
socketUri
};
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
}
throw new Error('Failed to connect to the terminal.');
}
async function initializeTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size) {
return requestWithLogging({
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
uri: consoleUri + '/terminals?cols=' + initialSize.cols + '&rows=' + initialSize.rows + '&shell=' + shellType,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${accessTokens.resource}`
},
simple: false,
resolveWithFullResponse: true,
json: true,
body: {
tokens: accessTokens.keyVault ? [accessTokens.graph, accessTokens.keyVault] : [accessTokens.graph]
}
});
}
function getWindowSize(): Size {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stdout: any = process.stdout;
@ -236,7 +52,7 @@ async function resize(accessTokens: AccessTokens, terminalUri: string) {
const { cols, rows } = getWindowSize();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await requestWithLogging({
const response = await request({
uri: `${terminalUri}/size?cols=${cols}&rows=${rows}`,
method: 'POST',
headers: {
@ -334,10 +150,6 @@ function connectSocket(ipcHandle: string, url: string) {
}
}
async function delay(ms: number) {
return new Promise<void>(resolve => setTimeout(resolve, ms));
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function main() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion