Remove HttpClient and Proxy agent options completely (#17828)
* change to axios call * fix lint error * fix linter * Remove Http Proxy agent options completely --------- Co-authored-by: Christopher Suh <chris.s.suh@gmail.com>
This commit is contained in:
Родитель
6f003d76e1
Коммит
68dbcda9ac
|
@ -116,8 +116,7 @@
|
|||
"@azure/msal-common": "^11.0.0",
|
||||
"@azure/msal-node": "^1.16.0",
|
||||
"@microsoft/ads-extension-telemetry": "^3.0.2",
|
||||
"http-proxy-agent": "5.0.0",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"axios": "^0.27.2",
|
||||
"core-js": "^2.4.1",
|
||||
"decompress-zip": "^0.2.2",
|
||||
"ejs": "^3.1.7",
|
||||
|
|
|
@ -1,343 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { INetworkModule, NetworkRequestOptions, NetworkResponse } from '@azure/msal-common';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { constants, HttpMethod, HttpStatus, ProxyStatus } from '../constants';
|
||||
import { NetworkUtils } from './networkUtils';
|
||||
|
||||
/**
|
||||
* This class implements the API for network requests.
|
||||
*/
|
||||
export class HttpClient implements INetworkModule {
|
||||
private proxyUrl: string;
|
||||
private customAgentOptions: http.AgentOptions | https.AgentOptions;
|
||||
|
||||
constructor(
|
||||
proxyUrl?: string,
|
||||
customAgentOptions?: http.AgentOptions | https.AgentOptions
|
||||
) {
|
||||
this.proxyUrl = proxyUrl || '';
|
||||
this.customAgentOptions = customAgentOptions || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Http Get request
|
||||
* @param url
|
||||
* @param options
|
||||
*/
|
||||
async sendGetRequestAsync<T>(
|
||||
url: string,
|
||||
options?: NetworkRequestOptions
|
||||
): Promise<NetworkResponse<T>> {
|
||||
if (this.proxyUrl) {
|
||||
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.GET, options, this.customAgentOptions as http.AgentOptions);
|
||||
} else {
|
||||
return networkRequestViaHttps(url, HttpMethod.GET, options, this.customAgentOptions as https.AgentOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Http Post request
|
||||
* @param url
|
||||
* @param options
|
||||
*/
|
||||
async sendPostRequestAsync<T>(
|
||||
url: string,
|
||||
options?: NetworkRequestOptions,
|
||||
cancellationToken?: number
|
||||
): Promise<NetworkResponse<T>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const networkRequestViaProxy = <T>(
|
||||
destinationUrlString: string,
|
||||
proxyUrlString: string,
|
||||
httpMethod: string,
|
||||
options?: NetworkRequestOptions,
|
||||
agentOptions?: http.AgentOptions,
|
||||
timeout?: number
|
||||
): Promise<NetworkResponse<T>> => {
|
||||
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<string, string>;
|
||||
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) {
|
||||
const body = options?.body || '';
|
||||
postRequestStringContent =
|
||||
'Content-Type: application/x-www-form-urlencoded\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<NetworkResponse<T>>(((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) {
|
||||
// tslint:disable-next-line no-string-based-set-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));
|
||||
const headerKey = headerKeyValue[0];
|
||||
let headerValue = headerKeyValue[1];
|
||||
|
||||
// check if the value of the header is supposed to be a JSON object
|
||||
try {
|
||||
const object = JSON.parse(headerValue);
|
||||
|
||||
// if it is, then convert it from a string to a JSON object
|
||||
if (object && (typeof object === 'object')) {
|
||||
headerValue = object;
|
||||
}
|
||||
} catch (e) {
|
||||
// otherwise, leave it as a string
|
||||
}
|
||||
|
||||
entries.set(headerKey, headerValue);
|
||||
});
|
||||
|
||||
const parsedHeaders = Object.fromEntries(entries) as Record<string, string>;
|
||||
const networkResponse = NetworkUtils.getNetworkResponse(
|
||||
parsedHeaders,
|
||||
parseBody(httpStatusCode, statusMessage, parsedHeaders, body) as T,
|
||||
httpStatusCode
|
||||
);
|
||||
|
||||
if (((httpStatusCode < HttpStatus.SUCCESS_RANGE_START) || (httpStatusCode > HttpStatus.SUCCESS_RANGE_END)) &&
|
||||
// do not destroy the request for the device code flow
|
||||
networkResponse.body['error'] !== constants.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 = <T>(
|
||||
urlString: string,
|
||||
httpMethod: string,
|
||||
options?: NetworkRequestOptions,
|
||||
agentOptions?: https.AgentOptions,
|
||||
timeout?: number
|
||||
): Promise<NetworkResponse<T>> => {
|
||||
const isPostRequest = httpMethod === HttpMethod.POST;
|
||||
const body: string = options?.body || '';
|
||||
const url = new URL(urlString);
|
||||
const optionHeaders = options?.headers || {} as Record<string, string>;
|
||||
let customOptions: https.RequestOptions = {
|
||||
method: httpMethod,
|
||||
headers: optionHeaders,
|
||||
...NetworkUtils.urlToHttpOptions(url)
|
||||
};
|
||||
|
||||
if (timeout) {
|
||||
customOptions.timeout = timeout;
|
||||
}
|
||||
|
||||
if (agentOptions && Object.keys(agentOptions).length) {
|
||||
customOptions.agent = new https.Agent(agentOptions);
|
||||
}
|
||||
|
||||
if (isPostRequest) {
|
||||
// needed for post request to work
|
||||
customOptions.headers = {
|
||||
...customOptions.headers,
|
||||
'Content-Length': body.length
|
||||
};
|
||||
}
|
||||
|
||||
return new Promise<NetworkResponse<T>>((resolve, reject) => {
|
||||
const request = https.request(customOptions);
|
||||
|
||||
if (timeout) {
|
||||
request.on('timeout', () => {
|
||||
request.destroy();
|
||||
reject(new Error('Request time out'));
|
||||
});
|
||||
}
|
||||
|
||||
if (isPostRequest) {
|
||||
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<string, string>;
|
||||
const networkResponse = NetworkUtils.getNetworkResponse(
|
||||
parsedHeaders,
|
||||
parseBody(statusCode, statusMessage, parsedHeaders, dataBody) as T,
|
||||
statusCode
|
||||
);
|
||||
|
||||
if (((statusCode < HttpStatus.SUCCESS_RANGE_START) || (statusCode > HttpStatus.SUCCESS_RANGE_END)) &&
|
||||
// do not destroy the request for the device code flow
|
||||
networkResponse.body['error'] !== constants.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 repsonse 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<string, string>} the headers of the response from the server
|
||||
* @param body {string} the body from the response of the server
|
||||
* @returns {Object} JSON parsed body or error object
|
||||
*/
|
||||
const parseBody = (statusCode: number, statusMessage: string | undefined, headers: Record<string, string>, 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;
|
||||
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 occured.\nHttp status code: ${statusCode}\nHttp status message: ${statusMessage || 'Unknown'}\nHeaders: ${JSON.stringify(headers)}`
|
||||
};
|
||||
}
|
||||
|
||||
return parsedBody;
|
||||
};
|
|
@ -15,8 +15,13 @@ import { Logger } from '../../models/logger';
|
|||
import * as Utils from '../../models/utils';
|
||||
import { AzureAuthError } from '../azureAuthError';
|
||||
import * as Constants from '../constants';
|
||||
import * as azureUtils from '../utils';
|
||||
import { HttpClient } from './httpClient';
|
||||
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
|
||||
import { ErrorResponseBody } from '@azure/arm-subscriptions';
|
||||
|
||||
export type GetTenantsResponseData = {
|
||||
value: ITenantResponse[];
|
||||
};
|
||||
export type ErrorResponseBodyWithError = Required<ErrorResponseBody>;
|
||||
|
||||
// tslint:disable:no-null-keyword
|
||||
export abstract class MsalAzureAuth {
|
||||
|
@ -26,7 +31,6 @@ export abstract class MsalAzureAuth {
|
|||
protected readonly scopesString: string;
|
||||
protected readonly clientId: string;
|
||||
protected readonly resources: Resource[];
|
||||
protected readonly httpClient: HttpClient;
|
||||
|
||||
constructor(
|
||||
protected readonly providerSettings: IProviderSettings,
|
||||
|
@ -41,7 +45,6 @@ export abstract class MsalAzureAuth {
|
|||
this.clientId = this.providerSettings.clientId;
|
||||
this.scopes = [...this.providerSettings.scopes];
|
||||
this.scopesString = this.scopes.join(' ');
|
||||
this.httpClient = azureUtils.getProxyEnabledHttpClient();
|
||||
}
|
||||
|
||||
public async startLogin(): Promise<IAccount | IPromptFailedResult> {
|
||||
|
@ -241,14 +244,9 @@ export abstract class MsalAzureAuth {
|
|||
try {
|
||||
this.logger.verbose('Fetching tenants with uri {0}', tenantUri);
|
||||
let tenantList: string[] = [];
|
||||
const tenantResponse = await this.httpClient.sendGetRequestAsync<any>(tenantUri, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data = tenantResponse.body;
|
||||
if (data.error) {
|
||||
const tenantResponse = await this.makeGetRequest<GetTenantsResponseData>(tenantUri, token);
|
||||
const data = tenantResponse.data;
|
||||
if (this.isErrorResponseBodyWithError(data)) {
|
||||
this.logger.error(`Error fetching tenants :${data.error.code} - ${data.error.message}`);
|
||||
throw new Error(`${data.error.code} - ${data.error.message}`);
|
||||
}
|
||||
|
@ -281,6 +279,24 @@ export abstract class MsalAzureAuth {
|
|||
}
|
||||
}
|
||||
|
||||
private isErrorResponseBodyWithError(body: any): body is ErrorResponseBodyWithError {
|
||||
return 'error' in body && body.error;
|
||||
}
|
||||
|
||||
private async makeGetRequest<T>(uri: string, token: string): Promise<AxiosResponse<T>> {
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
validateStatus: () => true // Never throw
|
||||
};
|
||||
|
||||
const response: AxiosResponse = await axios.get<T>(uri, config);
|
||||
this.logger.piiSanitized('GET request ', [{ name: 'response', objOrArray: response.data?.value as ITenantResponse[] ?? response.data as GetTenantsResponseData }], [], uri);
|
||||
return response;
|
||||
}
|
||||
|
||||
//#region interaction handling
|
||||
public async handleInteractionRequired(tenant: ITenant, settings: IAADResource, promptUser: boolean = true): Promise<AuthenticationResult | null> {
|
||||
let shouldOpen: boolean;
|
||||
|
|
|
@ -12,7 +12,6 @@ import { AzureAuthType, IAADResource, IAccount, IToken } from '../../models/cont
|
|||
import { AccountStore } from '../accountStore';
|
||||
import { AzureController } from '../azureController';
|
||||
import { getAzureActiveDirectoryConfig, getEnableSqlAuthenticationProviderConfig } from '../utils';
|
||||
import { HttpClient } from './httpClient';
|
||||
import { MsalAzureAuth } from './msalAzureAuth';
|
||||
import { MsalAzureCodeGrant } from './msalAzureCodeGrant';
|
||||
import { MsalAzureDeviceCode } from './msalAzureDeviceCode';
|
||||
|
@ -113,7 +112,7 @@ export class MsalAzureController extends AzureController {
|
|||
public async getAccountSecurityToken(account: IAccount, tenantId: string, settings: IAADResource): Promise<IToken | undefined> {
|
||||
let azureAuth = await this.getAzureAuthInstance(getAzureActiveDirectoryConfig());
|
||||
if (azureAuth) {
|
||||
this.logger.piiSantized(`Getting account security token for ${JSON.stringify(account?.key)} (tenant ${tenantId}). Auth Method = ${AzureAuthType[account?.properties.azureAuthType]}`, [], []);
|
||||
this.logger.piiSanitized(`Getting account security token for ${JSON.stringify(account?.key)} (tenant ${tenantId}). Auth Method = ${AzureAuthType[account?.properties.azureAuthType]}`, [], []);
|
||||
tenantId = tenantId || account.properties.owningTenant.id;
|
||||
let result = await azureAuth.getToken(account, tenantId, settings);
|
||||
if (!result || !result.account || !result.account.idTokenClaims) {
|
||||
|
@ -229,8 +228,7 @@ export class MsalAzureController extends AzureController {
|
|||
loggerCallback: this.getLoggerCallback(),
|
||||
logLevel: MsalLogLevel.Trace,
|
||||
piiLoggingEnabled: true
|
||||
},
|
||||
networkClient: new HttpClient()
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
cachePlugin: this._cachePluginProvider?.getCachePlugin()
|
||||
|
|
|
@ -7,23 +7,15 @@ import { ResourceManagementClient } from '@azure/arm-resources';
|
|||
import { SqlManagementClient } from '@azure/arm-sql';
|
||||
import { SubscriptionClient } from '@azure/arm-subscriptions';
|
||||
import { PagedAsyncIterableIterator } from '@azure/core-paging';
|
||||
import { HttpsProxyAgentOptions } from 'https-proxy-agent';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { parse } from 'url';
|
||||
import * as vscode from 'vscode';
|
||||
import { getProxyAgentOptions } from '../languageservice/proxy';
|
||||
import { AzureAuthType, IToken } from '../models/contracts/azure';
|
||||
import * as Constants from './constants';
|
||||
import { TokenCredentialWrapper } from './credentialWrapper';
|
||||
import { HttpClient } from './msal/httpClient';
|
||||
|
||||
const configAzureAD = 'azureActiveDirectory';
|
||||
|
||||
const configProxy = 'proxy';
|
||||
const configProxyStrictSSL = 'proxyStrictSSL';
|
||||
const configProxyAuthorization = 'proxyAuthorization';
|
||||
|
||||
/**
|
||||
* Helper method to convert azure results that comes as pages to an array
|
||||
* @param pages azure resources as pages
|
||||
|
@ -59,9 +51,6 @@ function getConfiguration(): vscode.WorkspaceConfiguration {
|
|||
return vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
|
||||
}
|
||||
|
||||
function getHttpConfiguration(): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(Constants.httpConfigSectionName);
|
||||
}
|
||||
export function getAzureActiveDirectoryConfig(): AzureAuthType {
|
||||
let config = getConfiguration();
|
||||
if (config) {
|
||||
|
@ -96,25 +85,6 @@ export function getEnableConnectionPoolingConfig(): boolean {
|
|||
return true; // default setting
|
||||
}
|
||||
|
||||
export function getProxyEnabledHttpClient(): HttpClient {
|
||||
const proxy = <string>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);
|
||||
}
|
||||
|
||||
export function getAppDataPath(): string {
|
||||
let platform = process.platform;
|
||||
switch (platform) {
|
||||
|
|
|
@ -9,7 +9,6 @@ import * as https from 'https';
|
|||
import { parse as parseUrl, Url } from 'url';
|
||||
import { ILogger } from '../models/interfaces';
|
||||
import { IHttpClient, IPackage, IStatusView, PackageError } from './interfaces';
|
||||
import { getProxyAgent, isBoolean } from './proxy';
|
||||
|
||||
/*
|
||||
* Http client class to handle downloading files using http or https urls
|
||||
|
@ -23,12 +22,9 @@ export default class HttpClient implements IHttpClient {
|
|||
urlString: string,
|
||||
pkg: IPackage,
|
||||
logger: ILogger,
|
||||
statusView: IStatusView,
|
||||
proxy?: string,
|
||||
strictSSL?: boolean,
|
||||
authorization?: string): Promise<void> {
|
||||
statusView: IStatusView): Promise<void> {
|
||||
const url = parseUrl(urlString);
|
||||
let options = this.getHttpClientOptions(url, proxy, strictSSL, authorization);
|
||||
let options = this.getHttpClientOptions(url);
|
||||
let clientRequest = url.protocol === 'http:' ? http.request : https.request;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
@ -39,7 +35,7 @@ export default class HttpClient implements IHttpClient {
|
|||
let request = clientRequest(options, response => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
// Redirect - download from new location
|
||||
return resolve(this.downloadFile(response.headers.location!, pkg, logger, statusView, proxy, strictSSL, authorization));
|
||||
return resolve(this.downloadFile(response.headers.location!, pkg, logger, statusView));
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
|
@ -65,27 +61,22 @@ export default class HttpClient implements IHttpClient {
|
|||
});
|
||||
}
|
||||
|
||||
private getHttpClientOptions(url: Url, proxy?: string, strictSSL?: boolean, authorization?: string): any {
|
||||
const agent = getProxyAgent(url, proxy, strictSSL);
|
||||
|
||||
private getHttpClientOptions(url: Url): any {
|
||||
let options: http.RequestOptions = {
|
||||
host: url.hostname,
|
||||
path: url.path,
|
||||
agent: agent
|
||||
agent: undefined
|
||||
};
|
||||
|
||||
if (url.protocol === 'https:') {
|
||||
let httpsOptions: https.RequestOptions = {
|
||||
host: url.hostname,
|
||||
path: url.path,
|
||||
agent: agent,
|
||||
rejectUnauthorized: isBoolean(strictSSL) ? strictSSL : true
|
||||
agent: undefined,
|
||||
rejectUnauthorized: true
|
||||
};
|
||||
options = httpsOptions;
|
||||
}
|
||||
if (authorization) {
|
||||
options.headers = Object.assign(options.headers || {}, { 'Proxy-Authorization': authorization });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export class PackageError extends Error {
|
|||
}
|
||||
|
||||
export interface IHttpClient {
|
||||
downloadFile(urlString: string, pkg: IPackage, logger: ILogger, statusView: IStatusView, proxy: string, strictSSL: boolean, authorization?: string):
|
||||
downloadFile(urlString: string, pkg: IPackage, logger: ILogger, statusView: IStatusView):
|
||||
Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { HttpProxyAgent, HttpProxyAgentOptions } from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent';
|
||||
import { parse as parseUrl, Url } from 'url';
|
||||
|
||||
function getSystemProxyURL(requestURL: Url): string | undefined {
|
||||
if (requestURL.protocol === 'http:') {
|
||||
return process.env.HTTP_PROXY || process.env.http_proxy || undefined;
|
||||
} else if (requestURL.protocol === 'https:') {
|
||||
return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isBoolean(obj: any): obj is boolean {
|
||||
return obj === true || obj === false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the proxy agent using the proxy url in the parameters or the system proxy. Returns null if no proxy found
|
||||
*/
|
||||
export function getProxyAgent(requestURL: Url, proxy?: string, strictSSL?: boolean): HttpsProxyAgent | HttpProxyAgent {
|
||||
const proxyURL = proxy || getSystemProxyURL(requestURL);
|
||||
if (!proxyURL) {
|
||||
return undefined;
|
||||
}
|
||||
const proxyEndpoint = parseUrl(proxyURL);
|
||||
const opts = getProxyAgentOptions(requestURL, proxy, strictSSL);
|
||||
return proxyEndpoint.protocol === 'https:' ? new HttpsProxyAgent(opts as HttpsProxyAgentOptions) : new HttpProxyAgent(opts as HttpProxyAgentOptions);
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the proxy agent using the proxy url in the parameters or the system proxy. Returns null if no proxy found
|
||||
*/
|
||||
export function getProxyAgentOptions(requestURL: Url, proxy?: string, strictSSL?: boolean): HttpsProxyAgentOptions | HttpProxyAgentOptions {
|
||||
const proxyURL = proxy || getSystemProxyURL(requestURL);
|
||||
|
||||
if (!proxyURL) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const proxyEndpoint = parseUrl(proxyURL);
|
||||
|
||||
if (!/^https?:$/.test(proxyEndpoint.protocol!)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const opts: HttpsProxyAgentOptions | HttpProxyAgentOptions = {
|
||||
host: proxyEndpoint.hostname,
|
||||
port: Number(proxyEndpoint.port),
|
||||
auth: proxyEndpoint.auth,
|
||||
rejectUnauthorized: isBoolean(strictSSL) ? strictSSL : true
|
||||
};
|
||||
|
||||
return opts;
|
||||
}
|
|
@ -89,10 +89,6 @@ export default class ServiceDownloadProvider {
|
|||
* Downloads the SQL tools service and decompress it in the install folder.
|
||||
*/
|
||||
public async installSQLToolsService(platform: Runtime): Promise<boolean> {
|
||||
const proxy = <string>this._config.getWorkspaceConfig('http.proxy');
|
||||
const strictSSL = this._config.getWorkspaceConfig('http.proxyStrictSSL', true);
|
||||
const authorization = this._config.getWorkspaceConfig('http.proxyAuthorization');
|
||||
|
||||
const fileName = this.getDownloadFileName(platform);
|
||||
const installDirectory = await this.getOrMakeInstallDirectory(platform);
|
||||
|
||||
|
@ -112,7 +108,7 @@ export default class ServiceDownloadProvider {
|
|||
pkg.tmpFile = tmpResult;
|
||||
|
||||
try {
|
||||
await this._httpClient.downloadFile(pkg.url, pkg, this._logger, this._statusView, proxy, strictSSL, authorization);
|
||||
await this._httpClient.downloadFile(pkg.url, pkg, this._logger, this._statusView);
|
||||
this._logger.logDebug(`Downloaded to ${pkg.tmpFile.name}...`);
|
||||
this._logger.appendLine(' Done!');
|
||||
await this.install(pkg);
|
||||
|
@ -149,6 +145,3 @@ export default class ServiceDownloadProvider {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ export class Logger implements ILogger {
|
|||
* @param stringsToShorten Set of strings to shorten
|
||||
* @param vals Any other values to add on to the end of the log message
|
||||
*/
|
||||
public piiSantized(msg: any, objsToSanitize: { name: string, objOrArray: any | any[] }[],
|
||||
public piiSanitized(msg: any, objsToSanitize: { name: string, objOrArray: any | any[] }[],
|
||||
stringsToShorten: { name: string, value: string }[], ...vals: any[]): void {
|
||||
if (this.piiLogging) {
|
||||
msg = [
|
||||
|
|
|
@ -119,9 +119,6 @@ suite('ServiceDownloadProvider Tests', () => {
|
|||
config.setup(x => x.getSqlToolsConfigValue('downloadFileNames')).returns(() => fileNamesJson);
|
||||
config.setup(x => x.getSqlToolsServiceDownloadUrl()).returns(() => baseDownloadUrl);
|
||||
config.setup(x => x.getSqlToolsPackageVersion()).returns(() => version);
|
||||
config.setup(x => x.getWorkspaceConfig('http.proxy')).returns(() => <any>'proxy');
|
||||
config.setup(x => x.getWorkspaceConfig('http.proxyStrictSSL', true)).returns(() => <any>true);
|
||||
config.setup(x => x.getWorkspaceConfig('http.proxyAuthorization')).returns(() => '');
|
||||
testStatusView.setup(x => x.installingService());
|
||||
testStatusView.setup(x => x.serviceInstalled());
|
||||
testLogger.setup(x => x.append(TypeMoq.It.isAny()));
|
||||
|
@ -129,8 +126,7 @@ suite('ServiceDownloadProvider Tests', () => {
|
|||
|
||||
testDecompressProvider.setup(x => x.decompress(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => { return fixture.decompressResult; });
|
||||
testHttpClient.setup(x => x.downloadFile(downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(),
|
||||
TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
testHttpClient.setup(x => x.downloadFile(downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => { return fixture.downloadResult; });
|
||||
let downloadProvider = new ServiceDownloadProvider(config.object, testLogger.object, testStatusView.object,
|
||||
testHttpClient.object, testDecompressProvider.object);
|
||||
|
@ -149,8 +145,7 @@ suite('ServiceDownloadProvider Tests', () => {
|
|||
|
||||
fixture = await createDownloadProvider(fixture);
|
||||
return fixture.downloadProvider.installSQLToolsService(Runtime.Windows_64).then(_ => {
|
||||
testHttpClient.verify(x => x.downloadFile(fixture.downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(),
|
||||
TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
testHttpClient.verify(x => x.downloadFile(fixture.downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
TypeMoq.Times.once());
|
||||
testDecompressProvider.verify(x => x.decompress(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
TypeMoq.Times.once());
|
||||
|
@ -170,8 +165,7 @@ suite('ServiceDownloadProvider Tests', () => {
|
|||
|
||||
fixture = await createDownloadProvider(fixture);
|
||||
return fixture.downloadProvider.installSQLToolsService(Runtime.Windows_64).catch(_ => {
|
||||
testHttpClient.verify(x => x.downloadFile(fixture.downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(),
|
||||
TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
testHttpClient.verify(x => x.downloadFile(fixture.downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
TypeMoq.Times.once());
|
||||
testDecompressProvider.verify(x => x.decompress(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
TypeMoq.Times.never());
|
||||
|
@ -190,8 +184,7 @@ suite('ServiceDownloadProvider Tests', () => {
|
|||
|
||||
fixture = await createDownloadProvider(fixture);
|
||||
return fixture.downloadProvider.installSQLToolsService(Runtime.Windows_64).catch(_ => {
|
||||
testHttpClient.verify(x => x.downloadFile(fixture.downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(),
|
||||
TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
testHttpClient.verify(x => x.downloadFile(fixture.downloadUrl, TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
TypeMoq.Times.once());
|
||||
testDecompressProvider.verify(x => x.decompress(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
|
||||
TypeMoq.Times.once());
|
||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -266,11 +266,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
||||
|
||||
"@types/azdata@^1.44.0":
|
||||
version "1.44.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/azdata/-/azdata-1.44.0.tgz#c6bb4baacd1b0f74b46742bffd61ae40e922989d"
|
||||
|
@ -747,6 +742,14 @@ aws4@^1.8.0:
|
|||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||
|
||||
axios@^0.27.2:
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
|
||||
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.9"
|
||||
form-data "^4.0.0"
|
||||
|
||||
babel-code-frame@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
|
@ -2183,6 +2186,11 @@ fmerge@1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/fmerge/-/fmerge-1.2.0.tgz#36e99d2ae255e3ee1af666b4df780553671cf692"
|
||||
integrity sha1-NumdKuJV4+4a9ma033gFU2cc9pI=
|
||||
|
||||
follow-redirects@^1.14.9:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||
|
||||
for-in@^1.0.1, for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
|
@ -2798,15 +2806,6 @@ hosted-git-info@^2.1.4:
|
|||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
http-proxy-agent@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
|
||||
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
|
||||
dependencies:
|
||||
"@tootallnate/once" "2"
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
http-proxy-agent@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
|
||||
|
@ -2825,7 +2824,7 @@ http-signature@~1.2.0:
|
|||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
|
||||
https-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
||||
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
|
||||
|
|
Загрузка…
Ссылка в новой задаче