diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fba908e30..5b4e2a406f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x, 18.x] + node-version: [12.x, 14.x, 16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 3f289c1061..03f0f96e25 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -580,7 +580,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) { } response.data = responseData; } catch (err) { - reject(AxiosError.from(err, null, config, response.request, response)); + return reject(AxiosError.from(err, null, config, response.request, response)); } settle(resolve, reject, response); }); diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 51fb9d71b3..f51500df2b 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -58,14 +58,15 @@ const LOCAL_SERVER_URL = 'http://localhost:4444'; const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res); -function startHTTPServer(options) { +function startHTTPServer(handlerOrOptions, options) { - const {handler, useBuffering = false, rate = undefined, port = 4444} = typeof options === 'function' ? { - handler: options - } : options || {}; + const {handler, useBuffering = false, rate = undefined, port = 4444, keepAlive = 1000} = + Object.assign(typeof handlerOrOptions === 'function' ? { + handler: handlerOrOptions + } : handlerOrOptions || {}, options); return new Promise((resolve, reject) => { - http.createServer(handler || async function (req, res) { + const server = http.createServer(handler || async function (req, res) { try { req.headers['content-length'] && res.setHeader('content-length', req.headers['content-length']); @@ -93,9 +94,21 @@ function startHTTPServer(options) { }).listen(port, function (err) { err ? reject(err) : resolve(this); }); + + server.keepAliveTimeout = keepAlive; }); } +const stopHTTPServer = async (server, timeout = 10000) => { + if (server) { + if (typeof server.closeAllConnections === 'function') { + server.closeAllConnections(); + } + + await Promise.race([new Promise(resolve => server.close(resolve)), setTimeoutAsync(timeout)]); + } +} + const handleFormData = (req) => { return new Promise((resolve, reject) => { const form = new formidable.IncomingForm(); @@ -131,16 +144,12 @@ function generateReadableStream(length = 1024 * 1024, chunkSize = 10 * 1024, sle } describe('supports http with nodejs', function () { + afterEach(async function () { + await Promise.all([stopHTTPServer(server), stopHTTPServer(proxy)]); + + server = null; + proxy = null; - afterEach(function () { - if (server) { - server.close(); - server = null; - } - if (proxy) { - proxy.close(); - proxy = null; - } delete process.env.http_proxy; delete process.env.https_proxy; delete process.env.no_proxy; @@ -382,53 +391,57 @@ describe('supports http with nodejs', function () { }); }); - it('should support beforeRedirect and proxy with redirect', function (done) { - var requestCount = 0; - var totalRedirectCount = 5; - server = http.createServer(function (req, res) { + it('should support beforeRedirect and proxy with redirect', async () => { + let requestCount = 0; + let totalRedirectCount = 5; + + server = await startHTTPServer(function (req, res) { requestCount += 1; if (requestCount <= totalRedirectCount) { res.setHeader('Location', 'http://localhost:4444'); res.writeHead(302); } res.end(); - }).listen(4444, function () { - var proxyUseCount = 0; - proxy = http.createServer(function (request, response) { - proxyUseCount += 1; - var parsed = url.parse(request.url); - var opts = { - host: parsed.hostname, - port: parsed.port, - path: parsed.path - }; + }, {port: 4444}); + + let proxyUseCount = 0; + proxy = await startHTTPServer(function (req, res) { + proxyUseCount += 1; + const targetUrl = new URL(req.url, 'http://' + req.headers.host); + const opts = { + host: targetUrl.hostname, + port: targetUrl.port, + path: targetUrl.path, + method: req.method + }; - http.get(opts, function (res) { - response.writeHead(res.statusCode, res.headers); - res.on('data', function (data) { - response.write(data) - }); - res.on('end', function () { - response.end(); - }); - }); - }).listen(4000, function () { - var configBeforeRedirectCount = 0; - axios.get('http://localhost:4444/', { - proxy: { - host: 'localhost', - port: 4000 - }, - maxRedirects: totalRedirectCount, - beforeRedirect: function (options) { - configBeforeRedirectCount += 1; - } - }).then(function (res) { - assert.equal(totalRedirectCount, configBeforeRedirectCount, 'should invoke config.beforeRedirect option on every redirect'); - assert.equal(totalRedirectCount + 1, proxyUseCount, 'should go through proxy on every redirect'); - done(); - }).catch(done); + const request = http.get(opts, function (response) { + res.writeHead(response.statusCode, response.headers); + stream.pipeline(response, res, () => {}); }); + + request.on('error', (err) => { + console.warn('request error', err); + res.statusCode = 500; + res.end(); + }) + + }, {port: 4000}); + + let configBeforeRedirectCount = 0; + + await axios.get('http://localhost:4444/', { + proxy: { + host: 'localhost', + port: 4000 + }, + maxRedirects: totalRedirectCount, + beforeRedirect: function (options) { + configBeforeRedirectCount += 1; + } + }).then(function (res) { + assert.equal(totalRedirectCount, configBeforeRedirectCount, 'should invoke config.beforeRedirect option on every redirect'); + assert.equal(totalRedirectCount + 1, proxyUseCount, 'should go through proxy on every redirect'); }); }); @@ -666,31 +679,18 @@ describe('supports http with nodejs', function () { }); }); - it('should support max content length', function (done) { - var str = Array(100000).join('ж'); - - server = http.createServer(function (req, res) { + it('should support max content length', async function () { + server = await startHTTPServer(function (req, res) { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); - res.end(str); - }).listen(4444, function () { - var success = false, failure = false, error; + res.end(Array(5000).join('#')); + }, {port: 4444}); - axios.get('http://localhost:4444/', { - maxContentLength: 2000 - }).then(function (res) { - success = true; - }).catch(function (err) { - error = err; - failure = true; - }); - - setTimeout(function () { - assert.equal(success, false, 'request should not succeed'); - assert.equal(failure, true, 'request should fail'); - assert.equal(error.message, 'maxContentLength size of 2000 exceeded'); - done(); - }, 100); - }); + await assert.rejects(() => { + return axios.get('http://localhost:4444/', { + maxContentLength: 2000, + maxRedirects: 0 + }) + },/maxContentLength size of 2000 exceeded/); }); it('should support max content length for redirected', function (done) { @@ -711,7 +711,7 @@ describe('supports http with nodejs', function () { var success = false, failure = false, error; axios.get('http://localhost:4444/one', { - maxContentLength: 2000 + maxContentLength: 2000, }).then(function (res) { success = true; }).catch(function (err) { diff --git a/test/unit/regression/SNYK-JS-AXIOS-1038255.js b/test/unit/regression/SNYK-JS-AXIOS-1038255.js index ca64a78841..1bdf58114c 100644 --- a/test/unit/regression/SNYK-JS-AXIOS-1038255.js +++ b/test/unit/regression/SNYK-JS-AXIOS-1038255.js @@ -20,8 +20,9 @@ describe('Server-Side Request Forgery (SSRF)', () => { fail = true; res.end('rm -rf /'); }).listen(EVIL_PORT); + proxy = http.createServer(function (req, res) { - if (req.url === 'http://localhost:' + EVIL_PORT + '/') { + if (new URL(req.url, 'http://' + req.headers.host).toString() === 'http://localhost:' + EVIL_PORT + '/') { return res.end(JSON.stringify({ msg: 'Protected', headers: req.headers, @@ -35,8 +36,10 @@ describe('Server-Side Request Forgery (SSRF)', () => { server.close(); proxy.close(); }); + it('obeys proxy settings when following redirects', async () => { location = 'http://localhost:' + EVIL_PORT; + let response = await axios({ method: "get", url: "http://www.google.com/",