From 83d1a15586a713ec80f1db25b048c9a3e92b09f1 Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Fri, 6 Aug 2021 16:09:25 -0700 Subject: [PATCH] background: Support for system proxy configuration. This adds support for use the system proxy configuration (by asking the embedded Chrome to resolve the proxy configuration). The idea to ask Chrome was from the electron-proxy-agent package; however, it had significant issues on supporting system CA certificates, and the result ended up being a complete rewrite. We need the wrapper classes for HttpsProxyAgent and SocksProxyAgent so that we can pass the CA options down to the eventual tls.connect() call. This is due to https://github.com/TooTallNate/node-https-proxy-agent/issues/89 Signed-off-by: Mark Yen --- background.ts | 54 +---------- package-lock.json | 175 +++++++++++++++++++++++++++++------ package.json | 4 + src/main/networking/index.ts | 74 +++++++++++++++ src/main/networking/proxy.ts | 99 ++++++++++++++++++++ 5 files changed, 326 insertions(+), 80 deletions(-) create mode 100644 src/main/networking/index.ts create mode 100644 src/main/networking/proxy.ts diff --git a/background.ts b/background.ts index 393998092fa..067bbc813c8 100644 --- a/background.ts +++ b/background.ts @@ -6,8 +6,6 @@ import { URL } from 'url'; import Electron from 'electron'; import _ from 'lodash'; -import MacCA from 'mac-ca'; -import WinCA from 'win-ca'; import mainEvents from '@/main/mainEvents'; import { setupKim } from '@/main/kim'; @@ -18,6 +16,7 @@ import * as K8s from '@/k8s-engine/k8s'; import resources from '@/resources'; import Logging from '@/utils/logging'; import * as childProcess from '@/utils/childProcess'; +import setupNetworking from '@/main/networking'; import setupUpdate from '@/main/update'; Electron.app.setName('Rancher Desktop'); @@ -53,10 +52,7 @@ Electron.app.whenReady().then(async() => { } catch (err) { console.log(`Can't get app version: ${ err }`); } - if (os.platform().startsWith('win')) { - // Inject the Windows certs. - WinCA({ inject: '+' }); - } + setupNetworking(); try { tray = new Tray(); } catch (e) { @@ -220,52 +216,6 @@ Electron.ipcMain.handle('settings-write', (event, arg: RecursivePartial { - if (error === 'net::ERR_CERT_INVALID') { - // If we're getting *this* particular error, it means it's an untrusted cert. - // Ask the system store. - console.log(`Attempting to check system certificates for ${ url } (${ certificate.subjectName }/${ certificate.fingerprint })`); - if (os.platform().startsWith('win')) { - const certs: string[] = []; - - WinCA({ - format: WinCA.der2.pem, ondata: certs, fallback: false - }); - for (const cert of certs) { - // For now, just check that the PEM data matches exactly; this is - // probably a little more strict than necessary, but avoids issues like - // an attacker generating a cert with the same serial. - if (cert === certificate.data) { - console.log(`Accepting system certificate for ${ certificate.subjectName } (${ certificate.fingerprint })`); - // eslint-disable-next-line node/no-callback-literal - callback(true); - - return; - } - } - } else if (os.platform() === 'darwin') { - for (const cert of MacCA.all(MacCA.der2.pem)) { - // For now, just check that the PEM data matches exactly; this is - // probably a little more strict than necessary, but avoids issues like - // an attacker generating a cert with the same serial. - if (cert === certificate.data) { - console.log(`Accepting system certificate for ${ certificate.subjectName } (${ certificate.fingerprint })`); - // eslint-disable-next-line node/no-callback-literal - callback(true); - - return; - } - } - } - } - - console.log(`Not handling certificate error ${ error } for ${ url }`); - - // eslint-disable-next-line node/no-callback-literal - callback(false); -}); - Electron.ipcMain.on('k8s-state', (event) => { event.returnValue = k8smanager.state; }); diff --git a/package-lock.json b/package-lock.json index 22b8bc51511..698e5c4a31c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "Apache-2.0", "dependencies": { "@kubernetes/client-node": "^0.13.0", + "agent-base": "^6.0.2", "bufferutil": "^4.0.3", "cookie-universal-nuxt": "^2.0.17", "core-js": "^3.8.1", @@ -19,6 +20,8 @@ "dompurify": "^2.2.9", "electron-updater": "^4.3.9", "fs-extra": "^10.0.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", "intl-messageformat": "^7.8.4", "jquery": "^3.5.1", "jsonpath": "^1.0.2", @@ -28,6 +31,7 @@ "node-fetch": "^2.6.1", "sass": "^1.32.2", "semver": "^7.3.5", + "socks-proxy-agent": "^6.0.0", "sudo-prompt": "^9.2.1", "utf-8-validate": "^5.0.4", "vue": "^2.6.12", @@ -3911,6 +3915,14 @@ "node": ">=6" } }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -5342,10 +5354,12 @@ "dev": true }, "node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true, + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, "engines": { "node": ">= 6.0.0" } @@ -12754,6 +12768,19 @@ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", "dev": true }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -12787,16 +12814,15 @@ "dev": true }, "node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "dependencies": { - "agent-base": "5", + "agent-base": "6", "debug": "4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 6" } }, "node_modules/human-signals": { @@ -13173,8 +13199,7 @@ "node_modules/ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" }, "node_modules/is-absolute-url": { "version": "2.1.0", @@ -19428,6 +19453,15 @@ "node": ">=10.18.1" } }, + "node_modules/puppeteer-core/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/puppeteer-core/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -19441,6 +19475,19 @@ "node": ">=8" } }, + "node_modules/puppeteer-core/node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/puppeteer-core/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -21341,8 +21388,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", - "dev": true, - "optional": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -21536,6 +21581,32 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/socks": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", + "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.0.0.tgz", + "integrity": "sha512-FIgZbQWlnjVEQvMkylz64/rUggGtrKstPnx8OZyYFG0tAFR8CSBtpXxSwbFLHyeXFn/cunFL7MpuSOvDSOPo9g==", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -29082,6 +29153,11 @@ "defer-to-connect": "^1.0.1" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, "@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -30427,10 +30503,12 @@ "dev": true }, "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } }, "aggregate-error": { "version": "3.1.0", @@ -36659,6 +36737,16 @@ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", "dev": true }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -36685,12 +36773,11 @@ "dev": true }, "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "requires": { - "agent-base": "5", + "agent-base": "6", "debug": "4" } }, @@ -36991,8 +37078,7 @@ "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" }, "is-absolute-url": { "version": "2.1.0", @@ -42165,6 +42251,12 @@ "ws": "^7.2.3" }, "dependencies": { + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -42175,6 +42267,16 @@ "path-exists": "^4.0.0" } }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "requires": { + "agent-base": "5", + "debug": "4" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -43770,9 +43872,7 @@ "smart-buffer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", - "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", - "dev": true, - "optional": true + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" }, "snapdragon": { "version": "0.8.2", @@ -43930,6 +44030,25 @@ } } }, + "socks": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", + "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.0.0.tgz", + "integrity": "sha512-FIgZbQWlnjVEQvMkylz64/rUggGtrKstPnx8OZyYFG0tAFR8CSBtpXxSwbFLHyeXFn/cunFL7MpuSOvDSOPo9g==", + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + } + }, "sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", diff --git a/package.json b/package.json index 314c8667874..4a06c4963c0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "main": "dist/app/background.js", "dependencies": { "@kubernetes/client-node": "^0.13.0", + "agent-base": "^6.0.2", "bufferutil": "^4.0.3", "cookie-universal-nuxt": "^2.0.17", "core-js": "^3.8.1", @@ -36,6 +37,8 @@ "dompurify": "^2.2.9", "electron-updater": "^4.3.9", "fs-extra": "^10.0.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", "intl-messageformat": "^7.8.4", "jquery": "^3.5.1", "jsonpath": "^1.0.2", @@ -45,6 +48,7 @@ "node-fetch": "^2.6.1", "sass": "^1.32.2", "semver": "^7.3.5", + "socks-proxy-agent": "^6.0.0", "sudo-prompt": "^9.2.1", "utf-8-validate": "^5.0.4", "vue": "^2.6.12", diff --git a/src/main/networking/index.ts b/src/main/networking/index.ts new file mode 100644 index 00000000000..387ce9e206c --- /dev/null +++ b/src/main/networking/index.ts @@ -0,0 +1,74 @@ +import http from 'http'; +import https from 'https'; +import os from 'os'; + +import Electron from 'electron'; +import MacCA from 'mac-ca'; +import WinCA from 'win-ca'; + +import ElectronProxyAgent from './proxy'; + +export default function setupNetworking() { + const session = Electron.session.defaultSession; + + const httpAgent = new ElectronProxyAgent(https.globalAgent.options, session); + + httpAgent.protocol = 'http:'; + http.globalAgent = httpAgent; + + const httpsAgent = new ElectronProxyAgent(https.globalAgent.options, session); + + httpsAgent.protocol = 'https:'; + https.globalAgent = httpsAgent; + + if (os.platform().startsWith('win')) { + // Inject the Windows certs. + WinCA({ inject: '+' }); + } +} + +// Set up certificate handling for system certificates on Windows and macOS +Electron.app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { + if (error === 'net::ERR_CERT_INVALID') { + // If we're getting *this* particular error, it means it's an untrusted cert. + // Ask the system store. + console.log(`Attempting to check system certificates for ${ url } (${ certificate.subjectName }/${ certificate.fingerprint })`); + if (os.platform().startsWith('win')) { + const certs: string[] = []; + + WinCA({ + format: WinCA.der2.pem, ondata: certs, fallback: false + }); + for (const cert of certs) { + // For now, just check that the PEM data matches exactly; this is + // probably a little more strict than necessary, but avoids issues like + // an attacker generating a cert with the same serial. + if (cert === certificate.data) { + console.log(`Accepting system certificate for ${ certificate.subjectName } (${ certificate.fingerprint })`); + // eslint-disable-next-line node/no-callback-literal + callback(true); + + return; + } + } + } else if (os.platform() === 'darwin') { + for (const cert of MacCA.all(MacCA.der2.pem)) { + // For now, just check that the PEM data matches exactly; this is + // probably a little more strict than necessary, but avoids issues like + // an attacker generating a cert with the same serial. + if (cert === certificate.data) { + console.log(`Accepting system certificate for ${ certificate.subjectName } (${ certificate.fingerprint })`); + // eslint-disable-next-line node/no-callback-literal + callback(true); + + return; + } + } + } + } + + console.log(`Not handling certificate error ${ error } for ${ url }`); + + // eslint-disable-next-line node/no-callback-literal + callback(false); +}); diff --git a/src/main/networking/proxy.ts b/src/main/networking/proxy.ts new file mode 100644 index 00000000000..a91fdb79f6c --- /dev/null +++ b/src/main/networking/proxy.ts @@ -0,0 +1,99 @@ +import { Console } from 'console'; +import { AgentOptions as HttpsAgentOptions } from 'https'; +import net from 'net'; +import tls from 'tls'; +import { URL } from 'url'; + +import { Agent, ClientRequest, RequestOptions, AgentCallbackReturn } from 'agent-base'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { SocksProxyAgent } from 'socks-proxy-agent'; + +import Logging from '@/utils/logging'; + +const console = new Console(Logging.background.stream); + +export default class ElectronProxyAgent extends Agent { + protected session: Electron.Session; + + constructor(options?: HttpsAgentOptions, session?: Electron.Session) { + super(); + this.options = options || this.options || {}; + this.session = session || Electron.session.defaultSession; + } + + async callback(req: ClientRequest, opts: RequestOptions): Promise { + const port = opts.port || (opts.secureEndpoint ? 443 : 80); + const requestURL = new URL(`${ req.protocol }//${ req.host }:${ port }/${ req.path }`); + const mergedOptions = Object.assign({}, this.options, opts); + + // proxies is a string as in a proxy auto-config file + // https://en.wikipedia.org/wiki/Proxy_auto-config + const proxies = (await this.session.resolveProxy(requestURL.toString())) || 'DIRECT'; + + for (const proxy of proxies.split(';')) { + const [_, mode, host] = /\s*(\S+)\s+(\S+?:\d+)/.exec(proxy) || []; + + switch (mode) { + case 'DIRECT': + return opts.secureEndpoint ? tls.connect(mergedOptions) : net.connect(mergedOptions); + case 'SOCKS': case 'SOCKS4': case 'SOCKS5': + return new CustomSocksProxyAgent(`socks://${ host }`, this.options); + case 'PROXY': case 'HTTP': case 'HTTPS': { + const protocol = mode === 'HTTPS' ? 'https' : 'http'; + const proxyURL = `${ protocol }://${ host }`; + + if (opts.secureEndpoint) { + return new CustomHttpsProxyAgent(proxyURL, this.options); + } else { + return HttpProxyAgent(proxyURL); + } + } + default: + console.log(`Skipping unknown proxy configuration ${ mode } ${ host }`); + } + } + + console.log('No valid proxy configuration found, falling back to DIRECT.'); + + return opts.secureEndpoint ? tls.connect(mergedOptions) : net.connect(mergedOptions); + } +} + +class CustomHttpsProxyAgent extends HttpsProxyAgent { + constructor(proxyURL: string, opts: HttpsAgentOptions) { + // Use object destructing here to ensure we only get wanted properties. + const { hostname, port, protocol } = new URL(proxyURL); + const mergedOpts = Object.assign({}, opts, { + hostname, port, protocol + }); + + super(mergedOpts); + this.options = opts; + } + + callback(req: ClientRequest, opts: RequestOptions): Promise { + const mergedOptions = Object.assign({}, this.options, opts); + + return super.callback(req, mergedOptions); + } +} + +class CustomSocksProxyAgent extends SocksProxyAgent { + constructor(proxyURL: string, opts: HttpsAgentOptions) { + // Use object destructing here to ensure we only get wanted properties. + const { hostname, port, protocol } = new URL(proxyURL); + const mergedOpts = Object.assign({}, opts, { + hostname, port, protocol + }); + + super(mergedOpts); + this.options = opts; + } + + callback(req: ClientRequest, opts: RequestOptions): Promise { + const mergedOptions = Object.assign({}, this.options, opts); + + return super.callback(req, mergedOptions); + } +}