From 495d5fb1333f2cb0c7e49ecfd6470dd1700ca96c Mon Sep 17 00:00:00 2001 From: Maxime Bargiel Date: Wed, 11 May 2022 13:24:14 -0400 Subject: [PATCH] Resolving proxy from env on redirect (#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 2de3cabc60db2444e63a699bae9ec45531218a84. * Revert "Fixing proxy beforeRedirect regression" This reverts commit 57befc3215980e47333fedc1e9028cc22297540b. --- lib/adapters/http.js | 118 ++++++++++++++----------------------- package.json | 3 +- test/unit/adapters/http.js | 52 +++++++++++++++- 3 files changed, 96 insertions(+), 77 deletions(-) diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 4937250..bdf6d37 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -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; @@ -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; +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; + } - // 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; + 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); }; } @@ -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) { @@ -169,9 +188,9 @@ 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) { @@ -179,67 +198,16 @@ module.exports = function httpAdapter(config) { } 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; 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; @@ -247,7 +215,7 @@ module.exports = function httpAdapter(config) { if (config.beforeRedirect) { options.beforeRedirect = config.beforeRedirect; } - transport = isHttpsProxy ? httpsFollow : httpFollow; + transport = isHttpsRequest ? httpsFollow : httpFollow; } if (config.maxBodyLength > -1) { diff --git a/package.json b/package.json index 88a6def..5be6f78 100644 --- a/package.json +++ b/package.json @@ -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": [ { diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 05adfea..5264f63 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -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 @@ -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');