Browse Source

feat: HTTP preconnect feature minimal for electronjs (#19952)

* feat: HTTP preconnect feature minimal for electronjs

* fix type of PreconnectRequest::PreconnectRequest impl

* roll back past https://chromium-review.googlesource.com/c/chromium/src/+/1713306

* mark docs as experimental

* fix lint
Jeremy Apthorp 5 years ago
parent
commit
b09a1c7607

+ 2 - 0
BUILD.gn

@@ -356,6 +356,8 @@ source_set("electron_lib") {
     "//chrome/app/resources:platform_locale_settings",
     "//components/certificate_transparency",
     "//components/net_log",
+    "//components/network_hints/common",
+    "//components/network_hints/renderer",
     "//components/network_session_configurator/common",
     "//components/prefs",
     "//components/spellcheck/renderer",

+ 7 - 0
chromium_src/BUILD.gn

@@ -37,6 +37,12 @@ static_library("chrome") {
     "//chrome/browser/net/proxy_config_monitor.h",
     "//chrome/browser/net/proxy_service_factory.cc",
     "//chrome/browser/net/proxy_service_factory.h",
+    "//chrome/browser/predictors/preconnect_manager.cc",
+    "//chrome/browser/predictors/preconnect_manager.h",
+    "//chrome/browser/predictors/proxy_lookup_client_impl.cc",
+    "//chrome/browser/predictors/proxy_lookup_client_impl.h",
+    "//chrome/browser/predictors/resolve_host_client_impl.cc",
+    "//chrome/browser/predictors/resolve_host_client_impl.h",
     "//chrome/browser/ssl/security_state_tab_helper.cc",
     "//chrome/browser/ssl/security_state_tab_helper.h",
     "//chrome/browser/ui/autofill/popup_view_common.cc",
@@ -55,6 +61,7 @@ static_library("chrome") {
     "//content/public/browser",
   ]
   deps = [
+    "//chrome/browser:resource_prefetch_predictor_proto",
     "//components/feature_engagement:buildflags",
   ]
 

+ 22 - 0
docs/api/session.md

@@ -91,6 +91,20 @@ session.defaultSession.on('will-download', (event, item, webContents) => {
 })
 ```
 
+#### Event: 'preconnect' _Experimental_
+
+Returns:
+
+* `event` Event
+* `preconnectUrl` String - The URL being requested for preconnection by the
+  renderer.
+* `allowCredentials` Boolean - True if the renderer is requesting that the
+  connection include credentials (see the
+  [spec](https://w3c.github.io/resource-hints/#preconnect) for more details.)
+
+Emitted when a render process requests preconnection to a URL, generally due to
+a [resource hint](https://w3c.github.io/resource-hints/).
+
 ### Instance Methods
 
 The following methods are available on instances of `Session`:
@@ -238,6 +252,14 @@ window.webContents.session.enableNetworkEmulation({
 window.webContents.session.enableNetworkEmulation({ offline: true })
 ```
 
+#### `ses.preconnect(options)` _Experimental_
+
+* `options` Object
+  * `url` String - URL for preconnect. Only the origin is relevant for opening the socket.
+  * `numSockets` Number (optional) - number of sockets to preconnect. Must be between 1 and 6. Defaults to 1.
+
+Preconnects the given number of sockets to an origin.
+
 #### `ses.disableNetworkEmulation()`
 
 Disables any network emulation already active for the `session`. Resets to

+ 2 - 0
filenames.gni

@@ -316,6 +316,8 @@ filenames = {
     "shell/browser/relauncher_win.cc",
     "shell/browser/relauncher.cc",
     "shell/browser/relauncher.h",
+    "shell/browser/renderer_host/electron_render_message_filter.cc",
+    "shell/browser/renderer_host/electron_render_message_filter.h",
     "shell/browser/session_preferences.cc",
     "shell/browser/session_preferences.h",
     "shell/browser/special_storage_policy.cc",

+ 1 - 1
package.json

@@ -125,4 +125,4 @@
       "git add filenames.auto.gni"
     ]
   }
-}
+}

+ 1 - 0
patches/chromium/.patches

@@ -71,6 +71,7 @@ fix_breakpad_symbol_generation_on_linux_arm.patch
 frame_host_manager.patch
 cross_site_document_resource_handler.patch
 crashpad_pid_check.patch
+preconnect_feature.patch
 network_service_allow_remote_certificate_verification_logic.patch
 put_back_deleted_colors_for_autofill.patch
 build_win_disable_zc_twophase.patch

+ 69 - 0
patches/chromium/preconnect_feature.patch

@@ -0,0 +1,69 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexey Kuts <[email protected]>
+Date: Fri, 26 Jul 2019 22:32:54 +0300
+Subject: remove references to Profile from PreconnectManager
+
+The PreconnectManager in Chrome only depends on Profile for testing purposes;
+this patch removes that dependency so we can reuse it.
+Ideally we would change this class in upstream to not depend on Profile.
+
+diff --git a/chrome/browser/predictors/preconnect_manager.cc b/chrome/browser/predictors/preconnect_manager.cc
+index cdee4d11f2d2..7312fb4e4ea5 100644
+--- a/chrome/browser/predictors/preconnect_manager.cc
++++ b/chrome/browser/predictors/preconnect_manager.cc
+@@ -71,7 +71,7 @@ PreresolveJob::PreresolveJob(PreresolveJob&& other) = default;
+ PreresolveJob::~PreresolveJob() = default;
+ 
+ PreconnectManager::PreconnectManager(base::WeakPtr<Delegate> delegate,
+-                                     Profile* profile)
++                                     content::BrowserContext* profile)
+     : delegate_(std::move(delegate)),
+       profile_(profile),
+       inflight_preresolves_count_(0) {
+@@ -327,11 +327,13 @@ network::mojom::NetworkContext* PreconnectManager::GetNetworkContext() const {
+   if (network_context_)
+     return network_context_;
+ 
++#if 0
+   if (profile_->AsTestingProfile()) {
+     // We're testing and |network_context_| wasn't set. Return nullptr to avoid
+     // hitting the network.
+     return nullptr;
+   }
++#endif
+ 
+   return content::BrowserContext::GetDefaultStoragePartition(profile_)
+       ->GetNetworkContext();
+diff --git a/chrome/browser/predictors/preconnect_manager.h b/chrome/browser/predictors/preconnect_manager.h
+index 51a842d2e44f..097316e0cfb6 100644
+--- a/chrome/browser/predictors/preconnect_manager.h
++++ b/chrome/browser/predictors/preconnect_manager.h
+@@ -22,6 +22,10 @@
+ 
+ class Profile;
+ 
++namespace content {
++class BrowserContext;
++}
++
+ namespace network {
+ namespace mojom {
+ class NetworkContext;
+@@ -138,7 +142,7 @@ class PreconnectManager {
+ 
+   static const size_t kMaxInflightPreresolves = 3;
+ 
+-  PreconnectManager(base::WeakPtr<Delegate> delegate, Profile* profile);
++  PreconnectManager(base::WeakPtr<Delegate> delegate, content::BrowserContext* profile);
+   virtual ~PreconnectManager();
+ 
+   // Starts preconnect and preresolve jobs keyed by |url|.
+@@ -202,7 +206,7 @@ class PreconnectManager {
+   network::mojom::NetworkContext* GetNetworkContext() const;
+ 
+   base::WeakPtr<Delegate> delegate_;
+-  Profile* const profile_;
++  content::BrowserContext* const profile_;
+   std::list<PreresolveJobId> queued_jobs_;
+   PreresolveJobMap preresolve_jobs_;
+   std::map<std::string, std::unique_ptr<PreresolveInfo>> preresolve_info_;

+ 38 - 0
shell/browser/api/atom_api_session.cc

@@ -4,6 +4,7 @@
 
 #include "shell/browser/api/atom_api_session.h"
 
+#include <algorithm>
 #include <map>
 #include <memory>
 #include <string>
@@ -690,6 +691,42 @@ v8::Local<v8::Value> Session::NetLog(v8::Isolate* isolate) {
   return v8::Local<v8::Value>::New(isolate, net_log_);
 }
 
+static void StartPreconnectOnUI(
+    scoped_refptr<AtomBrowserContext> browser_context,
+    const GURL& url,
+    int num_sockets_to_preconnect) {
+  std::vector<predictors::PreconnectRequest> requests = {
+      {url.GetOrigin(), num_sockets_to_preconnect, net::NetworkIsolationKey()}};
+  browser_context->GetPreconnectManager()->Start(url, requests);
+}
+
+void Session::Preconnect(const mate::Dictionary& options,
+                         mate::Arguments* args) {
+  GURL url;
+  if (!options.Get("url", &url) || !url.is_valid()) {
+    args->ThrowError("Must pass non-empty valid url to session.preconnect.");
+    return;
+  }
+  int num_sockets_to_preconnect = 1;
+  if (options.Get("numSockets", &num_sockets_to_preconnect)) {
+    const int kMinSocketsToPreconnect = 1;
+    const int kMaxSocketsToPreconnect = 6;
+    if (num_sockets_to_preconnect < kMinSocketsToPreconnect ||
+        num_sockets_to_preconnect > kMaxSocketsToPreconnect) {
+      args->ThrowError(
+          base::StringPrintf("numSocketsToPreconnect is outside range [%d,%d]",
+                             kMinSocketsToPreconnect, kMaxSocketsToPreconnect));
+      return;
+    }
+  }
+
+  DCHECK_GT(num_sockets_to_preconnect, 0);
+  base::PostTaskWithTraits(
+      FROM_HERE, {content::BrowserThread::UI},
+      base::BindOnce(&StartPreconnectOnUI, base::RetainedRef(browser_context_),
+                     url, num_sockets_to_preconnect));
+}
+
 // static
 mate::Handle<Session> Session::CreateFrom(v8::Isolate* isolate,
                                           AtomBrowserContext* browser_context) {
@@ -760,6 +797,7 @@ void Session::BuildPrototype(v8::Isolate* isolate,
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
       .SetMethod("loadChromeExtension", &Session::LoadChromeExtension)
 #endif
+      .SetMethod("preconnect", &Session::Preconnect)
       .SetProperty("cookies", &Session::Cookies)
       .SetProperty("netLog", &Session::NetLog)
       .SetProperty("protocol", &Session::Protocol)

+ 1 - 0
shell/browser/api/atom_api_session.h

@@ -86,6 +86,7 @@ class Session : public mate::TrackableObject<Session>,
   v8::Local<v8::Value> Protocol(v8::Isolate* isolate);
   v8::Local<v8::Value> WebRequest(v8::Isolate* isolate);
   v8::Local<v8::Value> NetLog(v8::Isolate* isolate);
+  void Preconnect(const mate::Dictionary& options, mate::Arguments* args);
 
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
   void LoadChromeExtension(const base::FilePath extension_path);

+ 5 - 0
shell/browser/atom_browser_client.cc

@@ -52,6 +52,7 @@
 #include "shell/browser/api/atom_api_app.h"
 #include "shell/browser/api/atom_api_protocol.h"
 #include "shell/browser/api/atom_api_protocol_ns.h"
+#include "shell/browser/api/atom_api_session.h"
 #include "shell/browser/api/atom_api_web_contents.h"
 #include "shell/browser/atom_browser_context.h"
 #include "shell/browser/atom_browser_main_parts.h"
@@ -69,6 +70,7 @@
 #include "shell/browser/net/proxying_url_loader_factory.h"
 #include "shell/browser/notifications/notification_presenter.h"
 #include "shell/browser/notifications/platform_notification_service.h"
+#include "shell/browser/renderer_host/electron_render_message_filter.h"
 #include "shell/browser/session_preferences.h"
 #include "shell/browser/ui/devtools_manager_delegate.h"
 #include "shell/browser/web_contents_permission_helper.h"
@@ -370,6 +372,9 @@ void AtomBrowserClient::RenderProcessWillLaunch(
     prefs.web_security = web_preferences->IsEnabled(options::kWebSecurity,
                                                     true /* default value */);
   }
+
+  host->AddFilter(new ElectronRenderMessageFilter(host->GetBrowserContext()));
+
   AddProcessPreferences(host->GetID(), prefs);
   // ensure the ProcessPreferences is removed later
   host->AddObserver(this);

+ 7 - 0
shell/browser/atom_browser_context.cc

@@ -319,6 +319,13 @@ AtomBlobReader* AtomBrowserContext::GetBlobReader() {
   return blob_reader_.get();
 }
 
+predictors::PreconnectManager* AtomBrowserContext::GetPreconnectManager() {
+  if (!preconnect_manager_.get()) {
+    preconnect_manager_.reset(new predictors::PreconnectManager(nullptr, this));
+  }
+  return preconnect_manager_.get();
+}
+
 content::PushMessagingService* AtomBrowserContext::GetPushMessagingService() {
   return nullptr;
 }

+ 5 - 0
shell/browser/atom_browser_context.h

@@ -13,6 +13,7 @@
 #include "base/memory/ref_counted_delete_on_sequence.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/net/proxy_config_monitor.h"
+#include "chrome/browser/predictors/preconnect_manager.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/resource_context.h"
 #include "electron/buildflags/buildflags.h"
@@ -91,6 +92,8 @@ class AtomBrowserContext
   net::URLRequestContextGetter* GetRequestContext();
   ResolveProxyHelper* GetResolveProxyHelper();
 
+  predictors::PreconnectManager* GetPreconnectManager();
+
   // content::BrowserContext:
   base::FilePath GetPath() override;
   bool IsOffTheRecord() override;
@@ -175,6 +178,8 @@ class AtomBrowserContext
   // ProxyConfigClient.
   std::unique_ptr<ProxyConfigMonitor> proxy_config_monitor_;
 
+  std::unique_ptr<predictors::PreconnectManager> preconnect_manager_;
+
   std::string user_agent_;
   base::FilePath path_;
   bool in_memory_ = false;

+ 93 - 0
shell/browser/renderer_host/electron_render_message_filter.cc

@@ -0,0 +1,93 @@
+// Copyright (c) 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/renderer_host/electron_render_message_filter.h"
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/logging.h"
+#include "base/stl_util.h"
+#include "chrome/browser/predictors/preconnect_manager.h"
+#include "components/network_hints/common/network_hints_common.h"
+#include "components/network_hints/common/network_hints_messages.h"
+#include "content/public/browser/browser_context.h"
+#include "shell/browser/api/atom_api_session.h"
+#include "shell/browser/atom_browser_context.h"
+#include "shell/common/native_mate_converters/gurl_converter.h"
+
+using content::BrowserThread;
+
+namespace {
+
+const uint32_t kRenderFilteredMessageClasses[] = {
+    NetworkHintsMsgStart,
+};
+
+void EmitPreconnect(content::BrowserContext* browser_context,
+                    const GURL& url,
+                    bool allow_credentials) {
+  auto* session = electron::api::Session::FromWrappedClass(
+      v8::Isolate::GetCurrent(),
+      static_cast<electron::AtomBrowserContext*>(browser_context));
+  if (session) {
+    session->Emit("preconnect", url, allow_credentials);
+  }
+}
+
+}  // namespace
+
+ElectronRenderMessageFilter::ElectronRenderMessageFilter(
+    content::BrowserContext* browser_context)
+    : BrowserMessageFilter(kRenderFilteredMessageClasses,
+                           base::size(kRenderFilteredMessageClasses)),
+      browser_context_(browser_context) {}
+
+ElectronRenderMessageFilter::~ElectronRenderMessageFilter() {}
+
+bool ElectronRenderMessageFilter::OnMessageReceived(
+    const IPC::Message& message) {
+  bool handled = true;
+  IPC_BEGIN_MESSAGE_MAP(ElectronRenderMessageFilter, message)
+    IPC_MESSAGE_HANDLER(NetworkHintsMsg_Preconnect, OnPreconnect)
+    IPC_MESSAGE_UNHANDLED(handled = false)
+  IPC_END_MESSAGE_MAP()
+
+  return handled;
+}
+
+void ElectronRenderMessageFilter::OnPreconnect(const GURL& url,
+                                               bool allow_credentials,
+                                               int count) {
+  if (count < 1) {
+    LOG(WARNING) << "NetworkHintsMsg_Preconnect IPC with invalid count: "
+                 << count;
+    return;
+  }
+
+  if (!url.is_valid() || !url.has_host() || !url.has_scheme() ||
+      !url.SchemeIsHTTPOrHTTPS()) {
+    return;
+  }
+
+  base::PostTask(FROM_HERE, {BrowserThread::UI},
+                 base::BindOnce(&EmitPreconnect, browser_context_, url,
+                                allow_credentials));
+}
+
+namespace predictors {
+
+PreconnectRequest::PreconnectRequest(
+    const GURL& origin,
+    int num_sockets,
+    net::NetworkIsolationKey network_isolation_key)
+    : origin(origin), num_sockets(num_sockets) {
+  DCHECK_GE(num_sockets, 0);
+}
+
+}  // namespace predictors

+ 43 - 0
shell/browser/renderer_host/electron_render_message_filter.h

@@ -0,0 +1,43 @@
+// Copyright (c) 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SHELL_BROWSER_RENDERER_HOST_ELECTRON_RENDER_MESSAGE_FILTER_H_
+#define SHELL_BROWSER_RENDERER_HOST_ELECTRON_RENDER_MESSAGE_FILTER_H_
+
+#include <string>
+#include <vector>
+
+#include "content/public/browser/browser_message_filter.h"
+
+class GURL;
+
+namespace content {
+class BrowserContext;
+}
+
+namespace predictors {
+class PreconnectManager;
+}
+
+// This class filters out incoming Chrome-specific IPC messages for the renderer
+// process on the IPC thread.
+class ElectronRenderMessageFilter : public content::BrowserMessageFilter {
+ public:
+  explicit ElectronRenderMessageFilter(
+      content::BrowserContext* browser_context);
+
+  // content::BrowserMessageFilter methods:
+  bool OnMessageReceived(const IPC::Message& message) override;
+
+ private:
+  ~ElectronRenderMessageFilter() override;
+
+  void OnPreconnect(const GURL& url, bool allow_credentials, int count);
+
+  content::BrowserContext* browser_context_;
+
+  DISALLOW_COPY_AND_ASSIGN(ElectronRenderMessageFilter);
+};
+
+#endif  // SHELL_BROWSER_RENDERER_HOST_ELECTRON_RENDER_MESSAGE_FILTER_H_

+ 8 - 0
shell/renderer/renderer_client_base.cc

@@ -11,6 +11,7 @@
 #include "base/command_line.h"
 #include "base/strings/string_split.h"
 #include "base/strings/stringprintf.h"
+#include "components/network_hints/renderer/prescient_networking_dispatcher.h"
 #include "content/common/buildflags.h"
 #include "content/public/common/content_constants.h"
 #include "content/public/common/content_switches.h"
@@ -201,6 +202,9 @@ void RendererClientBase::RenderThreadStarted() {
   blink::WebSecurityPolicy::RegisterURLSchemeAsAllowingServiceWorkers("file");
   blink::SchemeRegistry::RegisterURLSchemeAsSupportingFetchAPI("file");
 
+  prescient_networking_dispatcher_.reset(
+      new network_hints::PrescientNetworkingDispatcher());
+
 #if defined(OS_WIN)
   // Set ApplicationUserModelID in renderer process.
   base::string16 app_id =
@@ -318,6 +322,10 @@ void RendererClientBase::DidSetUserAgent(const std::string& user_agent) {
 #endif
 }
 
+blink::WebPrescientNetworking* RendererClientBase::GetPrescientNetworking() {
+  return prescient_networking_dispatcher_.get();
+}
+
 void RendererClientBase::RunScriptsAtDocumentStart(
     content::RenderFrame* render_frame) {
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)

+ 9 - 0
shell/renderer/renderer_client_base.h

@@ -19,6 +19,10 @@
 #include "chrome/renderer/media/chrome_key_systems_provider.h"  // nogncheck
 #endif
 
+namespace network_hints {
+class PrescientNetworkingDispatcher;
+}
+
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
 namespace extensions {
 class ExtensionsClient;
@@ -47,6 +51,7 @@ class RendererClientBase : public content::ContentRendererClient {
                                             content::RenderFrame* render_frame,
                                             int world_id) = 0;
 
+  blink::WebPrescientNetworking* GetPrescientNetworking() override;
   bool isolated_world() const { return isolated_world_; }
 
   // Get the context that the Electron API is running in.
@@ -90,10 +95,14 @@ class RendererClientBase : public content::ContentRendererClient {
 #endif
 
  private:
+  std::unique_ptr<network_hints::PrescientNetworkingDispatcher>
+      prescient_networking_dispatcher_;
+
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
   std::unique_ptr<extensions::ExtensionsClient> extensions_client_;
   std::unique_ptr<AtomExtensionsRendererClient> extensions_renderer_client_;
 #endif
+
 #if defined(WIDEVINE_CDM_AVAILABLE)
   ChromeKeySystemsProvider key_systems_provider_;
 #endif

+ 60 - 0
spec-main/api-browser-window-spec.ts

@@ -1091,6 +1091,66 @@ describe('BrowserWindow module', () => {
     })
   })
 
+  describe('preconnect feature', () => {
+    let w = null as unknown as BrowserWindow
+
+    let server = null as unknown as http.Server
+    let url = null as unknown as string
+    let connections = 0
+
+    beforeEach(async () => {
+      connections = 0
+      server = http.createServer((req, res) => {
+        if (req.url === '/link') {
+          res.setHeader('Content-type', 'text/html')
+          res.end(`<head><link rel="preconnect" href="//example.com" /></head><body>foo</body>`)
+          return
+        }
+        res.end()
+      })
+      server.on('connection', (connection) => { connections++ })
+
+      await new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve()))
+      url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
+    })
+    afterEach(async () => {
+      server.close()
+      await closeWindow(w)
+      w = null as unknown as BrowserWindow
+      server = null as unknown as http.Server
+    })
+
+    it('calling preconnect() connects to the server', (done) => {
+      w = new BrowserWindow({show: false})
+      w.webContents.on('did-start-navigation', (event, preconnectUrl, isInPlace, isMainFrame, frameProcessId, frameRoutingId) => {
+        w.webContents.session.preconnect({
+          url: preconnectUrl,
+          numSockets: 4
+        })
+      })
+      w.webContents.on('did-finish-load', () => {
+        expect(connections).to.equal(4)
+        done()
+      })
+      w.loadURL(url)
+    })
+
+    it('does not preconnect unless requested', async () => {
+      w = new BrowserWindow({show: false})
+      await w.loadURL(url)
+      expect(connections).to.equal(1)
+    })
+
+    it('parses <link rel=preconnect>', async () => {
+      w = new BrowserWindow({show: true})
+      const p = emittedOnce(w.webContents.session, 'preconnect')
+      w.loadURL(url + '/link')
+      const [, preconnectUrl, allowCredentials] = await p
+      expect(preconnectUrl).to.equal('http://example.com/')
+      expect(allowCredentials).to.be.true('allowCredentials')
+    })
+  })
+
   describe('BrowserWindow.setAutoHideCursor(autoHide)', () => {
     let w = null as unknown as BrowserWindow
     beforeEach(() => {