Skip to content

Commit 495d5fb

Browse files
authored
Resolving proxy from env on redirect (axios#4436)
* Fixing http adapter to recompute proxy on redirect Redirections can target different hosts or change the protocol from http to https or vice versa. When the proxy option is inferred from the environment, it should be recomputed when the protocol or host changes because the proxy host can differ or even whether to proxy or not can differ. * Fixing proxy protocol handling 1) setProxy now changes request options protocol when using a proxy with explicit protocol. 2) As a result, selection of the correct transport can be simplified. 3) Legacy agent selection needs to be moved done accordingly. (Is 'agent' option even still used?) * Using proxy-from-env library to handle proxy env vars The proxy-from-env library is a popular, lightweight library that is very easy to use and covers a few more cases, not to mention it has extensive test coverage. * Fixing proxy auth handling * Adding test proving env vars are re-resolved on redirect * Revert unnecessary change * Fixing proxy beforeRedirect regression * Fixing lint errors * Revert "Fixing lint errors" This reverts commit 2de3cab. * Revert "Fixing proxy beforeRedirect regression" This reverts commit 57befc3.
1 parent de5e006 commit 495d5fb

File tree

3 files changed

+97
-78
lines changed

3 files changed

+97
-78
lines changed

lib/adapters/http.js

Lines changed: 44 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var utils = require('./../utils');
44
var settle = require('./../core/settle');
55
var buildFullPath = require('../core/buildFullPath');
66
var buildURL = require('./../helpers/buildURL');
7+
var getProxyForUrl = require('proxy-from-env').getProxyForUrl;
78
var http = require('http');
89
var https = require('https');
910
var httpFollow = require('follow-redirects').http;
@@ -22,25 +23,46 @@ var supportedProtocols = [ 'http:', 'https:', 'file:' ];
2223
/**
2324
*
2425
* @param {http.ClientRequestArgs} options
25-
* @param {AxiosProxyConfig} proxy
26+
* @param {AxiosProxyConfig} configProxy
2627
* @param {string} location
2728
*/
28-
function setProxy(options, proxy, location) {
29-
options.hostname = proxy.host;
30-
options.host = proxy.host;
31-
options.port = proxy.port;
32-
options.path = location;
33-
34-
// Basic proxy authorization
35-
if (proxy.auth) {
36-
var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64');
37-
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
29+
function setProxy(options, configProxy, location) {
30+
var proxy = configProxy;
31+
if (!proxy && proxy !== false) {
32+
var proxyUrl = getProxyForUrl(location);
33+
if (proxyUrl) {
34+
proxy = url.parse(proxyUrl);
35+
// replace 'host' since the proxy object is not a URL object
36+
proxy.host = proxy.hostname;
37+
}
38+
}
39+
if (proxy) {
40+
// Basic proxy authorization
41+
if (proxy.auth) {
42+
// Support proxy auth object form
43+
if (proxy.auth.username || proxy.auth.password) {
44+
proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');
45+
}
46+
var base64 = Buffer
47+
.from(proxy.auth, 'utf8')
48+
.toString('base64');
49+
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
50+
}
51+
52+
options.headers.host = options.hostname + (options.port ? ':' + options.port : '');
53+
options.hostname = proxy.host;
54+
options.host = proxy.host;
55+
options.port = proxy.port;
56+
options.path = location;
57+
if (proxy.protocol) {
58+
options.protocol = proxy.protocol;
59+
}
3860
}
3961

40-
// If a proxy is used, any redirects must also pass through the proxy
41-
options.beforeRedirect = function beforeRedirect(redirection) {
42-
redirection.headers.host = redirection.host;
43-
setProxy(redirection, proxy, redirection.href);
62+
options.beforeRedirect = function beforeRedirect(redirectOptions) {
63+
// Configure proxy for redirected request, passing the original config proxy to apply
64+
// the exact same logic as if the redirected request was performed by axios directly.
65+
setProxy(redirectOptions, configProxy, redirectOptions.href);
4466
};
4567
}
4668

@@ -152,9 +174,6 @@ module.exports = function httpAdapter(config) {
152174
delete headers[headerNames.authorization];
153175
}
154176

155-
var isHttpsRequest = isHttps.test(protocol);
156-
var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
157-
158177
try {
159178
buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, '');
160179
} catch (err) {
@@ -169,85 +188,34 @@ module.exports = function httpAdapter(config) {
169188
path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
170189
method: config.method.toUpperCase(),
171190
headers: headers,
172-
agent: agent,
173191
agents: { http: config.httpAgent, https: config.httpsAgent },
174-
auth: auth
192+
auth: auth,
193+
protocol: protocol
175194
};
176195

177196
if (config.socketPath) {
178197
options.socketPath = config.socketPath;
179198
} else {
180199
options.hostname = parsed.hostname;
181200
options.port = parsed.port;
182-
}
183-
184-
var proxy = config.proxy;
185-
if (!proxy && proxy !== false) {
186-
var proxyEnv = protocol.slice(0, -1) + '_proxy';
187-
var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()];
188-
if (proxyUrl) {
189-
var parsedProxyUrl = url.parse(proxyUrl);
190-
var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY;
191-
var shouldProxy = true;
192-
193-
if (noProxyEnv) {
194-
var noProxy = noProxyEnv.split(',').map(function trim(s) {
195-
return s.trim();
196-
});
197-
198-
shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
199-
if (!proxyElement) {
200-
return false;
201-
}
202-
if (proxyElement === '*') {
203-
return true;
204-
}
205-
if (proxyElement[0] === '.' &&
206-
parsed.hostname.slice(parsed.hostname.length - proxyElement.length) === proxyElement) {
207-
return true;
208-
}
209-
210-
return parsed.hostname === proxyElement;
211-
});
212-
}
213-
214-
if (shouldProxy) {
215-
proxy = {
216-
host: parsedProxyUrl.hostname,
217-
port: parsedProxyUrl.port,
218-
protocol: parsedProxyUrl.protocol
219-
};
220-
221-
if (parsedProxyUrl.auth) {
222-
var proxyUrlAuth = parsedProxyUrl.auth.split(':');
223-
proxy.auth = {
224-
username: proxyUrlAuth[0],
225-
password: proxyUrlAuth[1]
226-
};
227-
}
228-
}
229-
}
230-
}
231-
232-
if (proxy) {
233-
options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
234-
setProxy(options, proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
201+
setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
235202
}
236203

237204
var transport;
238-
var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);
205+
var isHttpsRequest = isHttps.test(options.protocol);
206+
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
239207
if (config.transport) {
240208
transport = config.transport;
241209
} else if (config.maxRedirects === 0) {
242-
transport = isHttpsProxy ? https : http;
210+
transport = isHttpsRequest ? https : http;
243211
} else {
244212
if (config.maxRedirects) {
245213
options.maxRedirects = config.maxRedirects;
246214
}
247215
if (config.beforeRedirect) {
248216
options.beforeRedirect = config.beforeRedirect;
249217
}
250-
transport = isHttpsProxy ? httpsFollow : httpFollow;
218+
transport = isHttpsRequest ? httpsFollow : httpFollow;
251219
}
252220

253221
if (config.maxBodyLength > -1) {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@
8484
"typings": "./index.d.ts",
8585
"dependencies": {
8686
"follow-redirects": "^1.15.0",
87-
"form-data": "^4.0.0"
87+
"form-data": "^4.0.0",
88+
"proxy-from-env": "^1.1.0"
8889
},
8990
"bundlesize": [
9091
{

test/unit/adapters/http.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ describe('supports http with nodejs', function () {
687687
proxy: {
688688
host: 'localhost',
689689
port: 4000,
690-
protocol: 'https'
690+
protocol: 'https:'
691691
},
692692
httpsAgent: new https.Agent({
693693
rejectUnauthorized: false
@@ -798,6 +798,56 @@ describe('supports http with nodejs', function () {
798798
});
799799
});
800800

801+
it('should re-evaluate proxy on redirect when proxy set via env var', function (done) {
802+
process.env.http_proxy = 'http://localhost:4000'
803+
process.env.no_proxy = 'localhost:4000'
804+
805+
var proxyUseCount = 0;
806+
807+
server = http.createServer(function (req, res) {
808+
res.setHeader('Location', 'http://localhost:4000/redirected');
809+
res.statusCode = 302;
810+
res.end();
811+
}).listen(4444, function () {
812+
proxy = http.createServer(function (request, response) {
813+
var parsed = url.parse(request.url);
814+
if (parsed.pathname === '/redirected') {
815+
response.statusCode = 200;
816+
response.end();
817+
return;
818+
}
819+
820+
proxyUseCount += 1;
821+
822+
var opts = {
823+
host: parsed.hostname,
824+
port: parsed.port,
825+
path: parsed.path,
826+
protocol: parsed.protocol,
827+
rejectUnauthorized: false
828+
};
829+
830+
http.get(opts, function (res) {
831+
var body = '';
832+
res.on('data', function (data) {
833+
body += data;
834+
});
835+
res.on('end', function () {
836+
response.setHeader('Content-Type', 'text/html; charset=UTF-8');
837+
response.setHeader('Location', res.headers.location);
838+
response.end(body);
839+
});
840+
});
841+
}).listen(4000, function () {
842+
axios.get('http://localhost:4444/').then(function(res) {
843+
assert.equal(res.status, 200);
844+
assert.equal(proxyUseCount, 1);
845+
done();
846+
}).catch(done);
847+
});
848+
});
849+
});
850+
801851
it('should not use proxy for domains in no_proxy', function (done) {
802852
server = http.createServer(function (req, res) {
803853
res.setHeader('Content-Type', 'text/html; charset=UTF-8');

0 commit comments

Comments
 (0)