diff --git a/background.ts b/background.ts index 8d2d21fc56d..dcef4c0b80d 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'); @@ -55,10 +54,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) { @@ -212,52 +208,6 @@ Electron.ipcMain.handle('settings-write', (event, arg: Partial { - 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 a4df714b6d0..068772cb944 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", @@ -3901,6 +3905,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", @@ -5175,6 +5187,17 @@ "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, + "node_modules/agent-base": { + "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" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -12299,6 +12322,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", @@ -12331,6 +12367,18 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "node_modules/https-proxy-agent": { + "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": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -12705,8 +12753,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", @@ -20610,8 +20657,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" @@ -20805,6 +20850,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", @@ -27788,6 +27859,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", @@ -29010,6 +29086,14 @@ "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, + "agent-base": { + "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", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -34999,6 +35083,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", @@ -35024,6 +35118,15 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "https-proxy-agent": { + "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": "6", + "debug": "4" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -35321,8 +35424,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", @@ -41877,9 +41979,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", @@ -42037,6 +42137,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 9231a536bfe..81c81ea3c94 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,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", @@ -35,6 +36,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", @@ -44,6 +47,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); + } +}