diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 8a434376..95b1e2d1 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -8,6 +8,7 @@ import https from 'https'; import http2 from 'http2'; import util from 'util'; import followRedirects from 'follow-redirects'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import zlib from 'zlib'; import { VERSION } from '../env/data.js'; import transitionalDefaults from '../defaults/transitional.js'; @@ -187,7 +188,7 @@ function dispatchBeforeRedirect(options, responseDetails) { * * @returns {http.ClientRequestArgs} */ -function setProxy(options, configProxy, location) { +function setProxy(options, configProxy, location, agentOptions = {}) { let proxy = configProxy; if (!proxy && proxy !== false) { const proxyUrl = getProxyForUrl(location); @@ -210,28 +211,65 @@ function setProxy(options, configProxy, location) { } else if (typeof proxy.auth === 'object') { throw new AxiosError('Invalid proxy authorization', AxiosError.ERR_BAD_OPTION, { proxy }); } - - const base64 = Buffer.from(proxy.auth, 'utf8').toString('base64'); - - options.headers['Proxy-Authorization'] = 'Basic ' + base64; } - options.headers.host = options.hostname + (options.port ? ':' + options.port : ''); + const targetIsHttps = isHttps.test(options.protocol); + const proxyProtocol = proxy.protocol + ? proxy.protocol.includes(':') + ? proxy.protocol + : `${proxy.protocol}:` + : 'http:'; const proxyHost = proxy.hostname || proxy.host; - options.hostname = proxyHost; - // Replace 'host' since options is not a URL object - options.host = proxyHost; - options.port = proxy.port; - options.path = location; - if (proxy.protocol) { - options.protocol = proxy.protocol.includes(':') ? proxy.protocol : `${proxy.protocol}:`; + + if (targetIsHttps && proxyProtocol === 'http:') { + // HTTPS target over an HTTP proxy must use CONNECT tunneling. + if (proxy.auth) { + const [username, ...passwordParts] = String(proxy.auth).split(':'); + const password = passwordParts.join(':'); + const proxyUrl = `http://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${proxyHost}:${proxy.port}`; + options.agent = new HttpsProxyAgent(proxyUrl, agentOptions); + } else { + options.agent = new HttpsProxyAgent(`http://${proxyHost}:${proxy.port}`, agentOptions); + } + + // The tunnel agent handles proxy auth during CONNECT. Do not forward it to origin. + delete options.headers['Proxy-Authorization']; + delete options.headers['proxy-authorization']; + + if (options.agents) { + options.agents.https = options.agent; + } + + // Preserve TLS settings from config.httpsAgent for the tunneled HTTPS request. + ['rejectUnauthorized', 'ca', 'cert', 'key', 'passphrase', 'pfx', 'servername'].forEach( + (key) => { + if (!utils.isUndefined(agentOptions[key])) { + options[key] = agentOptions[key]; + } + } + ); + } else { + if (proxy.auth) { + const 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 = proxyHost; + // Replace 'host' since options is not a URL object + options.host = proxyHost; + options.port = proxy.port; + options.path = location; + if (proxy.protocol) { + options.protocol = proxy.protocol.includes(':') ? proxy.protocol : `${proxy.protocol}:`; + } } } options.beforeRedirects.proxy = 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); + setProxy(redirectOptions, configProxy, redirectOptions.href, agentOptions); }; } @@ -658,6 +696,9 @@ export default isHttpAdapterSupported && if (config.socketPath) { options.socketPath = config.socketPath; } else { + const httpsAgentOptions = + config.httpsAgent instanceof https.Agent ? config.httpsAgent.options || {} : {}; + options.hostname = parsed.hostname.startsWith('[') ? parsed.hostname.slice(1, -1) : parsed.hostname; @@ -665,13 +706,14 @@ export default isHttpAdapterSupported && setProxy( options, config.proxy, - protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path + protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path, + httpsAgentOptions ); } let transport; const isHttpsRequest = isHttps.test(options.protocol); - options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; + options.agent = options.agent || (isHttpsRequest ? config.httpsAgent : config.httpAgent); if (isHttp2) { transport = http2Transport; diff --git a/package-lock.json b/package-lock.json index f6b85a86..f1bf89f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", + "https-proxy-agent": "^8.0.0", "proxy-from-env": "^2.1.0" }, "devDependencies": { @@ -2373,6 +2374,20 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@npmcli/agent/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5685,7 +5700,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7999,19 +8013,27 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-8.0.0.tgz", + "integrity": "sha512-YYeW+iCnAS3xhvj2dvVoWgsbca3RfQy/IlaNHHOtDmU0jMqPI9euIq3Y9BJETdxk16h9NHHCKqp/KB9nIMStCQ==", "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "agent-base": "8.0.0", + "debug": "^4.3.4" }, "engines": { "node": ">= 14" } }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-8.0.0.tgz", + "integrity": "sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -10066,7 +10088,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/multer": { diff --git a/package.json b/package.json index b6125fea..dde13996 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", + "https-proxy-agent": "^8.0.0", "proxy-from-env": "^2.1.0" }, "contributors": [ diff --git a/tests/unit/adapters/fetch.test.js b/tests/unit/adapters/fetch.test.js index 8b1bc3fd..8a191624 100644 --- a/tests/unit/adapters/fetch.test.js +++ b/tests/unit/adapters/fetch.test.js @@ -16,15 +16,9 @@ import util from 'util'; import NodeFormData from 'form-data'; const SERVER_PORT = 8010; -const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`; const pipelineAsync = util.promisify(stream.pipeline); -const fetchAxios = axios.create({ - baseURL: LOCAL_SERVER_URL, - adapter: 'fetch', -}); - describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => { describe('responses', () => { it('should support text response type', async () => { @@ -35,7 +29,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'text', }); @@ -53,7 +51,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'arraybuffer', }); @@ -74,7 +76,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'blob', }); @@ -92,7 +98,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'stream', }); @@ -123,7 +133,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => ); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'formdata', }); @@ -146,7 +160,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'json', }); @@ -188,7 +206,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const samples = []; - const { data } = await fetchAxios.post( + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.post( `http://localhost:${server.address().port}/`, readable, { @@ -241,7 +263,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { onUploadProgress() {}, }); @@ -284,7 +310,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const samples = []; - const { data } = await fetchAxios.post( + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.post( `http://localhost:${server.address().port}/`, readable, { @@ -359,7 +389,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const server = await startHTTPServer(async (req, res) => res.end('OK'), { port: SERVER_PORT }); try { - const { data } = await fetchAxios.post( + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.post( `http://localhost:${server.address().port}/`, stream.Readable.from('OK') ); @@ -388,14 +422,14 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }, 500); await assert.rejects(async () => { - await fetchAxios.post( - `http://localhost:${server.address().port}/`, - makeReadableStream(), - { - responseType: 'stream', - signal: controller.signal, - } - ); + const instance = axios.create({ + adapter: 'fetch', + }); + + await instance.post(`http://localhost:${server.address().port}/`, makeReadableStream(), { + responseType: 'stream', + signal: controller.signal, + }); }, /CanceledError/); } finally { await stopHTTPServer(server); @@ -419,7 +453,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => controller.abort(new Error('test')); }, 800); - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'stream', signal: controller.signal, }); @@ -448,7 +486,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const ts = Date.now(); await assert.rejects(async () => { - await fetchAxios(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + await instance.get(`http://localhost:${server.address().port}/`, { timeout, }); }, /timeout/); @@ -464,9 +506,14 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => it('should combine baseURL and url', async () => { const server = await startHTTPServer(async (req, res) => res.end('OK'), { port: SERVER_PORT }); try { - const res = await fetchAxios('/foo'); + const instance = axios.create({ + baseURL: `http://localhost:${server.address().port}`, + adapter: 'fetch', + }); - assert.equal(res.config.baseURL, LOCAL_SERVER_URL); + const res = await instance.get(`/foo`); + + assert.equal(res.config.baseURL, `http://localhost:${server.address().port}`); assert.equal(res.config.url, '/foo'); } finally { await stopHTTPServer(server); @@ -476,7 +523,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => it('should support params', async () => { const server = await startHTTPServer((req, res) => res.end(req.url), { port: SERVER_PORT }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/?test=1`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/?test=1`, { params: { foo: 1, bar: 2, @@ -491,7 +542,12 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => it('should handle fetch failed error as an AxiosError with ERR_NETWORK code', async () => { try { - await fetchAxios('http://notExistsUrl.in.nowhere'); + const instance = axios.create({ + adapter: 'fetch', + }); + + await instance.get('http://notExistsUrl.in.nowhere'); + assert.fail('should fail'); } catch (err) { assert.strictEqual(String(err), 'AxiosError: Network Error'); @@ -509,7 +565,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => ); try { - const { headers } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { headers } = await instance.get(`http://localhost:${server.address().port}/`, { responseType: 'stream', }); @@ -534,7 +594,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => ); try { - await fetchAxios.post(`http://localhost:${server.address().port}/form`, form); + const instance = axios.create({ + adapter: 'fetch', + }); + + await instance.post(`http://localhost:${server.address().port}/form`, form); } finally { await stopHTTPServer(server); } @@ -543,7 +607,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => describe('env config', () => { it('should respect env fetch API configuration', async () => { - const { data, headers } = await fetchAxios.get('/', { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data, headers } = await instance.get(`http://localhost:${SERVER_PORT}/`, { env: { fetch() { return { @@ -565,7 +633,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => form.append('x', '1'); - const { data, headers } = await fetchAxios.post('/', form, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data, headers } = await instance.post('/', form, { onUploadProgress() { // dummy listener to activate streaming }, @@ -587,7 +659,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); it('should be able to handle response with lack of Response object', async () => { - const { data, headers } = await fetchAxios.get('/', { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data, headers } = await instance.get('/', { onDownloadProgress() { // dummy listener to activate streaming }, @@ -613,7 +689,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { env: { fetch: undefined, }, @@ -640,7 +720,11 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT }); try { - const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, { + const instance = axios.create({ + adapter: 'fetch', + }); + + const { data } = await instance.get(`http://localhost:${server.address().port}/`, { env: { fetch: undefined, }, diff --git a/tests/unit/adapters/http.test.js b/tests/unit/adapters/http.test.js index c94d98a1..92ca8d05 100644 --- a/tests/unit/adapters/http.test.js +++ b/tests/unit/adapters/http.test.js @@ -1253,6 +1253,10 @@ describe('supports http with nodejs', () => { const closeServer = (server) => new Promise((resolve, reject) => { + if (typeof server.closeAllConnections === 'function') { + server.closeAllConnections(); + } + server.close((error) => { if (error) { reject(error); @@ -1335,6 +1339,223 @@ describe('supports http with nodejs', () => { } }); + it('should use CONNECT tunnel for HTTPS target via HTTP proxy', async () => { + const tlsOptions = { + key: fs.readFileSync(path.join(adaptersTestsDir, 'key.pem')), + cert: fs.readFileSync(path.join(adaptersTestsDir, 'cert.pem')), + }; + + const targetServer = await new Promise((resolve, reject) => { + const server = https + .createServer(tlsOptions, (req, res) => { + res.end('secure-data'); + }) + .listen(0, () => resolve(server)); + + server.on('error', reject); + }); + + let connectSeen = false; + let plaintextForwardSeen = false; + + const proxyServer = await new Promise((resolve, reject) => { + const server = http + .createServer((request, response) => { + plaintextForwardSeen = true; + response.statusCode = 500; + response.end('unexpected plaintext proxying'); + }) + .on('connect', (req, clientSocket, head) => { + connectSeen = true; + const serverSocket = net.connect(targetServer.address().port, 'localhost', () => { + clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); + if (head && head.length) { + serverSocket.write(head); + } + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); + }) + .listen(0, () => resolve(server)); + + server.on('error', reject); + }); + + try { + const response = await axios.get(`https://localhost:${targetServer.address().port}/`, { + proxy: { + host: 'localhost', + port: proxyServer.address().port, + protocol: 'http:', + }, + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + + assert.strictEqual(connectSeen, true, 'proxy should receive CONNECT request'); + assert.strictEqual( + plaintextForwardSeen, + false, + 'proxy should not receive plaintext HTTPS request data' + ); + assert.strictEqual(String(response.data), 'secure-data'); + } finally { + await Promise.all([stopHTTPServer(targetServer, 200), stopHTTPServer(proxyServer, 200)]); + } + }); + + it('should include Proxy-Authorization in CONNECT for authenticated HTTP proxy', async () => { + const tlsOptions = { + key: fs.readFileSync(path.join(adaptersTestsDir, 'key.pem')), + cert: fs.readFileSync(path.join(adaptersTestsDir, 'cert.pem')), + }; + + const targetServer = await new Promise((resolve, reject) => { + const server = https + .createServer(tlsOptions, (req, res) => { + res.end('ok'); + }) + .listen(0, () => resolve(server)); + + server.on('error', reject); + }); + + let proxyAuthorization; + + const proxyServer = await new Promise((resolve, reject) => { + const server = http + .createServer() + .on('connect', (req, clientSocket, head) => { + proxyAuthorization = req.headers['proxy-authorization']; + const serverSocket = net.connect(targetServer.address().port, 'localhost', () => { + clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); + if (head && head.length) { + serverSocket.write(head); + } + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); + }) + .listen(0, () => resolve(server)); + + server.on('error', reject); + }); + + try { + await axios.get(`https://localhost:${targetServer.address().port}/`, { + proxy: { + host: 'localhost', + port: proxyServer.address().port, + protocol: 'http:', + auth: { + username: 'user', + password: 'secret', + }, + }, + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + + assert.ok(proxyAuthorization, 'Proxy-Authorization should be set on CONNECT request'); + assert.strictEqual( + proxyAuthorization, + 'Basic ' + Buffer.from('user:secret').toString('base64') + ); + } finally { + await Promise.all([stopHTTPServer(targetServer, 200), stopHTTPServer(proxyServer, 200)]); + } + }); + + it('should use CONNECT tunnel for HTTPS redirect via HTTP proxy', async () => { + const tlsOptions = { + key: fs.readFileSync(path.join(adaptersTestsDir, 'key.pem')), + cert: fs.readFileSync(path.join(adaptersTestsDir, 'cert.pem')), + }; + + const httpsTargetServer = await new Promise((resolve, reject) => { + const server = https + .createServer(tlsOptions, (req, res) => { + res.end('redirected-data'); + }) + .listen(0, () => resolve(server)); + + server.on('error', reject); + }); + + const redirectServer = await startHTTPServer( + (req, res) => { + res.writeHead(302, { Location: `https://localhost:${httpsTargetServer.address().port}/` }); + res.end(); + }, + { port: 0 } + ); + + let connectCount = 0; + + const proxyServer = await new Promise((resolve, reject) => { + const server = http + .createServer((request, response) => { + const parsed = new URL(request.url); + const opts = { + host: parsed.hostname, + port: parsed.port, + path: `${parsed.pathname}${parsed.search}`, + method: request.method, + }; + + const proxyRequest = http.request(opts, (res) => { + response.writeHead(res.statusCode || 500, res.headers); + stream.pipeline(res, response, () => {}); + }); + + proxyRequest.on('error', () => { + response.statusCode = 502; + response.end(); + }); + + request.pipe(proxyRequest); + }) + .on('connect', (req, clientSocket, head) => { + connectCount += 1; + const serverSocket = net.connect(httpsTargetServer.address().port, 'localhost', () => { + clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); + if (head && head.length) { + serverSocket.write(head); + } + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); + }) + .listen(0, () => resolve(server)); + + server.on('error', reject); + }); + + try { + const response = await axios.get(`http://localhost:${redirectServer.address().port}/`, { + proxy: { + host: 'localhost', + port: proxyServer.address().port, + protocol: 'http:', + }, + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + + assert.ok(connectCount >= 1, 'CONNECT should be used for HTTPS redirect'); + assert.strictEqual(String(response.data), 'redirected-data'); + } finally { + await Promise.all([ + stopHTTPServer(redirectServer, 200), + stopHTTPServer(httpsTargetServer, 200), + stopHTTPServer(proxyServer, 200), + ]); + } + }); + it('should not pass through disabled proxy', async () => { const originalHttpProxy = process.env.http_proxy; process.env.http_proxy = 'http://does-not-exists.example.com:4242/';