Skip to content

Commit

Permalink
Resolving proxy from env on redirect (#4436)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
mbargiel committed May 11, 2022
1 parent de5e006 commit 495d5fb
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 78 deletions.
120 changes: 44 additions & 76 deletions lib/adapters/http.js
Expand Up @@ -4,6 +4,7 @@ var utils = require('./../utils');
var settle = require('./../core/settle');
var buildFullPath = require('../core/buildFullPath');
var buildURL = require('./../helpers/buildURL');
var getProxyForUrl = require('proxy-from-env').getProxyForUrl;
var http = require('http');
var https = require('https');
var httpFollow = require('follow-redirects').http;
Expand All @@ -22,25 +23,46 @@ var supportedProtocols = [ 'http:', 'https:', 'file:' ];
/**
*
* @param {http.ClientRequestArgs} options
* @param {AxiosProxyConfig} proxy
* @param {AxiosProxyConfig} configProxy
* @param {string} location
*/
function setProxy(options, proxy, location) {
options.hostname = proxy.host;
options.host = proxy.host;
options.port = proxy.port;
options.path = location;

// Basic proxy authorization
if (proxy.auth) {
var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64');
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
function setProxy(options, configProxy, location) {
var proxy = configProxy;
if (!proxy && proxy !== false) {
var proxyUrl = getProxyForUrl(location);
if (proxyUrl) {
proxy = url.parse(proxyUrl);
// replace 'host' since the proxy object is not a URL object
proxy.host = proxy.hostname;
}
}
if (proxy) {
// Basic proxy authorization
if (proxy.auth) {
// Support proxy auth object form
if (proxy.auth.username || proxy.auth.password) {
proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');
}
var base64 = Buffer
.from(proxy.auth, 'utf8')
.toString('base64');
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
}

options.headers.host = options.hostname + (options.port ? ':' + options.port : '');
options.hostname = proxy.host;
options.host = proxy.host;
options.port = proxy.port;
options.path = location;
if (proxy.protocol) {
options.protocol = proxy.protocol;
}
}

// If a proxy is used, any redirects must also pass through the proxy
options.beforeRedirect = function beforeRedirect(redirection) {
redirection.headers.host = redirection.host;
setProxy(redirection, proxy, redirection.href);
options.beforeRedirect = function beforeRedirect(redirectOptions) {
// Configure proxy for redirected request, passing the original config proxy to apply
// the exact same logic as if the redirected request was performed by axios directly.
setProxy(redirectOptions, configProxy, redirectOptions.href);
};
}

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

var isHttpsRequest = isHttps.test(protocol);
var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;

try {
buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, '');
} catch (err) {
Expand All @@ -169,85 +188,34 @@ module.exports = function httpAdapter(config) {
path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
method: config.method.toUpperCase(),
headers: headers,
agent: agent,
agents: { http: config.httpAgent, https: config.httpsAgent },
auth: auth
auth: auth,
protocol: protocol
};

if (config.socketPath) {
options.socketPath = config.socketPath;
} else {
options.hostname = parsed.hostname;
options.port = parsed.port;
}

var proxy = config.proxy;
if (!proxy && proxy !== false) {
var proxyEnv = protocol.slice(0, -1) + '_proxy';
var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()];
if (proxyUrl) {
var parsedProxyUrl = url.parse(proxyUrl);
var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY;
var shouldProxy = true;

if (noProxyEnv) {
var noProxy = noProxyEnv.split(',').map(function trim(s) {
return s.trim();
});

shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
if (!proxyElement) {
return false;
}
if (proxyElement === '*') {
return true;
}
if (proxyElement[0] === '.' &&
parsed.hostname.slice(parsed.hostname.length - proxyElement.length) === proxyElement) {
return true;
}

return parsed.hostname === proxyElement;
});
}

if (shouldProxy) {
proxy = {
host: parsedProxyUrl.hostname,
port: parsedProxyUrl.port,
protocol: parsedProxyUrl.protocol
};

if (parsedProxyUrl.auth) {
var proxyUrlAuth = parsedProxyUrl.auth.split(':');
proxy.auth = {
username: proxyUrlAuth[0],
password: proxyUrlAuth[1]
};
}
}
}
}

if (proxy) {
options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
setProxy(options, proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
}

var transport;
var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);
var isHttpsRequest = isHttps.test(options.protocol);
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;

This comment has been minimized.

Copy link
@danhenrik

danhenrik Sep 10, 2022

Is there anything I am missing here? Isn't it exactly the same as simply making options.agent = config.httpsAgent? Why is that?

if (config.transport) {
transport = config.transport;
} else if (config.maxRedirects === 0) {
transport = isHttpsProxy ? https : http;
transport = isHttpsRequest ? https : http;
} else {
if (config.maxRedirects) {
options.maxRedirects = config.maxRedirects;
}
if (config.beforeRedirect) {
options.beforeRedirect = config.beforeRedirect;
}
transport = isHttpsProxy ? httpsFollow : httpFollow;
transport = isHttpsRequest ? httpsFollow : httpFollow;
}

if (config.maxBodyLength > -1) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -84,7 +84,8 @@
"typings": "./index.d.ts",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0"
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"bundlesize": [
{
Expand Down
52 changes: 51 additions & 1 deletion test/unit/adapters/http.js
Expand Up @@ -687,7 +687,7 @@ describe('supports http with nodejs', function () {
proxy: {
host: 'localhost',
port: 4000,
protocol: 'https'
protocol: 'https:'
},
httpsAgent: new https.Agent({
rejectUnauthorized: false
Expand Down Expand Up @@ -798,6 +798,56 @@ describe('supports http with nodejs', function () {
});
});

it('should re-evaluate proxy on redirect when proxy set via env var', function (done) {
process.env.http_proxy = 'http://localhost:4000'
process.env.no_proxy = 'localhost:4000'

var proxyUseCount = 0;

server = http.createServer(function (req, res) {
res.setHeader('Location', 'http://localhost:4000/redirected');
res.statusCode = 302;
res.end();
}).listen(4444, function () {
proxy = http.createServer(function (request, response) {
var parsed = url.parse(request.url);
if (parsed.pathname === '/redirected') {
response.statusCode = 200;
response.end();
return;
}

proxyUseCount += 1;

var opts = {
host: parsed.hostname,
port: parsed.port,
path: parsed.path,
protocol: parsed.protocol,
rejectUnauthorized: false
};

http.get(opts, function (res) {
var body = '';
res.on('data', function (data) {
body += data;
});
res.on('end', function () {
response.setHeader('Content-Type', 'text/html; charset=UTF-8');
response.setHeader('Location', res.headers.location);
response.end(body);
});
});
}).listen(4000, function () {
axios.get('http://localhost:4444/').then(function(res) {
assert.equal(res.status, 200);
assert.equal(proxyUseCount, 1);
done();
}).catch(done);
});
});
});

it('should not use proxy for domains in no_proxy', function (done) {
server = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
Expand Down

0 comments on commit 495d5fb

Please sign in to comment.