diff --git a/BUILD.gn b/BUILD.gn index 84a07a51bbc..282a5bac303 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -349,6 +349,8 @@ source_set("electron_lib") { "//chrome/services/printing/public/mojom", "//components/certificate_transparency", "//components/net_log", + "//components/network_hints/common", + "//components/network_hints/renderer", "//components/network_session_configurator/common", "//components/prefs", "//components/spellcheck/renderer", diff --git a/chromium_src/BUILD.gn b/chromium_src/BUILD.gn index 27b15fd7433..ac1ca1b8665 100644 --- a/chromium_src/BUILD.gn +++ b/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/printing/printing_service.cc", "//chrome/browser/printing/printing_service.h", "//chrome/browser/ssl/security_state_tab_helper.cc", @@ -57,6 +63,7 @@ static_library("chrome") { "//content/public/browser", ] deps = [ + "//chrome/browser:resource_prefetch_predictor_proto", "//components/feature_engagement:buildflags", ] diff --git a/docs/api/session.md b/docs/api/session.md index 82da931e928..c5956657dcc 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -91,6 +91,20 @@ session.defaultSession.on('will-download', (event, item, webContents) => { }) ``` +#### Event: 'preconnect' + +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)` + +* `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 diff --git a/filenames.gni b/filenames.gni index 9a560de5611..6519c174873 100644 --- a/filenames.gni +++ b/filenames.gni @@ -276,6 +276,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", diff --git a/package.json b/package.json index c99ac23cee6..dfdd99c222c 100644 --- a/package.json +++ b/package.json @@ -126,4 +126,4 @@ "git add filenames.auto.gni" ] } -} \ No newline at end of file +} diff --git a/patches/chromium/.patches b/patches/chromium/.patches index fa259b5d131..6668faf1bc6 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -67,6 +67,7 @@ worker_context_will_destroy.patch fix_breakpad_symbol_generation_on_linux_arm.patch frame_host_manager.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 diff --git a/patches/chromium/preconnect_feature.patch b/patches/chromium/preconnect_feature.patch new file mode 100644 index 00000000000..809722bf1e0 --- /dev/null +++ b/patches/chromium/preconnect_feature.patch @@ -0,0 +1,69 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Alexey Kuts +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, +- 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, Profile* profile); ++ PreconnectManager(base::WeakPtr 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_; +- Profile* const profile_; ++ content::BrowserContext* const profile_; + std::list queued_jobs_; + PreresolveJobMap preresolve_jobs_; + std::map> preresolve_info_; diff --git a/shell/browser/api/atom_api_session.cc b/shell/browser/api/atom_api_session.cc index d8339808e92..d6a047fcdbe 100644 --- a/shell/browser/api/atom_api_session.cc +++ b/shell/browser/api/atom_api_session.cc @@ -4,6 +4,7 @@ #include "shell/browser/api/atom_api_session.h" +#include #include #include #include @@ -594,6 +595,42 @@ v8::Local Session::NetLog(v8::Isolate* isolate) { return v8::Local::New(isolate, net_log_); } +static void StartPreconnectOnUI( + scoped_refptr browser_context, + const GURL& url, + int num_sockets_to_preconnect) { + std::vector 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::CreateFrom(v8::Isolate* isolate, AtomBrowserContext* browser_context) { @@ -664,6 +701,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) diff --git a/shell/browser/api/atom_api_session.h b/shell/browser/api/atom_api_session.h index 07322b85a01..ebf467d3d46 100644 --- a/shell/browser/api/atom_api_session.h +++ b/shell/browser/api/atom_api_session.h @@ -86,6 +86,7 @@ class Session : public mate::TrackableObject, v8::Local Protocol(v8::Isolate* isolate); v8::Local WebRequest(v8::Isolate* isolate); v8::Local 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); diff --git a/shell/browser/atom_browser_client.cc b/shell/browser/atom_browser_client.cc index 5f68a81048f..ded6a3552aa 100644 --- a/shell/browser/atom_browser_client.cc +++ b/shell/browser/atom_browser_client.cc @@ -51,6 +51,7 @@ #include "shell/app/manifests.h" #include "shell/browser/api/atom_api_app.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/api/atom_api_web_request_ns.h" #include "shell/browser/atom_autofill_driver_factory.h" @@ -70,6 +71,7 @@ #include "shell/browser/net/system_network_context_manager.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" @@ -365,6 +367,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); diff --git a/shell/browser/atom_browser_context.cc b/shell/browser/atom_browser_context.cc index bd3047e7ac4..45d82b836b3 100644 --- a/shell/browser/atom_browser_context.cc +++ b/shell/browser/atom_browser_context.cc @@ -264,6 +264,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; } diff --git a/shell/browser/atom_browser_context.h b/shell/browser/atom_browser_context.h index 0fe74a39033..8019869573d 100644 --- a/shell/browser/atom_browser_context.h +++ b/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" @@ -87,6 +88,8 @@ class AtomBrowserContext AtomBlobReader* GetBlobReader(); ResolveProxyHelper* GetResolveProxyHelper(); + predictors::PreconnectManager* GetPreconnectManager(); + // content::BrowserContext: base::FilePath GetPath() override; bool IsOffTheRecord() override; @@ -164,6 +167,8 @@ class AtomBrowserContext // ProxyConfigClient. std::unique_ptr proxy_config_monitor_; + std::unique_ptr preconnect_manager_; + std::string user_agent_; base::FilePath path_; bool in_memory_ = false; diff --git a/shell/browser/renderer_host/electron_render_message_filter.cc b/shell/browser/renderer_host/electron_render_message_filter.cc new file mode 100644 index 00000000000..10a74c5e638 --- /dev/null +++ b/shell/browser/renderer_host/electron_render_message_filter.cc @@ -0,0 +1,94 @@ +// 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 + +#include +#include + +#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(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(int render_frame_id, + 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, + const net::NetworkIsolationKey& network_isolation_key) + : origin(origin), num_sockets(num_sockets) { + DCHECK_GE(num_sockets, 0); +} + +} // namespace predictors diff --git a/shell/browser/renderer_host/electron_render_message_filter.h b/shell/browser/renderer_host/electron_render_message_filter.h new file mode 100644 index 00000000000..0ced6f39c0c --- /dev/null +++ b/shell/browser/renderer_host/electron_render_message_filter.h @@ -0,0 +1,46 @@ +// 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 +#include + +#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(int render_frame_id, + 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_ diff --git a/shell/renderer/renderer_client_base.cc b/shell/renderer/renderer_client_base.cc index 5830b26ecd4..afb8990c720 100644 --- a/shell/renderer/renderer_client_base.cc +++ b/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) diff --git a/shell/renderer/renderer_client_base.h b/shell/renderer/renderer_client_base.h index 6c124adafbd..58ad929edd5 100644 --- a/shell/renderer/renderer_client_base.h +++ b/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 + prescient_networking_dispatcher_; + #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) std::unique_ptr extensions_client_; std::unique_ptr extensions_renderer_client_; #endif + #if defined(WIDEVINE_CDM_AVAILABLE) ChromeKeySystemsProvider key_systems_provider_; #endif diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index 76e311d4102..03d30154512 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -1136,6 +1136,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(`foo`) + 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 ', 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(() => {