Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTP(S) over HTTP(S) proxy support #322

Merged
merged 15 commits into from
Jan 25, 2019
23 changes: 18 additions & 5 deletions lib/axiosHttpClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosProxyConfig } from "axios";
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosProxyConfig, AxiosInstance } from "axios";
import { Transform, Readable } from "stream";
import FormData from "form-data";
import * as tough from "tough-cookie";
Expand All @@ -11,10 +11,7 @@ import { HttpOperationResponse } from "./httpOperationResponse";
import { RestError } from "./restError";
import { WebResource, HttpRequestBody } from "./webResource";
import { ProxySettings } from "./serviceClient";

const axiosClient = axios.create();
// Workaround for https://github.com/axios/axios/issues/1158
axiosClient.interceptors.request.use(config => ({ ...config, method: config.method && config.method.toUpperCase() as any }));
import * as tunnel from "tunnel";

/**
* A HttpClient implementation that uses axios to send HTTP requests.
Expand Down Expand Up @@ -134,6 +131,22 @@ export class AxiosHttpClient implements HttpClient {
timeout: httpRequest.timeout,
proxy: convertToAxiosProxyConfig(httpRequest.proxySettings)
};

let axiosClient: AxiosInstance;
if (httpRequest.proxySettings) {
const agent = tunnel.httpsOverHttp({
proxy: {
host: httpRequest.proxySettings.host,
port: httpRequest.proxySettings.port,
headers: {}
}
});

axiosClient = axios.create({ httpAgent: agent, proxy: false });
} else {
axiosClient = axios.create();
}

res = await axiosClient(config);
} catch (err) {
if (err instanceof axios.Cancel) {
Expand Down
40 changes: 38 additions & 2 deletions lib/policies/proxyPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,47 @@ import { BaseRequestPolicy, RequestPolicy, RequestPolicyFactory, RequestPolicyOp
import { HttpOperationResponse } from "../httpOperationResponse";
import { ProxySettings } from "../serviceClient";
import { WebResource } from "../webResource";
import { Constants } from "../util/constants";
import { URLBuilder } from "../url";

export function proxyPolicy(proxySettings: ProxySettings): RequestPolicyFactory {
function loadEnvironmentProxyValue(): string | undefined {
if (!process) {
return undefined;
}

if (process.env[Constants.HTTPS_PROXY]) {
return process.env[Constants.HTTPS_PROXY];
} else if (process.env[Constants.HTTPS_PROXY.toLowerCase()]) {
return process.env[Constants.HTTPS_PROXY.toLowerCase()];
} else if (process.env[Constants.HTTP_PROXY]) {
return process.env[Constants.HTTP_PROXY];
} else if (process.env[Constants.HTTP_PROXY.toLowerCase()]) {
return process.env[Constants.HTTP_PROXY.toLowerCase()];
}

return undefined;
}

export function getDefaultProxySettings(proxyUrl?: string): ProxySettings | undefined {
if (!proxyUrl) {
proxyUrl = loadEnvironmentProxyValue();
if (!proxyUrl) {
return undefined;
}
}

const parsedUrl = URLBuilder.parse(proxyUrl);
return {
host: parsedUrl.getScheme() + "://" + parsedUrl.getHost(),
port: Number.parseInt(parsedUrl.getPort() || "80")
};
}


export function proxyPolicy(proxySettings?: ProxySettings): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptions) => {
return new ProxyPolicy(nextPolicy, options, proxySettings);
return new ProxyPolicy(nextPolicy, options, proxySettings!);
}
};
}
Expand Down
4 changes: 2 additions & 2 deletions lib/serviceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import { stringifyXML } from "./util/xml";
import { RequestOptionsBase, RequestPrepareOptions, WebResource } from "./webResource";
import { OperationResponse } from "./operationResponse";
import { ServiceCallback } from "./util/utils";
import { proxyPolicy, getDefaultProxySettings } from "./policies/proxyPolicy";
import { throttlingRetryPolicy } from "./policies/throttlingRetryPolicy";
import { proxyPolicy } from "./policies/proxyPolicy";


/**
Expand Down Expand Up @@ -410,7 +410,7 @@ function createDefaultRequestPolicyFactories(credentials: ServiceClientCredentia

factories.push(deserializationPolicy(options.deserializationContentTypes));

if (options.proxySettings) {
if (options.proxySettings || (options.proxySettings = getDefaultProxySettings())) {
factories.push(proxyPolicy(options.proxySettings));
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"form-data": "^2.3.2",
"tough-cookie": "^2.4.3",
"tslib": "^1.9.2",
"tunnel": "0.0.6",
"uuid": "^3.2.1",
"xml2js": "^0.4.19"
},
Expand All @@ -67,6 +68,7 @@
"@types/semver": "^5.5.0",
"@types/sinon": "^5.0.6",
"@types/tough-cookie": "^2.3.3",
"@types/tunnel": "0.0.0",
"@types/uuid": "^3.4.4",
"@types/webpack": "^4.4.13",
"@types/webpack-dev-middleware": "^2.0.2",
Expand Down
96 changes: 92 additions & 4 deletions test/policies/proxyPolicyTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

import "chai/register-should";
import { should } from "chai";
import { ProxySettings } from "../../lib/serviceClient";
import { RequestPolicyOptions } from "../../lib/policies/requestPolicy";
import { WebResource } from "../../lib/webResource";
import { HttpHeaders } from "../../lib/httpHeaders";
import { proxyPolicy, ProxyPolicy } from "../../lib/policies/proxyPolicy";
import { proxyPolicy, ProxyPolicy, getDefaultProxySettings } from "../../lib/policies/proxyPolicy";
import { Constants, isNode } from "../../lib/msRest";



describe("ProxyPolicy", function() {
describe("ProxyPolicy", function () {
const proxySettings: ProxySettings = {
host: "https://example.com",
port: 3030,
Expand All @@ -36,6 +36,7 @@ describe("ProxyPolicy", function() {
done();
});


it("sets correct proxy settings through constructor", function (done) {
const policy = new ProxyPolicy(emptyRequestPolicy, emptyPolicyOptions, proxySettings);
policy.proxySettings.should.be.deep.equal(proxySettings);
Expand All @@ -61,4 +62,91 @@ describe("ProxyPolicy", function() {

request.proxySettings!.should.be.deep.equal(requestSpecificProxySettings);
});

});

describe.only("getDefaultProxySettings", () => {
const proxyUrl = "https://proxy.microsoft.com";
const defaultPort = 80;

it("should return settings with passed address", () => {
const proxySettings: ProxySettings = getDefaultProxySettings(proxyUrl)!;
proxySettings.host.should.equal(proxyUrl);
});

it("should return settings with default port", () => {
const proxySettings: ProxySettings = getDefaultProxySettings(proxyUrl)!;
proxySettings.port.should.equal(defaultPort);
});

it("should return settings with passed port", () => {
const port = 3030;
const proxyUrl = "prot://proxy.microsoft.com";
const proxyUrlWithPort = `${proxyUrl}:${port}`;
const proxySettings: ProxySettings = getDefaultProxySettings(proxyUrlWithPort)!;
proxySettings.host.should.equal(proxyUrl);
proxySettings.port.should.equal(port);
});

describe("with loadEnvironmentProxyValue", () => {
beforeEach(() => {
delete process.env[Constants.HTTP_PROXY];
delete process.env[Constants.HTTPS_PROXY];
delete process.env[Constants.HTTP_PROXY.toLowerCase()];
delete process.env[Constants.HTTPS_PROXY.toLowerCase()];
});

it("should return undefined when no proxy passed and environment variable is not set", () => {
const proxySettings: ProxySettings | undefined = getDefaultProxySettings();
should().not.exist(proxySettings);
});

it("should load settings from environment variables when no proxyUrl passed", () => {
const proxyUrl = "http://proxy.azure.com";
process.env[Constants.HTTP_PROXY] = proxyUrl;
const proxySettings: ProxySettings = getDefaultProxySettings()!;

proxySettings.host.should.equal(proxyUrl);
proxySettings.port.should.equal(defaultPort);
});

describe("should prefer HTTPS proxy over HTTP proxy", () => {
[
{ name: "lower case", func: (envVar: string) => envVar.toLowerCase() },
{ name: "upper case", func: (envVar: string) => envVar.toUpperCase() }
].forEach(testCase => {
it(`with ${testCase.name}`, () => {
const httpProxy = "http://proxy.microsoft.com";
const httpsProxy = "https://proxy.azure.com";
process.env[testCase.func(Constants.HTTP_PROXY)] = httpProxy;
process.env[testCase.func(Constants.HTTPS_PROXY)] = httpsProxy;

const proxySettings: ProxySettings = getDefaultProxySettings()!;
proxySettings.host.should.equal(httpsProxy);
proxySettings.port.should.equal(defaultPort);
});
});

it("should prefer HTTPS proxy over HTTP proxy", () => {
const httpProxy = "http://proxy.microsoft.com";
const httpsProxy = "https://proxy.azure.com";
process.env[Constants.HTTP_PROXY] = httpProxy;
process.env[Constants.HTTPS_PROXY] = httpsProxy;

const proxySettings: ProxySettings = getDefaultProxySettings()!;
proxySettings.host.should.equal(httpsProxy);
proxySettings.port.should.equal(defaultPort);
});
});

["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"].forEach(envVariableName => {
it(`should should load setting from "${envVariableName}" environmental variable`, () => {
process.env[envVariableName] = proxyUrl;
const proxySettings: ProxySettings = getDefaultProxySettings()!;

proxySettings.host.should.equal(proxyUrl);
proxySettings.port.should.equal(defaultPort);
});
});
});
});
8 changes: 4 additions & 4 deletions webpack.testconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ const config: webpack.Configuration = {
extensions: [".tsx", ".ts", ".js"]
},
node: {
Buffer: false,
dns: false,
fs: "empty",
net: false,
path: "empty",
dns: false,
process: "mock",
stream: "empty",
tls: false,
tty: false,
v8: false,
Buffer: false,
process: false,
stream: "empty"
}
};

Expand Down