diff --git a/extensions/azurecore/src/account-provider/auths/azureAuth.ts b/extensions/azurecore/src/account-provider/auths/azureAuth.ts index ecab2962e5..b736f7562b 100644 --- a/extensions/azurecore/src/account-provider/auths/azureAuth.ts +++ b/extensions/azurecore/src/account-provider/auths/azureAuth.ts @@ -22,8 +22,7 @@ import { MemoryDatabase } from '../utils/memoryDatabase'; import { Logger } from '../../utils/Logger'; import { AzureAuthError } from './azureAuthError'; import { AccountInfo, AuthError, AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-node'; -import { HttpClient } from './httpClient'; -import { getProxyEnabledHttpClient, getTenantIgnoreList, updateTenantIgnoreList } from '../../utils'; +import { getTenantIgnoreList, updateTenantIgnoreList } from '../../utils'; import { errorToPromptFailedResult } from './networkUtils'; import { MsalCachePluginProvider } from '../utils/msalCachePlugin'; import { isErrorResponseBodyWithError } from '../../azureResource/utils'; @@ -45,7 +44,6 @@ export abstract class AzureAuth implements vscode.Disposable { protected readonly scopesString: string; protected readonly clientId: string; protected readonly resources: Resource[]; - protected readonly httpClient: HttpClient; private readonly _disposableStore: vscode.Disposable[]; constructor( @@ -100,7 +98,6 @@ export abstract class AzureAuth implements vscode.Disposable { this.scopes = [...this.metadata.settings.scopes]; this.scopesString = this.scopes.join(' '); - this.httpClient = getProxyEnabledHttpClient(); this._disposableStore.push(this.uriEventEmitter); } diff --git a/extensions/azurecore/src/account-provider/auths/httpClient.ts b/extensions/azurecore/src/account-provider/auths/httpClient.ts deleted file mode 100644 index c8c20922f8..0000000000 --- a/extensions/azurecore/src/account-provider/auths/httpClient.ts +++ /dev/null @@ -1,413 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureNetworkResponse } from 'azurecore'; -import * as http from 'http'; -import * as https from 'https'; -import { TextEncoder } from 'util'; -import { NetworkRequestOptions, urlToHttpOptions } from './networkUtils'; - -/** - * http methods - */ -export enum HttpMethod { - GET = 'get', - POST = 'post', - PUT = 'put', - DELETE = 'delete', - PATCH = 'patch' -} - -export enum HttpStatus { - SUCCESS_RANGE_START = 200, - SUCCESS_RANGE_END = 299, - REDIRECT = 302, - CLIENT_ERROR_RANGE_START = 400, - CLIENT_ERROR_RANGE_END = 499, - SERVER_ERROR_RANGE_START = 500, - SERVER_ERROR_RANGE_END = 599 -} - -export enum ProxyStatus { - SUCCESS_RANGE_START = 200, - SUCCESS_RANGE_END = 299, - SERVER_ERROR = 500 -} - -/** - * This class implements the API for network requests. - */ -export class HttpClient { - private proxyUrl: string; - private customAgentOptions: http.AgentOptions | https.AgentOptions; - static readonly AUTHORIZATION_PENDING: string = 'authorization_pending'; - - constructor( - proxyUrl?: string, - customAgentOptions?: http.AgentOptions | https.AgentOptions - ) { - this.proxyUrl = proxyUrl || ''; - this.customAgentOptions = customAgentOptions || {}; - } - - /** - * Http Get request - * @param url - * @param options - */ - async sendGetRequestAsync( - url: string, - options?: NetworkRequestOptions, - cancellationToken?: number | undefined - ): Promise> { - if (this.proxyUrl) { - return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.GET, options, this.customAgentOptions as http.AgentOptions, cancellationToken); - } else { - return networkRequestViaHttps(url, HttpMethod.GET, options, this.customAgentOptions as https.AgentOptions, cancellationToken); - } - } - - /** - * Http Post request - * @param url - * @param options - */ - async sendPostRequestAsync( - url: string, - options?: NetworkRequestOptions, - cancellationToken?: number - ): Promise> { - if (this.proxyUrl) { - return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.POST, options, this.customAgentOptions as http.AgentOptions, cancellationToken); - } else { - return networkRequestViaHttps(url, HttpMethod.POST, options, this.customAgentOptions as https.AgentOptions, cancellationToken); - } - } - - /** - * Http Put request - * @param url - * @param options - */ - async sendPutRequestAsync( - url: string, - options?: NetworkRequestOptions, - cancellationToken?: number - ): Promise> { - if (this.proxyUrl) { - return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.PUT, options, this.customAgentOptions as http.AgentOptions, cancellationToken); - } else { - return networkRequestViaHttps(url, HttpMethod.PUT, options, this.customAgentOptions as https.AgentOptions, cancellationToken); - } - } - - /** - * Http Delete request - * @param url - * @param options - */ - async sendDeleteRequestAsync( - url: string, - options?: NetworkRequestOptions - ): Promise> { - if (this.proxyUrl) { - return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.DELETE, options, this.customAgentOptions as http.AgentOptions); - } else { - return networkRequestViaHttps(url, HttpMethod.DELETE, options, this.customAgentOptions as https.AgentOptions); - } - } - - /** - * Http Patch request - * @param url - * @param options - */ - async sendPatchRequestAsync( - url: string, - options?: NetworkRequestOptions, - cancellationToken?: number - ): Promise> { - if (this.proxyUrl) { - return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.PATCH, options, this.customAgentOptions as http.AgentOptions, cancellationToken); - } else { - return networkRequestViaHttps(url, HttpMethod.PATCH, options, this.customAgentOptions as https.AgentOptions, cancellationToken); - } - } - -} - -const networkRequestViaProxy = ( - destinationUrlString: string, - proxyUrlString: string, - httpMethod: string, - options?: NetworkRequestOptions, - agentOptions?: http.AgentOptions, - timeout?: number -): Promise> => { - const destinationUrl = new URL(destinationUrlString); - const proxyUrl = new URL(proxyUrlString); - - // 'method: connect' must be used to establish a connection to the proxy - const headers = options?.headers || {} as Record; - const tunnelRequestOptions: https.RequestOptions = { - host: proxyUrl.hostname, - port: proxyUrl.port, - method: 'CONNECT', - path: destinationUrl.hostname, - headers: headers - }; - - if (timeout) { - tunnelRequestOptions.timeout = timeout; - } - - if (agentOptions && Object.keys(agentOptions).length) { - tunnelRequestOptions.agent = new http.Agent(agentOptions); - } - - // compose a request string for the socket - let postRequestStringContent: string = ''; - if (httpMethod === HttpMethod.POST || httpMethod === HttpMethod.PUT) { - // Note: Text Encoder is necessary here because otherwise it was not able to handle Chinese characters in table names. - const body = (new TextEncoder()).encode(JSON.stringify(options?.body || '')); - postRequestStringContent = - 'Content-Type: application/json\r\n' + - `Content-Length: ${body.length}\r\n` + - `\r\n${body}`; - } - const outgoingRequestString = `${httpMethod.toUpperCase()} ${destinationUrl.href} HTTP/1.1\r\n` + - `Host: ${destinationUrl.host}\r\n` + - 'Connection: close\r\n' + - postRequestStringContent + - '\r\n'; - - return new Promise>(((resolve, reject) => { - const request = http.request(tunnelRequestOptions); - - if (tunnelRequestOptions.timeout) { - request.on('timeout', () => { - request.destroy(); - reject(new Error('Request time out')); - }); - } - - request.end(); - - // establish connection to the proxy - request.on('connect', (response, socket) => { - const proxyStatusCode = response?.statusCode || ProxyStatus.SERVER_ERROR; - if ((proxyStatusCode < ProxyStatus.SUCCESS_RANGE_START) || (proxyStatusCode > ProxyStatus.SUCCESS_RANGE_END)) { - request.destroy(); - socket.destroy(); - reject(new Error(`Error connecting to proxy. Http status code: ${response.statusCode}. Http status message: ${response?.statusMessage || 'Unknown'}`)); - } - if (tunnelRequestOptions.timeout) { - socket.setTimeout(tunnelRequestOptions.timeout); - socket.on('timeout', () => { - request.destroy(); - socket.destroy(); - reject(new Error('Request time out')); - }); - } - - // make a request over an HTTP tunnel - socket.write(outgoingRequestString); - - const data: Buffer[] = []; - socket.on('data', (chunk) => { - data.push(chunk); - }); - - socket.on('end', () => { - // combine all received buffer streams into one buffer, and then into a string - const dataString = Buffer.concat([...data]).toString(); - - // separate each line into it's own entry in an arry - const dataStringArray = dataString.split('\r\n'); - // the first entry will contain the statusCode and statusMessage - const httpStatusCode = parseInt(dataStringArray[0].split(' ')[1], undefined); - // remove 'HTTP/1.1' and the status code to get the status message - const statusMessage = dataStringArray[0].split(' ').slice(2).join(' '); - // the last entry will contain the body - const body = dataStringArray[dataStringArray.length - 1]; - - // everything in between the first and last entries are the headers - const headersArray = dataStringArray.slice(1, dataStringArray.length - 2); - - // build an object out of all the headers - const entries = new Map(); - headersArray.forEach((header) => { - /** - * the header might look like 'Content-Length: 1531', but that is just a string - * it needs to be converted to a key/value pair - * split the string at the first instance of ':' - * there may be more than one ':' if the value of the header is supposed to be a JSON object - */ - const headerKeyValue = header.split(new RegExp(/:\s(.*)/s)); - entries.set(headerKeyValue[0], headerKeyValue[1]); - }); - - const parsedHeaders = Object.fromEntries(entries) as Record; - const networkResponse: AzureNetworkResponse = { - headers: parsedHeaders, - data: parseBody(httpStatusCode, statusMessage, parsedHeaders, body) as T, - status: httpStatusCode - }; - - if (((httpStatusCode < HttpStatus.SUCCESS_RANGE_START) || (httpStatusCode > HttpStatus.SUCCESS_RANGE_END)) && - // do not destroy the request for the device code flow - // @ts-ignore - networkResponse.data['error'] !== HttpClient.AUTHORIZATION_PENDING) { - request.destroy(); - } - resolve(networkResponse); - }); - - socket.on('error', (chunk) => { - request.destroy(); - socket.destroy(); - reject(new Error(chunk.toString())); - }); - }); - - request.on('error', (chunk) => { - request.destroy(); - reject(new Error(chunk.toString())); - }); - })); -}; - -const networkRequestViaHttps = ( - urlString: string, - httpMethod: string, - options?: NetworkRequestOptions, - agentOptions?: https.AgentOptions, - timeout?: number -): Promise> => { - const isPostRequest = httpMethod === HttpMethod.POST; - const isPutRequest = httpMethod === HttpMethod.PUT; - const isPatchRequest = httpMethod === HttpMethod.PATCH; - // Note: Text Encoder is necessary here because otherwise it was not able to handle Chinese characters in table names. - const body = (new TextEncoder()).encode(options?.body || ''); - const url = new URL(urlString); - const optionHeaders = options?.headers || {} as Record; - let customOptions: https.RequestOptions = { - method: httpMethod, - headers: optionHeaders, - ...urlToHttpOptions(url) - }; - - if (timeout) { - customOptions.timeout = timeout; - } - - if (agentOptions && Object.keys(agentOptions).length) { - customOptions.agent = new https.Agent(agentOptions); - } - - if (isPostRequest || isPutRequest || isPatchRequest) { - // needed for post request to work - customOptions.headers = { - ...customOptions.headers, - 'Content-Length': body.length - }; - } - - return new Promise>((resolve, reject) => { - const request = https.request(customOptions); - - if (timeout) { - request.on('timeout', () => { - request.destroy(); - reject(new Error('Request timed out')); - }); - } - - if (isPostRequest || isPutRequest || isPatchRequest) { - request.write(body); - } - - request.end(); - - request.on('response', (response) => { - const headers = response.headers; - const statusCode = response.statusCode as number; - const statusMessage = response.statusMessage; - - const data: Buffer[] = []; - response.on('data', (chunk) => { - data.push(chunk); - }); - - response.on('end', () => { - // combine all received buffer streams into one buffer, and then into a string - const dataBody = Buffer.concat([...data]).toString(); - - const parsedHeaders = headers as Record; - const networkResponse: AzureNetworkResponse = { - headers: parsedHeaders, - data: parseBody(statusCode, statusMessage, parsedHeaders, dataBody) as T, - status: statusCode - }; - - if (((statusCode < HttpStatus.SUCCESS_RANGE_START) || (statusCode > HttpStatus.SUCCESS_RANGE_END)) && - // do not destroy the request for the device code flow - // @ts-ignore - networkResponse.data['error'] !== HttpClient.AUTHORIZATION_PENDING) { - request.destroy(); - } - resolve(networkResponse); - }); - }); - - request.on('error', (chunk) => { - request.destroy(); - reject(new Error(chunk.toString())); - }); - }); -}; - -/** - * Check if extra parsing is needed on the response from the server - * @param statusCode {number} the status code of the response from the server - * @param statusMessage {string | undefined} the status message of the response from the server - * @param headers {Record} the headers of the response from the server - * @param body {string} the body from the response of the server - * @returns JSON parsed body or error object - */ -const parseBody = (statusCode: number, statusMessage: string | undefined, headers: Record, body: string) => { - /* - * Informational responses (100 - 199) - * Successful responses (200 - 299) - * Redirection messages (300 - 399) - * Client error responses (400 - 499) - * Server error responses (500 - 599) - */ - - let parsedBody: unknown; - try { - parsedBody = JSON.parse(body); - } catch (error) { - let errorType; - let errorDescriptionHelper; - if ((statusCode >= HttpStatus.CLIENT_ERROR_RANGE_START) && (statusCode <= HttpStatus.CLIENT_ERROR_RANGE_END)) { - errorType = 'client_error'; - errorDescriptionHelper = 'A client'; - } else if ((statusCode >= HttpStatus.SERVER_ERROR_RANGE_START) && (statusCode <= HttpStatus.SERVER_ERROR_RANGE_END)) { - errorType = 'server_error'; - errorDescriptionHelper = 'A server'; - } else { - errorType = 'unknown_error'; - errorDescriptionHelper = 'An unknown'; - } - - parsedBody = { - error: errorType, - error_description: `${errorDescriptionHelper} error occurred.\nHttp status code: ${statusCode}\nHttp status message: ${statusMessage || 'Unknown'}\nHeaders: ${JSON.stringify(headers)}` - }; - } - - return parsedBody; -}; diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts index dd11334691..773a9837ac 100644 --- a/extensions/azurecore/src/azureResource/utils.ts +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -9,7 +9,7 @@ import * as nls from 'vscode-nls'; import * as Constants from '../constants'; import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { TokenCredentials } from '@azure/ms-rest-js'; -import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult, CreateResourceGroupResult, GetBlobsResult, GetStorageAccountAccessKeyResult, AzureAccount, azureResource, AzureAccountProviderMetadata, AzureNetworkResponse } from 'azurecore'; +import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult, CreateResourceGroupResult, GetBlobsResult, GetStorageAccountAccessKeyResult, AzureAccount, azureResource, AzureAccountProviderMetadata } from 'azurecore'; import { EOL } from 'os'; import { AppContext } from '../appContext'; import { invalidAzureAccount, invalidTenant, unableToFetchTokenError } from '../localizedConstants'; @@ -18,9 +18,6 @@ import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionServ import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService'; import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob'; import providerSettings from '../account-provider/providerSettings'; -import { getProxyEnabledHttpClient } from '../utils'; -import { HttpClient } from '../account-provider/auths/httpClient'; -import { NetworkRequestOptions } from '@azure/msal-common'; import { ErrorResponseBody } from '@azure/arm-subscriptions/esm/models'; import { TenantIgnoredError } from '../utils/TenantIgnoredError'; import { AzureMonitorResourceService } from './providers/azuremonitor/azuremonitorService'; @@ -52,6 +49,7 @@ import { PostgresFlexibleServerTreeDataProvider } from './providers/postgresFlex import { PostgresFlexibleServerService } from './providers/postgresFlexibleServer/postgresFlexibleServerService'; import { CosmosDbPostgresTreeDataProvider } from './providers/cosmosdb/postgres/cosmosDbPostgresTreeDataProvider'; import { CosmosDbPostgresService } from './providers/cosmosdb/postgres/cosmosDbPostgresService'; +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; const localize = nls.loadMessageBundle(); @@ -463,7 +461,6 @@ export async function makeHttpRequest( requestHeaders: Record = {} ): Promise> { const result: AzureRestResponse = { response: undefined, errors: [] }; - const httpClient: HttpClient = getProxyEnabledHttpClient(); if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) { const error = new Error(invalidAzureAccount); @@ -509,10 +506,9 @@ export async function makeHttpRequest( ...requestHeaders } - const body = JSON.stringify(requestBody || ''); - let networkRequestOptions: NetworkRequestOptions = { + const config: AxiosRequestConfig = { headers: reqHeaders, - body + validateStatus: () => true // Never throw }; // Adding '/' if path does not begin with it. @@ -527,26 +523,22 @@ export async function makeHttpRequest( requestUrl = `${account.properties.providerSettings.settings.armResource.endpoint}${path}`; } - let response: AzureNetworkResponse | undefined = undefined; + let response: AxiosResponse | undefined; switch (requestType) { case HttpRequestMethod.GET: - response = await httpClient.sendGetRequestAsync(requestUrl, { - headers: reqHeaders - }); + response = await axios.get(requestUrl, config); break; case HttpRequestMethod.POST: - response = await httpClient.sendPostRequestAsync(requestUrl, networkRequestOptions); + response = await axios.post(requestUrl, requestBody, config); break; case HttpRequestMethod.PUT: - response = await httpClient.sendPutRequestAsync(requestUrl, networkRequestOptions); + response = await axios.put(requestUrl, requestBody, config); break; case HttpRequestMethod.DELETE: - response = await httpClient.sendDeleteRequestAsync(requestUrl, { - headers: reqHeaders - }); + response = await axios.delete(requestUrl, config); break; case HttpRequestMethod.PATCH: - response = await httpClient.sendPatchRequestAsync(requestUrl, networkRequestOptions); + response = await axios.patch(requestUrl, config); break; default: const error = new Error(`Unknown RequestType "${requestType}"`); @@ -576,7 +568,7 @@ export async function makeHttpRequest( result.errors.push(error); } else { // We know this isn't an error response at this point - result.response = response as AzureNetworkResponse; + result.response = response as AxiosResponse; } return result; diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index ee7cddae7e..4c87d6a9ab 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -8,6 +8,7 @@ declare module 'azurecore' { import * as vscode from 'vscode'; import * as msRest from '@azure/ms-rest-js'; import { BlobItem } from '@azure/storage-blob'; + import { AxiosResponse } from 'axios'; /** * Covers defining what the azurecore extension exports to other extensions @@ -273,17 +274,6 @@ declare module 'azurecore' { PATCH } - /** - * Custom version of NetworkResponse from @azure\msal-common\dist\network\NetworkManager.d.ts - * with body renamed to data to avoid breaking changes with extensions. See - * https://github.com/microsoft/azuredatastudio/pull/22761 for details. - */ - export type AzureNetworkResponse = { - headers: Record; - data: T; - status: number; - }; - export interface IExtension { /** * Gets the list of subscriptions for the specified AzureAccount @@ -350,7 +340,7 @@ declare module 'azurecore' { export type GetFileSharesResult = { fileShares: azureResource.FileShare[], errors: Error[] }; export type CreateResourceGroupResult = { resourceGroup: azureResource.AzureResourceResourceGroup | undefined, errors: Error[] }; export type ResourceQueryResult = { resources: T[], errors: Error[] }; - export type AzureRestResponse = { response: AzureNetworkResponse | undefined, errors: Error[] }; + export type AzureRestResponse = { response: AxiosResponse | undefined, errors: Error[] }; export type GetBlobsResult = { blobs: azureResource.Blob[], errors: Error[] }; export type GetStorageAccountAccessKeyResult = { keyName1: string, keyName2: string, errors: Error[] }; export type CacheEncryptionKeys = { key: string; iv: string; } diff --git a/extensions/azurecore/src/utils.ts b/extensions/azurecore/src/utils.ts index 8a564df27e..0e1b9f5e3b 100644 --- a/extensions/azurecore/src/utils.ts +++ b/extensions/azurecore/src/utils.ts @@ -10,19 +10,12 @@ import * as constants from './constants'; import { AzureRegion, azureResource } from 'azurecore'; import { AppContext } from './appContext'; -import { HttpClient } from './account-provider/auths/httpClient'; -import { parse } from 'url'; -import { getProxyAgentOptions } from './proxy'; -import { HttpsProxyAgentOptions } from 'https-proxy-agent'; import { ProviderSettings, ProviderSettingsJson, SettingIds } from './account-provider/interfaces'; import { AzureResource } from 'azdata'; import { Logger } from './utils/Logger'; import { TelemetryAction, TelemetryReporter, TelemetryViews } from './telemetry'; const localize = nls.loadMessageBundle(); -const configProxy = 'proxy'; -const configProxyStrictSSL = 'proxyStrictSSL'; -const configProxyAuthorization = 'proxyAuthorization'; /** * Converts a region value (@see AzureRegion) into the localized Display Name @@ -145,10 +138,6 @@ export function getResourceTypeDisplayName(type: string): string { return type; } -function getHttpConfiguration(): vscode.WorkspaceConfiguration { - return vscode.workspace.getConfiguration(constants.httpConfigSectionName); -} - /** * Gets tenants to be ignored. * @returns Tenants configured in ignore list @@ -303,25 +292,6 @@ export interface IPackageInfo { aiKey: string; } -export function getProxyEnabledHttpClient(): HttpClient { - const proxy = getHttpConfiguration().get(configProxy); - const strictSSL = getHttpConfiguration().get(configProxyStrictSSL, true); - const authorization = getHttpConfiguration().get(configProxyAuthorization); - - const url = parse(proxy); - let agentOptions = getProxyAgentOptions(url, proxy, strictSSL); - - if (authorization && url.protocol === 'https:') { - let httpsAgentOptions = agentOptions as HttpsProxyAgentOptions; - httpsAgentOptions!.headers = Object.assign(httpsAgentOptions!.headers || {}, { - 'Proxy-Authorization': authorization - }); - agentOptions = httpsAgentOptions; - } - - return new HttpClient(proxy, agentOptions); -} - /** * Display notification with button to reload * @param sectionName Name of section to reload