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
70 changes: 51 additions & 19 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 } from "axios";
import { Transform, Readable } from "stream";
import FormData from "form-data";
import * as tough from "tough-cookie";
Expand All @@ -10,9 +10,11 @@ import { HttpHeaders } from "./httpHeaders";
import { HttpOperationResponse } from "./httpOperationResponse";
import { RestError } from "./restError";
import { WebResource, HttpRequestBody } from "./webResource";
import * as tunnel from "tunnel";
import { ProxySettings } from "./serviceClient";

export const axiosClient = axios.create();
import http from "http";
import https from "https";
import { URLBuilder } from "./url";

/**
* A HttpClient implementation that uses axios to send HTTP requests.
Expand Down Expand Up @@ -130,9 +132,19 @@ export class AxiosHttpClient implements HttpClient {
responseType: httpRequest.streamResponseBody ? "stream" : "text",
cancelToken,
timeout: httpRequest.timeout,
proxy: convertToAxiosProxyConfig(httpRequest.proxySettings)
proxy: false
};
res = await axiosClient(config);

if (httpRequest.proxySettings) {
const agent = createProxyAgent(httpRequest.url, httpRequest.proxySettings, httpRequest.headers);
if (agent.isHttps) {
config.httpsAgent = agent.agent;
} else {
config.httpAgent = agent.agent;
}
}

res = await axios.request(config);
} catch (err) {
if (err instanceof axios.Cancel) {
throw new RestError(err.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
Expand Down Expand Up @@ -198,25 +210,45 @@ export class AxiosHttpClient implements HttpClient {
}
}

function convertToAxiosProxyConfig(proxySettings: ProxySettings | undefined): AxiosProxyConfig | undefined {
if (!proxySettings) {
return undefined;
function isReadableStream(body: any): body is Readable {
return typeof body.pipe === "function";
}

declare type ProxyAgent = { isHttps: boolean; agent: http.Agent | https.Agent };
export function createProxyAgent(requestUrl: string, proxySettings: ProxySettings, headers?: HttpHeaders): ProxyAgent {
const tunnelOptions: tunnel.HttpsOverHttpsOptions = {
proxy: {
host: proxySettings.host,
port: proxySettings.port,
headers: (headers && headers.rawHeaders()) || {}
}
};

if ((proxySettings.username && proxySettings.password)) {
tunnelOptions.proxy!.proxyAuth = `${proxySettings.username}:${proxySettings.password}`;
}

const axiosAuthConfig = (proxySettings.username && proxySettings.password) ? {
username: proxySettings.username,
password: proxySettings.password
} : undefined;
const requestScheme = URLBuilder.parse(requestUrl).getScheme() || "";
const isRequestHttps = requestScheme.toLowerCase() === "https";
const proxyScheme = URLBuilder.parse(proxySettings.host).getScheme() || "";
const isProxyHttps = proxyScheme.toLowerCase() === "https";

const axiosProxyConfig: AxiosProxyConfig = {
host: proxySettings.host,
port: proxySettings.port,
auth: axiosAuthConfig
const proxyAgent = {
isHttps: isRequestHttps,
agent: createTunnel(isRequestHttps, isProxyHttps, tunnelOptions)
};

return axiosProxyConfig;
return proxyAgent;
}

function isReadableStream(body: any): body is Readable {
return typeof body.pipe === "function";
export function createTunnel(isRequestHttps: boolean, isProxyHttps: boolean, tunnelOptions: tunnel.HttpsOverHttpsOptions): http.Agent | https.Agent {
if (isRequestHttps && isProxyHttps) {
return tunnel.httpsOverHttps(tunnelOptions);
} else if (isRequestHttps && !isProxyHttps) {
return tunnel.httpsOverHttp(tunnelOptions);
} else if (!isRequestHttps && isProxyHttps) {
return tunnel.httpOverHttps(tunnelOptions);
} else {
return tunnel.httpOverHttp(tunnelOptions);
}
}
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
7 changes: 4 additions & 3 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,8 +410,9 @@ function createDefaultRequestPolicyFactories(credentials: ServiceClientCredentia

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

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

return factories;
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
11 changes: 6 additions & 5 deletions rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
input: "./es/lib/msRest.js",
external: [
"axios",
"xml2js",
"tough-cookie",
"uuid/v4",
"tslib",
"form-data",
"os",
"stream",
"os"
"tough-cookie",
"tslib",
"tunnel",
"uuid/v4",
"xml2js",
],
output: {
file: "./dist/msRest.node.js",
Expand Down
111 changes: 111 additions & 0 deletions test/axiosHttpClientTests.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import "chai/register-should";
import { should } from "chai";
import tunnel from "tunnel";
import https from "https";

import { HttpHeaders } from "../lib/msRest";
import { createTunnel, createProxyAgent } from "../lib/axiosHttpClient";

describe("AxiosHttpClient", () => {
describe("createProxyAgent", () => {
type HttpsAgent = https.Agent & {
defaultPort: number | undefined,
options: {
proxy: tunnel.ProxyOptions
},
proxyOptions: tunnel.ProxyOptions
};

[
{ proxy: "http", request: "ftp", port: undefined, isProxyHttps: false },
{ proxy: "http", request: "http", port: undefined, isProxyHttps: false },
{ proxy: "hTtp", request: "https", port: 443, isProxyHttps: true },
{ proxy: "HTTPS", request: "http", port: undefined, isProxyHttps: false },
{ proxy: "https", request: "hTTps", port: 443, isProxyHttps: true }
].forEach(testCase => {
it(`should return ${testCase.isProxyHttps ? "HTTPS" : "HTTP"} proxy for ${testCase.proxy.toUpperCase()} proxy server and ${testCase.request.toUpperCase()} request`, function (done) {
const proxySettings = {
host: `${testCase.proxy}://proxy.microsoft.com`,
port: 8080
};
const requestUrl = `${testCase.request}://example.com`;

const proxyAgent = createProxyAgent(requestUrl, proxySettings);

proxyAgent.isHttps.should.equal(testCase.isProxyHttps);
const agent = proxyAgent.agent as HttpsAgent;
should().equal(agent.defaultPort, testCase.port);
agent.options.proxy.host!.should.equal(proxySettings.host);
agent.options.proxy.port!.should.equal(proxySettings.port);
done();
});
});

it("should copy headers correctly", function (done) {
const proxySettings = {
host: "http://proxy.microsoft.com",
port: 8080
};
const headers = new HttpHeaders({
"User-Agent": "Node.js"
});

const proxyAgent = createProxyAgent("http://example.com", proxySettings, headers);

const agent = proxyAgent.agent as HttpsAgent;
agent.proxyOptions.headers.should.contain({ "user-agent": "Node.js" });
done();
});
});

describe("createTunnel", () => {
const defaultProxySettings = {
host: "http://proxy.microsoft.com",
port: 8080
};

type HttpsAgent = https.Agent & {
defaultPort: number | undefined,
options: {
proxy: tunnel.ProxyOptions
}
};

[true, false].forEach(value => {
it(`returns HTTP agent for HTTP request and HTTP${value ? "S" : ""} proxy`, function () {
const tunnelConfig: tunnel.HttpsOverHttpsOptions = {
proxy: {
host: defaultProxySettings.host,
port: defaultProxySettings.port,
headers: {}
}
};

const tunnel = createTunnel(false, value, tunnelConfig) as HttpsAgent;
tunnel.options.proxy.host!.should.equal(defaultProxySettings.host);
tunnel.options.proxy.port!.should.equal(defaultProxySettings.port);
should().not.exist(tunnel.defaultPort);
});
});

[true, false].forEach(value => {
it(`returns HTTPS agent for HTTPS request and HTTP${value ? "S" : ""} proxy`, function () {
const tunnelConfig: tunnel.HttpsOverHttpsOptions = {
proxy: {
host: defaultProxySettings.host,
port: defaultProxySettings.port,
headers: {}
}
};

const tunnel = createTunnel(true, value, tunnelConfig) as HttpsAgent;
tunnel.options.proxy.host!.should.equal(defaultProxySettings.host);
tunnel.options.proxy.port!.should.equal(defaultProxySettings.port);
tunnel.defaultPort!.should.equal(443);
});
});
});
});
Loading