From 8bb5b4a557f2a46f300b79745982788c59f765f7 Mon Sep 17 00:00:00 2001 From: Jason Williams <936006+jasonwilliams@users.noreply.github.com> Date: Sun, 10 Jul 2022 14:54:31 +0100 Subject: [PATCH] Desktop: Resolves #164: Add support for proxy (#6537) --- cspell.json | 3 +- packages/lib/BaseApplication.ts | 12 +++++++ packages/lib/models/Setting.ts | 32 ++++++++++++++++++- packages/lib/package.json | 1 + packages/lib/shim-init-node.js | 56 +++++++++++++++++++++++++++++++-- packages/lib/shim.ts | 2 +- yarn.lock | 8 +++++ 7 files changed, 108 insertions(+), 6 deletions(-) diff --git a/cspell.json b/cspell.json index 2c79c27b1a..fb618b0fd4 100644 --- a/cspell.json +++ b/cspell.json @@ -325,6 +325,7 @@ "homenote", "hotfolder", "Howver", + "hpagent", "Hrvatska", "htmlentities", "htmlfile", @@ -950,4 +951,4 @@ "မြန်မာ", "កម្ពុជា" ] -} \ No newline at end of file +} diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 2cb9763fe6..0297ec005e 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -1,6 +1,7 @@ import Setting, { Env } from './models/Setting'; import Logger, { TargetType, LoggerWrapper } from './Logger'; import shim from './shim'; +const { setupProxySettings } = require('./shim-init-node'); import BaseService from './services/BaseService'; import reducer, { setStore } from './reducer'; import KeychainServiceDriver from './services/keychain/KeychainServiceDriver.node'; @@ -456,6 +457,14 @@ export default class BaseApplication { syswidecas.addCAs(f); } }, + 'net.proxyEnabled': async () => { + setupProxySettings({ + maxConcurrentConnections: Setting.value('sync.maxConcurrentConnections'), + proxyTimeout: Setting.value('net.proxyTimeout'), + proxyEnabled: Setting.value('net.proxyEnabled'), + proxyUrl: Setting.value('net.proxyUrl'), + }); + }, // Note: this used to run when "encryption.enabled" was changed, but // now we run it anytime any property of the sync target info is @@ -491,6 +500,9 @@ export default class BaseApplication { sideEffects['locale'] = sideEffects['dateFormat']; sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache']; sideEffects['encryption.masterPassword'] = sideEffects['syncInfoCache']; + sideEffects['sync.maxConcurrentConnections'] = sideEffects['net.proxyEnabled']; + sideEffects['sync.proxyTimeout'] = sideEffects['net.proxyEnabled']; + sideEffects['sync.proxyUrl'] = sideEffects['net.proxyEnabled']; if (action) { const effect = sideEffects[action.key]; diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 4b511ca2ca..af48f1afa2 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -1384,7 +1384,37 @@ class Setting extends BaseModel { label: () => _('Ignore TLS certificate errors'), storage: SettingStorage.File, }, - + 'net.proxyEnabled': { + value: false, + type: SettingItemType.Bool, + advanced: true, + section: 'sync', + isGlobal: true, + public: true, + label: () => _('Proxy enabled (beta)'), + storage: SettingStorage.File, + }, + 'net.proxyUrl': { + value: '', + type: SettingItemType.String, + advanced: true, + section: 'sync', + isGlobal: true, + public: true, + label: () => _('Proxy URL (beta)'), + description: () => _('e.g "http://my.proxy.com:80". You can also set via environment variables'), + storage: SettingStorage.File, + }, + 'net.proxyTimeout': { + value: 1, + type: SettingItemType.Int, + advanced: true, + section: 'sync', + isGlobal: true, + public: true, + label: () => _('proxy timeout (seconds) (beta)'), + storage: SettingStorage.File, + }, 'sync.wipeOutFailSafe': { value: true, type: SettingItemType.Bool, diff --git a/packages/lib/package.json b/packages/lib/package.json index fe719129f2..a5fb4e9828 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -53,6 +53,7 @@ "follow-redirects": "^1.2.4", "form-data": "^2.1.4", "fs-extra": "^5.0.0", + "hpagent": "^1.0.0", "html-entities": "^1.2.1", "html-minifier": "^3.5.15", "image-data-uri": "^2.0.0", diff --git a/packages/lib/shim-init-node.js b/packages/lib/shim-init-node.js index 114c4f998c..895a1a1131 100644 --- a/packages/lib/shim-init-node.js +++ b/packages/lib/shim-init-node.js @@ -13,12 +13,15 @@ const urlValidator = require('valid-url'); const { _ } = require('./locale'); const http = require('http'); const https = require('https'); +const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent'); const toRelative = require('relative'); const timers = require('timers'); const zlib = require('zlib'); const dgram = require('dgram'); const { basename, fileExtension, safeFileExtension } = require('./path-utils'); +const proxySettings = {}; + function fileExists(filePath) { try { return fs.statSync(filePath).isFile(); @@ -27,6 +30,20 @@ function fileExists(filePath) { } } +function isUrlHttps(url) { + return url.startsWith('https'); +} + +function resolveProxyUrl(proxyUrl) { + return ( + proxyUrl || + process.env['http_proxy'] || + process.env['https_proxy'] || + process.env['HTTP_PROXY'] || + process.env['HTTPS_PROXY'] + ); +} + // https://github.com/sindresorhus/callsites/blob/main/index.js function callsites() { const _prepareStackTrace = Error.prepareStackTrace; @@ -64,6 +81,13 @@ const gunzipFile = function(source, destination) { }); }; +function setupProxySettings(options) { + proxySettings.maxConcurrentConnections = options.maxConcurrentConnections; + proxySettings.proxyTimeout = options.proxyTimeout; + proxySettings.proxyEnabled = options.proxyEnabled; + proxySettings.proxyUrl = options.proxyUrl; +} + function shimInit(options = null) { options = { sharp: null, @@ -79,6 +103,7 @@ function shimInit(options = null) { const keytar = (shim.isWindows() || shim.isMac()) && !shim.isPortable() ? options.keytar : null; const appVersion = options.appVersion; + shim.setNodeSqlite(options.nodeSqlite); shim.fsDriver = () => { @@ -420,10 +445,11 @@ function shimInit(options = null) { return new Buffer(data).toString('base64'); }; - shim.fetch = async function(url, options = null) { + shim.fetch = async function(url, options = {}) { const validatedUrl = urlValidator.isUri(url); if (!validatedUrl) throw new Error(`Not a valid URL: ${url}`); - + const resolvedProxyUrl = resolveProxyUrl(proxySettings.proxyUrl); + options.agent = (resolvedProxyUrl && proxySettings.proxyEnabled) ? shim.proxyAgent(url, resolvedProxyUrl) : null; return shim.fetchWithRetry(() => { return nodeFetch(url, options); }, options); @@ -466,6 +492,9 @@ function shimInit(options = null) { headers: headers, }; + const resolvedProxyUrl = resolveProxyUrl(proxySettings.proxyUrl); + requestOptions.agent = (resolvedProxyUrl && proxySettings.proxyEnabled) ? shim.proxyAgent(url, resolvedProxyUrl) : null; + const doFetchOperation = async () => { return new Promise((resolve, reject) => { let file = null; @@ -572,6 +601,27 @@ function shimInit(options = null) { return url.startsWith('https') ? shim.httpAgent_.https : shim.httpAgent_.http; }; + shim.proxyAgent = (serverUrl, proxyUrl) => { + const proxyAgentConfig = { + keepAlive: true, + maxSockets: proxySettings.maxConcurrentConnections, + keepAliveMsecs: 5000, + proxy: proxyUrl, + timeout: proxySettings.proxyTimeout * 1000, + }; + + // Based on https://github.com/delvedor/hpagent#usage + if (!isUrlHttps(proxyUrl) && !isUrlHttps(serverUrl)) { + return new HttpProxyAgent(proxyAgentConfig); + } else if (isUrlHttps(proxyUrl) && !isUrlHttps(serverUrl)) { + return new HttpProxyAgent(proxyAgentConfig); + } else if (!isUrlHttps(proxyUrl) && isUrlHttps(serverUrl)) { + return new HttpsProxyAgent(proxyAgentConfig); + } else { + return new HttpsProxyAgent(proxyAgentConfig); + } + }; + shim.openOrCreateFile = (filepath, defaultContents) => { // If the file doesn't exist, create it if (!fs.existsSync(filepath)) { @@ -634,4 +684,4 @@ function shimInit(options = null) { }; } -module.exports = { shimInit }; +module.exports = { shimInit, setupProxySettings }; diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index a9495ad291..26dc7b1bf5 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -16,7 +16,7 @@ let isTestingEnv_ = false; // (app-desktop, app-mobile, etc.) since we are sure they won't be dependency to // other packages (unlike the lib which can be included anywhere). // -// Regarding the type - althought we import React, we only use it as a type +// Regarding the type - although we import React, we only use it as a type // using `typeof React`. This is just to get types in hooks. // // https://stackoverflow.com/a/42816077/561309 diff --git a/yarn.lock b/yarn.lock index 722a43a66a..4d92927990 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3436,6 +3436,7 @@ __metadata: follow-redirects: ^1.2.4 form-data: ^2.1.4 fs-extra: ^5.0.0 + hpagent: ^1.0.0 html-entities: ^1.2.1 html-minifier: ^3.5.15 image-data-uri: ^2.0.0 @@ -16672,6 +16673,13 @@ __metadata: languageName: node linkType: hard +"hpagent@npm:^1.0.0": + version: 1.0.0 + resolution: "hpagent@npm:1.0.0" + checksum: 911a9ba612747f5c9403fb60c1abd0c47a27fa634826caad9aecad41b899533489da36c92758b5cd83576e2bf9e84e23a4374a40aa513106d5450f62856c4b2d + languageName: node + linkType: hard + "html-encoding-sniffer@npm:^1.0.2": version: 1.0.2 resolution: "html-encoding-sniffer@npm:1.0.2"