Skip to content

Commit 1ee5a40

Browse files
authored
Add HTTP(S) over HTTP(S) proxy support (#322)
* Add proxy support using tunnel package # Conflicts: # lib/axiosHttpClient.ts # lib/policies/proxyPolicy.ts # lib/serviceClient.ts # package.json * Fix incorrect merge * Add tests * Remove commented code * Add tunnel to rollup configuration * Fix test title casing * Remove only * Add axios client tests * Mock buffer * Remove rewire * Fix default HTTP client tests * Add some proxy tests * Add support for HTTPS proxy * Address PR comments
1 parent 4c2b1c5 commit 1ee5a40

12 files changed

+343
-58
lines changed

lib/axiosHttpClient.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosProxyConfig } from "axios";
4+
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
55
import { Transform, Readable } from "stream";
66
import FormData from "form-data";
77
import * as tough from "tough-cookie";
@@ -10,9 +10,11 @@ import { HttpHeaders } from "./httpHeaders";
1010
import { HttpOperationResponse } from "./httpOperationResponse";
1111
import { RestError } from "./restError";
1212
import { WebResource, HttpRequestBody } from "./webResource";
13+
import * as tunnel from "tunnel";
1314
import { ProxySettings } from "./serviceClient";
14-
15-
export const axiosClient = axios.create();
15+
import http from "http";
16+
import https from "https";
17+
import { URLBuilder } from "./url";
1618

1719
/**
1820
* A HttpClient implementation that uses axios to send HTTP requests.
@@ -130,9 +132,19 @@ export class AxiosHttpClient implements HttpClient {
130132
responseType: httpRequest.streamResponseBody ? "stream" : "text",
131133
cancelToken,
132134
timeout: httpRequest.timeout,
133-
proxy: convertToAxiosProxyConfig(httpRequest.proxySettings)
135+
proxy: false
134136
};
135-
res = await axiosClient(config);
137+
138+
if (httpRequest.proxySettings) {
139+
const agent = createProxyAgent(httpRequest.url, httpRequest.proxySettings, httpRequest.headers);
140+
if (agent.isHttps) {
141+
config.httpsAgent = agent.agent;
142+
} else {
143+
config.httpAgent = agent.agent;
144+
}
145+
}
146+
147+
res = await axios.request(config);
136148
} catch (err) {
137149
if (err instanceof axios.Cancel) {
138150
throw new RestError(err.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
@@ -198,25 +210,45 @@ export class AxiosHttpClient implements HttpClient {
198210
}
199211
}
200212

201-
function convertToAxiosProxyConfig(proxySettings: ProxySettings | undefined): AxiosProxyConfig | undefined {
202-
if (!proxySettings) {
203-
return undefined;
213+
function isReadableStream(body: any): body is Readable {
214+
return typeof body.pipe === "function";
215+
}
216+
217+
declare type ProxyAgent = { isHttps: boolean; agent: http.Agent | https.Agent };
218+
export function createProxyAgent(requestUrl: string, proxySettings: ProxySettings, headers?: HttpHeaders): ProxyAgent {
219+
const tunnelOptions: tunnel.HttpsOverHttpsOptions = {
220+
proxy: {
221+
host: proxySettings.host,
222+
port: proxySettings.port,
223+
headers: (headers && headers.rawHeaders()) || {}
224+
}
225+
};
226+
227+
if ((proxySettings.username && proxySettings.password)) {
228+
tunnelOptions.proxy!.proxyAuth = `${proxySettings.username}:${proxySettings.password}`;
204229
}
205230

206-
const axiosAuthConfig = (proxySettings.username && proxySettings.password) ? {
207-
username: proxySettings.username,
208-
password: proxySettings.password
209-
} : undefined;
231+
const requestScheme = URLBuilder.parse(requestUrl).getScheme() || "";
232+
const isRequestHttps = requestScheme.toLowerCase() === "https";
233+
const proxyScheme = URLBuilder.parse(proxySettings.host).getScheme() || "";
234+
const isProxyHttps = proxyScheme.toLowerCase() === "https";
210235

211-
const axiosProxyConfig: AxiosProxyConfig = {
212-
host: proxySettings.host,
213-
port: proxySettings.port,
214-
auth: axiosAuthConfig
236+
const proxyAgent = {
237+
isHttps: isRequestHttps,
238+
agent: createTunnel(isRequestHttps, isProxyHttps, tunnelOptions)
215239
};
216240

217-
return axiosProxyConfig;
241+
return proxyAgent;
218242
}
219243

220-
function isReadableStream(body: any): body is Readable {
221-
return typeof body.pipe === "function";
244+
export function createTunnel(isRequestHttps: boolean, isProxyHttps: boolean, tunnelOptions: tunnel.HttpsOverHttpsOptions): http.Agent | https.Agent {
245+
if (isRequestHttps && isProxyHttps) {
246+
return tunnel.httpsOverHttps(tunnelOptions);
247+
} else if (isRequestHttps && !isProxyHttps) {
248+
return tunnel.httpsOverHttp(tunnelOptions);
249+
} else if (!isRequestHttps && isProxyHttps) {
250+
return tunnel.httpOverHttps(tunnelOptions);
251+
} else {
252+
return tunnel.httpOverHttp(tunnelOptions);
253+
}
222254
}

lib/policies/proxyPolicy.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,47 @@ import { BaseRequestPolicy, RequestPolicy, RequestPolicyFactory, RequestPolicyOp
55
import { HttpOperationResponse } from "../httpOperationResponse";
66
import { ProxySettings } from "../serviceClient";
77
import { WebResource } from "../webResource";
8+
import { Constants } from "../util/constants";
9+
import { URLBuilder } from "../url";
810

9-
export function proxyPolicy(proxySettings: ProxySettings): RequestPolicyFactory {
11+
function loadEnvironmentProxyValue(): string | undefined {
12+
if (!process) {
13+
return undefined;
14+
}
15+
16+
if (process.env[Constants.HTTPS_PROXY]) {
17+
return process.env[Constants.HTTPS_PROXY];
18+
} else if (process.env[Constants.HTTPS_PROXY.toLowerCase()]) {
19+
return process.env[Constants.HTTPS_PROXY.toLowerCase()];
20+
} else if (process.env[Constants.HTTP_PROXY]) {
21+
return process.env[Constants.HTTP_PROXY];
22+
} else if (process.env[Constants.HTTP_PROXY.toLowerCase()]) {
23+
return process.env[Constants.HTTP_PROXY.toLowerCase()];
24+
}
25+
26+
return undefined;
27+
}
28+
29+
export function getDefaultProxySettings(proxyUrl?: string): ProxySettings | undefined {
30+
if (!proxyUrl) {
31+
proxyUrl = loadEnvironmentProxyValue();
32+
if (!proxyUrl) {
33+
return undefined;
34+
}
35+
}
36+
37+
const parsedUrl = URLBuilder.parse(proxyUrl);
38+
return {
39+
host: parsedUrl.getScheme() + "://" + parsedUrl.getHost(),
40+
port: Number.parseInt(parsedUrl.getPort() || "80")
41+
};
42+
}
43+
44+
45+
export function proxyPolicy(proxySettings?: ProxySettings): RequestPolicyFactory {
1046
return {
1147
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptions) => {
12-
return new ProxyPolicy(nextPolicy, options, proxySettings);
48+
return new ProxyPolicy(nextPolicy, options, proxySettings!);
1349
}
1450
};
1551
}

lib/serviceClient.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import { stringifyXML } from "./util/xml";
2626
import { RequestOptionsBase, RequestPrepareOptions, WebResource } from "./webResource";
2727
import { OperationResponse } from "./operationResponse";
2828
import { ServiceCallback } from "./util/utils";
29+
import { proxyPolicy, getDefaultProxySettings } from "./policies/proxyPolicy";
2930
import { throttlingRetryPolicy } from "./policies/throttlingRetryPolicy";
30-
import { proxyPolicy } from "./policies/proxyPolicy";
3131

3232

3333
/**
@@ -410,8 +410,9 @@ function createDefaultRequestPolicyFactories(credentials: ServiceClientCredentia
410410

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

413-
if (options.proxySettings) {
414-
factories.push(proxyPolicy(options.proxySettings));
413+
const proxySettings = options.proxySettings || getDefaultProxySettings();
414+
if (proxySettings) {
415+
factories.push(proxyPolicy(proxySettings));
415416
}
416417

417418
return factories;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"form-data": "^2.3.2",
5353
"tough-cookie": "^2.4.3",
5454
"tslib": "^1.9.2",
55+
"tunnel": "0.0.6",
5556
"uuid": "^3.2.1",
5657
"xml2js": "^0.4.19"
5758
},
@@ -67,6 +68,7 @@
6768
"@types/semver": "^5.5.0",
6869
"@types/sinon": "^5.0.6",
6970
"@types/tough-cookie": "^2.3.3",
71+
"@types/tunnel": "0.0.0",
7072
"@types/uuid": "^3.4.4",
7173
"@types/webpack": "^4.4.13",
7274
"@types/webpack-dev-middleware": "^2.0.2",

rollup.config.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@
2424
input: "./es/lib/msRest.js",
2525
external: [
2626
"axios",
27-
"xml2js",
28-
"tough-cookie",
29-
"uuid/v4",
30-
"tslib",
3127
"form-data",
28+
"os",
3229
"stream",
33-
"os"
30+
"tough-cookie",
31+
"tslib",
32+
"tunnel",
33+
"uuid/v4",
34+
"xml2js",
3435
],
3536
output: {
3637
file: "./dist/msRest.node.js",

test/axiosHttpClientTests.node.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
import "chai/register-should";
5+
import { should } from "chai";
6+
import tunnel from "tunnel";
7+
import https from "https";
8+
9+
import { HttpHeaders } from "../lib/msRest";
10+
import { createTunnel, createProxyAgent } from "../lib/axiosHttpClient";
11+
12+
describe("AxiosHttpClient", () => {
13+
describe("createProxyAgent", () => {
14+
type HttpsAgent = https.Agent & {
15+
defaultPort: number | undefined,
16+
options: {
17+
proxy: tunnel.ProxyOptions
18+
},
19+
proxyOptions: tunnel.ProxyOptions
20+
};
21+
22+
[
23+
{ proxy: "http", request: "ftp", port: undefined, isProxyHttps: false },
24+
{ proxy: "http", request: "http", port: undefined, isProxyHttps: false },
25+
{ proxy: "hTtp", request: "https", port: 443, isProxyHttps: true },
26+
{ proxy: "HTTPS", request: "http", port: undefined, isProxyHttps: false },
27+
{ proxy: "https", request: "hTTps", port: 443, isProxyHttps: true }
28+
].forEach(testCase => {
29+
it(`should return ${testCase.isProxyHttps ? "HTTPS" : "HTTP"} proxy for ${testCase.proxy.toUpperCase()} proxy server and ${testCase.request.toUpperCase()} request`, function (done) {
30+
const proxySettings = {
31+
host: `${testCase.proxy}://proxy.microsoft.com`,
32+
port: 8080
33+
};
34+
const requestUrl = `${testCase.request}://example.com`;
35+
36+
const proxyAgent = createProxyAgent(requestUrl, proxySettings);
37+
38+
proxyAgent.isHttps.should.equal(testCase.isProxyHttps);
39+
const agent = proxyAgent.agent as HttpsAgent;
40+
should().equal(agent.defaultPort, testCase.port);
41+
agent.options.proxy.host!.should.equal(proxySettings.host);
42+
agent.options.proxy.port!.should.equal(proxySettings.port);
43+
done();
44+
});
45+
});
46+
47+
it("should copy headers correctly", function (done) {
48+
const proxySettings = {
49+
host: "http://proxy.microsoft.com",
50+
port: 8080
51+
};
52+
const headers = new HttpHeaders({
53+
"User-Agent": "Node.js"
54+
});
55+
56+
const proxyAgent = createProxyAgent("http://example.com", proxySettings, headers);
57+
58+
const agent = proxyAgent.agent as HttpsAgent;
59+
agent.proxyOptions.headers.should.contain({ "user-agent": "Node.js" });
60+
done();
61+
});
62+
});
63+
64+
describe("createTunnel", () => {
65+
const defaultProxySettings = {
66+
host: "http://proxy.microsoft.com",
67+
port: 8080
68+
};
69+
70+
type HttpsAgent = https.Agent & {
71+
defaultPort: number | undefined,
72+
options: {
73+
proxy: tunnel.ProxyOptions
74+
}
75+
};
76+
77+
[true, false].forEach(value => {
78+
it(`returns HTTP agent for HTTP request and HTTP${value ? "S" : ""} proxy`, function () {
79+
const tunnelConfig: tunnel.HttpsOverHttpsOptions = {
80+
proxy: {
81+
host: defaultProxySettings.host,
82+
port: defaultProxySettings.port,
83+
headers: {}
84+
}
85+
};
86+
87+
const tunnel = createTunnel(false, value, tunnelConfig) as HttpsAgent;
88+
tunnel.options.proxy.host!.should.equal(defaultProxySettings.host);
89+
tunnel.options.proxy.port!.should.equal(defaultProxySettings.port);
90+
should().not.exist(tunnel.defaultPort);
91+
});
92+
});
93+
94+
[true, false].forEach(value => {
95+
it(`returns HTTPS agent for HTTPS request and HTTP${value ? "S" : ""} proxy`, function () {
96+
const tunnelConfig: tunnel.HttpsOverHttpsOptions = {
97+
proxy: {
98+
host: defaultProxySettings.host,
99+
port: defaultProxySettings.port,
100+
headers: {}
101+
}
102+
};
103+
104+
const tunnel = createTunnel(true, value, tunnelConfig) as HttpsAgent;
105+
tunnel.options.proxy.host!.should.equal(defaultProxySettings.host);
106+
tunnel.options.proxy.port!.should.equal(defaultProxySettings.port);
107+
tunnel.defaultPort!.should.equal(443);
108+
});
109+
});
110+
});
111+
});

0 commit comments

Comments
 (0)