Browse Source

feat: add support for configuring system network context proxies (#41335)

* feat: add support for configuring system network context proxies

* chore: add specs

* chore: fix lint

* fix: address review feedback
Robo 1 year ago
parent
commit
26131b23b8

+ 18 - 0
docs/api/app.md

@@ -1468,6 +1468,24 @@ details.
 
 **Note:** Enable `Secure Keyboard Entry` only when it is needed and disable it when it is no longer needed.
 
+### `app.setProxy(config)`
+
+* `config` [ProxyConfig](structures/proxy-config.md)
+
+Returns `Promise<void>` - Resolves when the proxy setting process is complete.
+
+Sets the proxy settings for networks requests made without an associated [Session](session.md).
+Currently this will affect requests made with [Net](net.md) in the [utility process](../glossary.md#utility-process)
+and internal requests made by the runtime (ex: geolocation queries).
+
+This method can only be called after app is ready.
+
+#### `app.resolveProxy(url)`
+
+* `url` URL
+
+Returns `Promise<string>` - Resolves with the proxy information for `url` that will be used when attempting to make requests using [Net](net.md) in the [utility process](../glossary.md#utility-process).
+
 ## Properties
 
 ### `app.accessibilitySupportEnabled` _macOS_ _Windows_

+ 1 - 91
docs/api/session.md

@@ -589,105 +589,15 @@ Writes any unwritten DOMStorage data to disk.
 
 #### `ses.setProxy(config)`
 
-* `config` Object
-  * `mode` string (optional) - The proxy mode. Should be one of `direct`,
-    `auto_detect`, `pac_script`, `fixed_servers` or `system`. If it's
-    unspecified, it will be automatically determined based on other specified
-    options.
-    * `direct`
-      In direct mode all connections are created directly, without any proxy involved.
-    * `auto_detect`
-      In auto_detect mode the proxy configuration is determined by a PAC script that can
-      be downloaded at http://wpad/wpad.dat.
-    * `pac_script`
-      In pac_script mode the proxy configuration is determined by a PAC script that is
-      retrieved from the URL specified in the `pacScript`. This is the default mode
-      if `pacScript` is specified.
-    * `fixed_servers`
-      In fixed_servers mode the proxy configuration is specified in `proxyRules`.
-      This is the default mode if `proxyRules` is specified.
-    * `system`
-      In system mode the proxy configuration is taken from the operating system.
-      Note that the system mode is different from setting no proxy configuration.
-      In the latter case, Electron falls back to the system settings
-      only if no command-line options influence the proxy configuration.
-  * `pacScript` string (optional) - The URL associated with the PAC file.
-  * `proxyRules` string (optional) - Rules indicating which proxies to use.
-  * `proxyBypassRules` string (optional) - Rules indicating which URLs should
-    bypass the proxy settings.
+* `config` [ProxyConfig](structures/proxy-config.md)
 
 Returns `Promise<void>` - Resolves when the proxy setting process is complete.
 
 Sets the proxy settings.
 
-When `mode` is unspecified, `pacScript` and `proxyRules` are provided together, the `proxyRules`
-option is ignored and `pacScript` configuration is applied.
-
 You may need `ses.closeAllConnections` to close currently in flight connections to prevent
 pooled sockets using previous proxy from being reused by future requests.
 
-The `proxyRules` has to follow the rules below:
-
-```sh
-proxyRules = schemeProxies[";"<schemeProxies>]
-schemeProxies = [<urlScheme>"="]<proxyURIList>
-urlScheme = "http" | "https" | "ftp" | "socks"
-proxyURIList = <proxyURL>[","<proxyURIList>]
-proxyURL = [<proxyScheme>"://"]<proxyHost>[":"<proxyPort>]
-```
-
-For example:
-
-* `http=foopy:80;ftp=foopy2` - Use HTTP proxy `foopy:80` for `http://` URLs, and
-  HTTP proxy `foopy2:80` for `ftp://` URLs.
-* `foopy:80` - Use HTTP proxy `foopy:80` for all URLs.
-* `foopy:80,bar,direct://` - Use HTTP proxy `foopy:80` for all URLs, failing
-  over to `bar` if `foopy:80` is unavailable, and after that using no proxy.
-* `socks4://foopy` - Use SOCKS v4 proxy `foopy:1080` for all URLs.
-* `http=foopy,socks5://bar.com` - Use HTTP proxy `foopy` for http URLs, and fail
-  over to the SOCKS5 proxy `bar.com` if `foopy` is unavailable.
-* `http=foopy,direct://` - Use HTTP proxy `foopy` for http URLs, and use no
-  proxy if `foopy` is unavailable.
-* `http=foopy;socks=foopy2` - Use HTTP proxy `foopy` for http URLs, and use
-  `socks4://foopy2` for all other URLs.
-
-The `proxyBypassRules` is a comma separated list of rules described below:
-
-* `[ URL_SCHEME "://" ] HOSTNAME_PATTERN [ ":" <port> ]`
-
-   Match all hostnames that match the pattern HOSTNAME_PATTERN.
-
-   Examples:
-     "foobar.com", "\*foobar.com", "\*.foobar.com", "\*foobar.com:99",
-     "https://x.\*.y.com:99"
-
-* `"." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ]`
-
-   Match a particular domain suffix.
-
-   Examples:
-     ".google.com", ".com", "http://.google.com"
-
-* `[ SCHEME "://" ] IP_LITERAL [ ":" PORT ]`
-
-   Match URLs which are IP address literals.
-
-   Examples:
-     "127.0.1", "\[0:0::1]", "\[::1]", "http://\[::1]:99"
-
-* `IP_LITERAL "/" PREFIX_LENGTH_IN_BITS`
-
-   Match any URL that is to an IP literal that falls between the
-   given range. IP range is specified using CIDR notation.
-
-   Examples:
-     "192.168.1.1/16", "fefe:13::abc/33".
-
-* `<local>`
-
-   Match local addresses. The meaning of `<local>` is whether the
-   host matches one of: "127.0.0.1", "::1", "localhost".
-
 #### `ses.resolveHost(host, [options])`
 
 * `host` string - Hostname to resolve.

+ 86 - 0
docs/api/structures/proxy-config.md

@@ -0,0 +1,86 @@
+# ProxyConfig Object
+
+* `mode` string (optional) - The proxy mode. Should be one of `direct`,
+`auto_detect`, `pac_script`, `fixed_servers` or `system`.
+Defaults to `pac_script` proxy mode if `pacScript` option is specified
+otherwise defaults to `fixed_servers`.
+  * `direct` - In direct mode all connections are created directly, without any proxy involved.
+  * `auto_detect` - In auto_detect mode the proxy configuration is determined by a PAC script that can
+    be downloaded at http://wpad/wpad.dat.
+  * `pac_script` - In pac_script mode the proxy configuration is determined by a PAC script that is
+    retrieved from the URL specified in the `pacScript`. This is the default mode if `pacScript` is specified.
+  * `fixed_servers` - In fixed_servers mode the proxy configuration is specified in `proxyRules`.
+    This is the default mode if `proxyRules` is specified.
+  * `system` - In system mode the proxy configuration is taken from the operating system.
+    Note that the system mode is different from setting no proxy configuration.
+    In the latter case, Electron falls back to the system settings only if no
+    command-line options influence the proxy configuration.
+* `pacScript` string (optional) - The URL associated with the PAC file.
+* `proxyRules` string (optional) - Rules indicating which proxies to use.
+* `proxyBypassRules` string (optional) - Rules indicating which URLs should
+bypass the proxy settings.
+
+When `mode` is unspecified, `pacScript` and `proxyRules` are provided together, the `proxyRules`
+option is ignored and `pacScript` configuration is applied.
+
+The `proxyRules` has to follow the rules below:
+
+```sh
+proxyRules = schemeProxies[";"<schemeProxies>]
+schemeProxies = [<urlScheme>"="]<proxyURIList>
+urlScheme = "http" | "https" | "ftp" | "socks"
+proxyURIList = <proxyURL>[","<proxyURIList>]
+proxyURL = [<proxyScheme>"://"]<proxyHost>[":"<proxyPort>]
+```
+
+For example:
+
+* `http=foopy:80;ftp=foopy2` - Use HTTP proxy `foopy:80` for `http://` URLs, and
+  HTTP proxy `foopy2:80` for `ftp://` URLs.
+* `foopy:80` - Use HTTP proxy `foopy:80` for all URLs.
+* `foopy:80,bar,direct://` - Use HTTP proxy `foopy:80` for all URLs, failing
+  over to `bar` if `foopy:80` is unavailable, and after that using no proxy.
+* `socks4://foopy` - Use SOCKS v4 proxy `foopy:1080` for all URLs.
+* `http=foopy,socks5://bar.com` - Use HTTP proxy `foopy` for http URLs, and fail
+  over to the SOCKS5 proxy `bar.com` if `foopy` is unavailable.
+* `http=foopy,direct://` - Use HTTP proxy `foopy` for http URLs, and use no
+  proxy if `foopy` is unavailable.
+* `http=foopy;socks=foopy2` - Use HTTP proxy `foopy` for http URLs, and use
+  `socks4://foopy2` for all other URLs.
+
+The `proxyBypassRules` is a comma separated list of rules described below:
+
+* `[ URL_SCHEME "://" ] HOSTNAME_PATTERN [ ":" <port> ]`
+
+   Match all hostnames that match the pattern HOSTNAME_PATTERN.
+
+   Examples:
+     "foobar.com", "\*foobar.com", "\*.foobar.com", "\*foobar.com:99",
+     "https://x.\*.y.com:99"
+
+* `"." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ]`
+
+   Match a particular domain suffix.
+
+   Examples:
+     ".google.com", ".com", "http://.google.com"
+
+* `[ SCHEME "://" ] IP_LITERAL [ ":" PORT ]`
+
+   Match URLs which are IP address literals.
+
+   Examples:
+     "127.0.1", "\[0:0::1]", "\[::1]", "http://\[::1]:99"
+
+* `IP_LITERAL "/" PREFIX_LENGTH_IN_BITS`
+
+   Match any URL that is to an IP literal that falls between the
+   given range. IP range is specified using CIDR notation.
+
+   Examples:
+     "192.168.1.1/16", "fefe:13::abc/33".
+
+* `<local>`
+
+   Match local addresses. The meaning of `<local>` is whether the
+   host matches one of: "127.0.0.1", "::1", "localhost".

+ 1 - 0
filenames.auto.gni

@@ -118,6 +118,7 @@ auto_filenames = {
     "docs/api/structures/protocol-request.md",
     "docs/api/structures/protocol-response-upload-data.md",
     "docs/api/structures/protocol-response.md",
+    "docs/api/structures/proxy-config.md",
     "docs/api/structures/rectangle.md",
     "docs/api/structures/referrer.md",
     "docs/api/structures/render-process-gone-details.md",

+ 98 - 1
shell/browser/api/electron_api_app.cc

@@ -26,6 +26,9 @@
 #include "chrome/browser/icon_manager.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/common/chrome_paths.h"
+#include "components/proxy_config/proxy_config_dictionary.h"
+#include "components/proxy_config/proxy_config_pref_names.h"
+#include "components/proxy_config/proxy_prefs.h"
 #include "content/browser/gpu/compositor_util.h"        // nogncheck
 #include "content/browser/gpu/gpu_data_manager_impl.h"  // nogncheck
 #include "content/public/browser/browser_accessibility_state.h"
@@ -1472,6 +1475,98 @@ void App::EnableSandbox(gin_helper::ErrorThrower thrower) {
   command_line->AppendSwitch(switches::kEnableSandbox);
 }
 
+v8::Local<v8::Promise> App::SetProxy(gin::Arguments* args) {
+  v8::Isolate* isolate = args->isolate();
+  gin_helper::Promise<void> promise(isolate);
+  v8::Local<v8::Promise> handle = promise.GetHandle();
+
+  gin_helper::Dictionary options;
+  args->GetNext(&options);
+
+  if (!Browser::Get()->is_ready()) {
+    promise.RejectWithErrorMessage(
+        "app.setProxy() can only be called after app is ready.");
+    return handle;
+  }
+
+  if (!g_browser_process->local_state()) {
+    promise.RejectWithErrorMessage(
+        "app.setProxy() failed due to internal error.");
+    return handle;
+  }
+
+  std::string mode, proxy_rules, bypass_list, pac_url;
+
+  options.Get("pacScript", &pac_url);
+  options.Get("proxyRules", &proxy_rules);
+  options.Get("proxyBypassRules", &bypass_list);
+
+  ProxyPrefs::ProxyMode proxy_mode = ProxyPrefs::MODE_FIXED_SERVERS;
+  if (!options.Get("mode", &mode)) {
+    // pacScript takes precedence over proxyRules.
+    if (!pac_url.empty()) {
+      proxy_mode = ProxyPrefs::MODE_PAC_SCRIPT;
+    }
+  } else if (!ProxyPrefs::StringToProxyMode(mode, &proxy_mode)) {
+    promise.RejectWithErrorMessage(
+        "Invalid mode, must be one of direct, auto_detect, pac_script, "
+        "fixed_servers or system");
+    return handle;
+  }
+
+  base::Value::Dict proxy_config;
+  switch (proxy_mode) {
+    case ProxyPrefs::MODE_DIRECT:
+      proxy_config = ProxyConfigDictionary::CreateDirect();
+      break;
+    case ProxyPrefs::MODE_SYSTEM:
+      proxy_config = ProxyConfigDictionary::CreateSystem();
+      break;
+    case ProxyPrefs::MODE_AUTO_DETECT:
+      proxy_config = ProxyConfigDictionary::CreateAutoDetect();
+      break;
+    case ProxyPrefs::MODE_PAC_SCRIPT:
+      proxy_config = ProxyConfigDictionary::CreatePacScript(pac_url, true);
+      break;
+    case ProxyPrefs::MODE_FIXED_SERVERS:
+      proxy_config =
+          ProxyConfigDictionary::CreateFixedServers(proxy_rules, bypass_list);
+      break;
+    default:
+      NOTIMPLEMENTED();
+  }
+
+  static_cast<BrowserProcessImpl*>(g_browser_process)
+      ->in_memory_pref_store()
+      ->SetValue(proxy_config::prefs::kProxy,
+                 base::Value{std::move(proxy_config)},
+                 WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
+
+  g_browser_process->system_network_context_manager()
+      ->GetContext()
+      ->ForceReloadProxyConfig(base::BindOnce(
+          gin_helper::Promise<void>::ResolvePromise, std::move(promise)));
+
+  return handle;
+}
+
+v8::Local<v8::Promise> App::ResolveProxy(gin::Arguments* args) {
+  v8::Isolate* isolate = args->isolate();
+  gin_helper::Promise<std::string> promise(isolate);
+  v8::Local<v8::Promise> handle = promise.GetHandle();
+
+  GURL url;
+  args->GetNext(&url);
+
+  static_cast<BrowserProcessImpl*>(g_browser_process)
+      ->GetResolveProxyHelper()
+      ->ResolveProxy(
+          url, base::BindOnce(gin_helper::Promise<std::string>::ResolvePromise,
+                              std::move(promise)));
+
+  return handle;
+}
+
 void App::SetUserAgentFallback(const std::string& user_agent) {
   ElectronBrowserClient::Get()->SetUserAgent(user_agent);
 }
@@ -1776,7 +1871,9 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
       .SetProperty("userAgentFallback", &App::GetUserAgentFallback,
                    &App::SetUserAgentFallback)
       .SetMethod("configureHostResolver", &ConfigureHostResolver)
-      .SetMethod("enableSandbox", &App::EnableSandbox);
+      .SetMethod("enableSandbox", &App::EnableSandbox)
+      .SetMethod("setProxy", &App::SetProxy)
+      .SetMethod("resolveProxy", &App::ResolveProxy);
 }
 
 const char* App::GetTypeName() {

+ 2 - 0
shell/browser/api/electron_api_app.h

@@ -222,6 +222,8 @@ class App : public ElectronBrowserClient::Delegate,
   void EnableSandbox(gin_helper::ErrorThrower thrower);
   void SetUserAgentFallback(const std::string& user_agent);
   std::string GetUserAgentFallback();
+  v8::Local<v8::Promise> SetProxy(gin::Arguments* args);
+  v8::Local<v8::Promise> ResolveProxy(gin::Arguments* args);
 
 #if BUILDFLAG(IS_MAC)
   void SetActivationPolicy(gin_helper::ErrorThrower thrower,

+ 12 - 3
shell/browser/browser_process_impl.cc

@@ -37,6 +37,7 @@
 #include "net/proxy_resolution/proxy_config_with_annotation.h"
 #include "services/device/public/cpp/geolocation/geolocation_manager.h"
 #include "services/network/public/cpp/network_switches.h"
+#include "shell/browser/net/resolve_proxy_helper.h"
 #include "shell/common/electron_paths.h"
 #include "shell/common/thread_restrictions.h"
 
@@ -100,9 +101,9 @@ void BrowserProcessImpl::PostEarlyInitialization() {
   OSCrypt::RegisterLocalPrefs(pref_registry.get());
 #endif
 
-  auto pref_store = base::MakeRefCounted<ValueMapPrefStore>();
-  ApplyProxyModeFromCommandLine(pref_store.get());
-  prefs_factory.set_command_line_prefs(std::move(pref_store));
+  in_memory_pref_store_ = base::MakeRefCounted<ValueMapPrefStore>();
+  ApplyProxyModeFromCommandLine(in_memory_pref_store());
+  prefs_factory.set_command_line_prefs(in_memory_pref_store());
 
   // Only use a persistent prefs store when cookie encryption is enabled as that
   // is the only key that needs it
@@ -316,6 +317,14 @@ const std::string& BrowserProcessImpl::GetSystemLocale() const {
   return system_locale_;
 }
 
+electron::ResolveProxyHelper* BrowserProcessImpl::GetResolveProxyHelper() {
+  if (!resolve_proxy_helper_) {
+    resolve_proxy_helper_ = base::MakeRefCounted<electron::ResolveProxyHelper>(
+        system_network_context_manager()->GetContext());
+  }
+  return resolve_proxy_helper_.get();
+}
+
 #if BUILDFLAG(IS_LINUX)
 void BrowserProcessImpl::SetLinuxStorageBackend(
     os_crypt::SelectedLinuxBackend selected_backend) {

+ 11 - 1
shell/browser/browser_process_impl.h

@@ -31,6 +31,10 @@ namespace printing {
 class PrintJobManager;
 }
 
+namespace electron {
+class ResolveProxyHelper;
+}
+
 // Empty definition for std::unique_ptr, rather than a forward declaration
 class BackgroundModeManager {};
 
@@ -53,9 +57,9 @@ class BrowserProcessImpl : public BrowserProcess {
   void PreMainMessageLoopRun();
   void PostDestroyThreads() {}
   void PostMainMessageLoopRun();
-
   void SetSystemLocale(const std::string& locale);
   const std::string& GetSystemLocale() const;
+  electron::ResolveProxyHelper* GetResolveProxyHelper();
 
 #if BUILDFLAG(IS_LINUX)
   void SetLinuxStorageBackend(os_crypt::SelectedLinuxBackend selected_backend);
@@ -123,6 +127,10 @@ class BrowserProcessImpl : public BrowserProcess {
   printing::PrintJobManager* print_job_manager() override;
   StartupData* startup_data() override;
 
+  ValueMapPrefStore* in_memory_pref_store() const {
+    return in_memory_pref_store_.get();
+  }
+
  private:
   void CreateNetworkQualityObserver();
   void CreateOSCryptAsync();
@@ -139,6 +147,8 @@ class BrowserProcessImpl : public BrowserProcess {
 #endif
   embedder_support::OriginTrialsSettingsStorage origin_trials_settings_storage_;
 
+  scoped_refptr<ValueMapPrefStore> in_memory_pref_store_;
+  scoped_refptr<electron::ResolveProxyHelper> resolve_proxy_helper_;
   std::unique_ptr<network::NetworkQualityTracker> network_quality_tracker_;
   std::unique_ptr<
       network::NetworkQualityTracker::RTTAndThroughputEstimatesObserver>

+ 2 - 1
shell/browser/electron_browser_context.cc

@@ -535,7 +535,8 @@ ElectronBrowserContext::GetReduceAcceptLanguageControllerDelegate() {
 
 ResolveProxyHelper* ElectronBrowserContext::GetResolveProxyHelper() {
   if (!resolve_proxy_helper_) {
-    resolve_proxy_helper_ = base::MakeRefCounted<ResolveProxyHelper>(this);
+    resolve_proxy_helper_ = base::MakeRefCounted<ResolveProxyHelper>(
+        GetDefaultStoragePartition()->GetNetworkContext());
   }
   return resolve_proxy_helper_.get();
 }

+ 6 - 10
shell/browser/net/resolve_proxy_helper.cc

@@ -8,19 +8,17 @@
 
 #include "base/functional/bind.h"
 #include "content/public/browser/browser_thread.h"
-#include "content/public/browser/storage_partition.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "net/base/network_anonymization_key.h"
 #include "net/proxy_resolution/proxy_info.h"
-#include "services/network/public/mojom/network_context.mojom.h"
-#include "shell/browser/electron_browser_context.h"
 
 using content::BrowserThread;
 
 namespace electron {
 
-ResolveProxyHelper::ResolveProxyHelper(ElectronBrowserContext* browser_context)
-    : browser_context_(browser_context) {}
+ResolveProxyHelper::ResolveProxyHelper(
+    network::mojom::NetworkContext* network_context)
+    : network_context_(network_context) {}
 
 ResolveProxyHelper::~ResolveProxyHelper() {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
@@ -54,11 +52,9 @@ void ResolveProxyHelper::StartPendingRequest() {
   receiver_.set_disconnect_handler(
       base::BindOnce(&ResolveProxyHelper::OnProxyLookupComplete,
                      base::Unretained(this), net::ERR_ABORTED, std::nullopt));
-  browser_context_->GetDefaultStoragePartition()
-      ->GetNetworkContext()
-      ->LookUpProxyForURL(pending_requests_.front().url,
-                          net::NetworkAnonymizationKey(),
-                          std::move(proxy_lookup_client));
+  network_context_->LookUpProxyForURL(pending_requests_.front().url,
+                                      net::NetworkAnonymizationKey(),
+                                      std::move(proxy_lookup_client));
 }
 
 void ResolveProxyHelper::OnProxyLookupComplete(

+ 3 - 4
shell/browser/net/resolve_proxy_helper.h

@@ -12,20 +12,19 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted.h"
 #include "mojo/public/cpp/bindings/receiver.h"
+#include "services/network/public/mojom/network_context.mojom.h"
 #include "services/network/public/mojom/proxy_lookup_client.mojom.h"
 #include "url/gurl.h"
 
 namespace electron {
 
-class ElectronBrowserContext;
-
 class ResolveProxyHelper
     : public base::RefCountedThreadSafe<ResolveProxyHelper>,
       network::mojom::ProxyLookupClient {
  public:
   using ResolveProxyCallback = base::OnceCallback<void(std::string)>;
 
-  explicit ResolveProxyHelper(ElectronBrowserContext* browser_context);
+  explicit ResolveProxyHelper(network::mojom::NetworkContext* network_context);
 
   void ResolveProxy(const GURL& url, ResolveProxyCallback callback);
 
@@ -71,7 +70,7 @@ class ResolveProxyHelper
   mojo::Receiver<network::mojom::ProxyLookupClient> receiver_{this};
 
   // Weak Ref
-  raw_ptr<ElectronBrowserContext> browser_context_;
+  raw_ptr<network::mojom::NetworkContext> network_context_ = nullptr;
 };
 
 }  // namespace electron

+ 150 - 1
spec/api-app-spec.ts

@@ -6,9 +6,10 @@ import * as net from 'node:net';
 import * as fs from 'fs-extra';
 import * as path from 'node:path';
 import { promisify } from 'node:util';
-import { app, BrowserWindow, Menu, session, net as electronNet, WebContents } from 'electron/main';
+import { app, BrowserWindow, Menu, session, net as electronNet, WebContents, utilityProcess } from 'electron/main';
 import { closeWindow, closeAllWindows } from './lib/window-helpers';
 import { ifdescribe, ifit, listen, waitUntil } from './lib/spec-helpers';
+import { collectStreamBody, getResponse } from './lib/net-helpers';
 import { once } from 'node:events';
 import split = require('split')
 import * as semver from 'semver';
@@ -1895,6 +1896,154 @@ describe('app module', () => {
       app.showAboutPanel();
     });
   });
+
+  describe('app.setProxy(options)', () => {
+    let server: http.Server;
+
+    afterEach(async () => {
+      if (server) {
+        server.close();
+      }
+      await app.setProxy({ mode: 'direct' as const });
+    });
+
+    it('allows configuring proxy settings', async () => {
+      const config = { proxyRules: 'http=myproxy:80' };
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://example.com/');
+      expect(proxy).to.equal('PROXY myproxy:80');
+    });
+
+    it('allows removing the implicit bypass rules for localhost', async () => {
+      const config = {
+        proxyRules: 'http=myproxy:80',
+        proxyBypassRules: '<-loopback>'
+      };
+
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://localhost');
+      expect(proxy).to.equal('PROXY myproxy:80');
+    });
+
+    it('allows configuring proxy settings with pacScript', async () => {
+      server = http.createServer((req, res) => {
+        const pac = `
+          function FindProxyForURL(url, host) {
+            return "PROXY myproxy:8132";
+          }
+        `;
+        res.writeHead(200, {
+          'Content-Type': 'application/x-ns-proxy-autoconfig'
+        });
+        res.end(pac);
+      });
+      const { url } = await listen(server);
+      {
+        const config = { pacScript: url };
+        await app.setProxy(config);
+        const proxy = await app.resolveProxy('https://google.com');
+        expect(proxy).to.equal('PROXY myproxy:8132');
+      }
+      {
+        const config = { mode: 'pac_script' as any, pacScript: url };
+        await app.setProxy(config);
+        const proxy = await app.resolveProxy('https://google.com');
+        expect(proxy).to.equal('PROXY myproxy:8132');
+      }
+    });
+
+    it('allows bypassing proxy settings', async () => {
+      const config = {
+        proxyRules: 'http=myproxy:80',
+        proxyBypassRules: '<local>'
+      };
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://example/');
+      expect(proxy).to.equal('DIRECT');
+    });
+
+    it('allows configuring proxy settings with mode `direct`', async () => {
+      const config = { mode: 'direct' as const, proxyRules: 'http=myproxy:80' };
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://example.com/');
+      expect(proxy).to.equal('DIRECT');
+    });
+
+    it('allows configuring proxy settings with mode `auto_detect`', async () => {
+      const config = { mode: 'auto_detect' as const };
+      await app.setProxy(config);
+    });
+
+    it('allows configuring proxy settings with mode `pac_script`', async () => {
+      const config = { mode: 'pac_script' as const };
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://example.com/');
+      expect(proxy).to.equal('DIRECT');
+    });
+
+    it('allows configuring proxy settings with mode `fixed_servers`', async () => {
+      const config = { mode: 'fixed_servers' as const, proxyRules: 'http=myproxy:80' };
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://example.com/');
+      expect(proxy).to.equal('PROXY myproxy:80');
+    });
+
+    it('allows configuring proxy settings with mode `system`', async () => {
+      const config = { mode: 'system' as const };
+      await app.setProxy(config);
+    });
+
+    it('disallows configuring proxy settings with mode `invalid`', async () => {
+      const config = { mode: 'invalid' as any };
+      await expect(app.setProxy(config)).to.eventually.be.rejectedWith(/Invalid mode/);
+    });
+
+    it('impacts proxy for requests made from utility process', async () => {
+      const utilityFixturePath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process', 'api-net-spec.js');
+      const fn = async () => {
+        const urlRequest = electronNet.request('http://example.com/');
+        const response = await getResponse(urlRequest);
+        expect(response.statusCode).to.equal(200);
+        const message = await collectStreamBody(response);
+        expect(message).to.equal('ok from proxy\n');
+      };
+      server = http.createServer((req, res) => {
+        res.writeHead(200);
+        res.end('ok from proxy\n');
+      });
+      const { port, hostname } = await listen(server);
+      const config = { mode: 'fixed_servers' as const, proxyRules: `http=${hostname}:${port}` };
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://example.com/');
+      expect(proxy).to.equal(`PROXY ${hostname}:${port}`);
+      const child = utilityProcess.fork(utilityFixturePath, [], {
+        execArgv: ['--expose-gc']
+      });
+      child.postMessage({ fn: `(${fn})()` });
+      const [data] = await once(child, 'message');
+      expect(data.ok).to.be.true(data.message);
+      // Cleanup.
+      const [code] = await once(child, 'exit');
+      expect(code).to.equal(0);
+    });
+
+    it('does not impact proxy for requests made from main process', async () => {
+      server = http.createServer((req, res) => {
+        res.writeHead(200);
+        res.end('ok from server\n');
+      });
+      const { url } = await listen(server);
+      const config = { mode: 'fixed_servers' as const, proxyRules: 'http=myproxy:80' };
+      await app.setProxy(config);
+      const proxy = await app.resolveProxy('http://example.com/');
+      expect(proxy).to.equal('PROXY myproxy:80');
+      const urlRequest = electronNet.request(url);
+      const response = await getResponse(urlRequest);
+      expect(response.statusCode).to.equal(200);
+      const message = await collectStreamBody(response);
+      expect(message).to.equal('ok from server\n');
+    });
+  });
 });
 
 describe('default behavior', () => {

+ 1 - 1
spec/lib/spec-helpers.ts

@@ -200,5 +200,5 @@ export async function listen (server: http.Server | https.Server | http2.Http2Se
   await new Promise<void>(resolve => server.listen(0, hostname, () => resolve()));
   const { port } = server.address() as net.AddressInfo;
   const protocol = (server instanceof http.Server) ? 'http' : 'https';
-  return { port, url: url.format({ protocol, hostname, port }) };
+  return { port, hostname, url: url.format({ protocol, hostname, port }) };
 }