From 992cbd034ed1bd9cd0418e9b636c49cddb0a251a Mon Sep 17 00:00:00 2001 From: Arthur Edelstein Date: Fri, 13 Jan 2023 10:21:06 -0800 Subject: [PATCH] Partition the EventSource pool per first-party on Desktop - Sets the limit to 250 EventSources per first party. - Browser tests - Disable in Android for now fixes https://github.com/brave/brave-browser/issues/28077 --- android/android_browser_tests.gni | 1 + browser/about_flags.cc | 10 + .../eventsource_pool_limit_browsertest.cc | 402 ++++++++++++++++++ .../third_party/blink/common/features.cc | 10 + .../blink/public/common/features.h | 1 + .../modules/eventsource/event_source.cc | 85 ++++ .../modules/eventsource/event_source.h | 51 +++ test/BUILD.gn | 1 + test/data/service-worker-eventsource-limit.js | 39 ++ .../resource_pool_limiter.cc | 2 + .../resource_pool_limiter.h | 1 + 11 files changed, 603 insertions(+) create mode 100644 browser/brave_shields/eventsource_pool_limit_browsertest.cc create mode 100644 chromium_src/third_party/blink/renderer/modules/eventsource/event_source.cc create mode 100644 chromium_src/third_party/blink/renderer/modules/eventsource/event_source.h create mode 100644 test/data/service-worker-eventsource-limit.js diff --git a/android/android_browser_tests.gni b/android/android_browser_tests.gni index ff58cdddeb28..c8fa56f09dc1 100644 --- a/android/android_browser_tests.gni +++ b/android/android_browser_tests.gni @@ -17,6 +17,7 @@ android_test_exception_sources = [ "//brave/browser/brave_shields/brave_shields_web_contents_observer_browsertest.cc", "//brave/browser/brave_shields/cookie_expiry_browsertest.cc", "//brave/browser/brave_shields/domain_block_page_browsertest.cc", + "//brave/browser/brave_shields/eventsource_pool_limit_browsertest.cc", "//brave/browser/brave_shields/websockets_pool_limit_browsertest.cc", "//brave/browser/brave_wallet/brave_wallet_ethereum_chain_browsertest.cc", "//brave/browser/brave_wallet/brave_wallet_event_emitter_browsertest.cc", diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 6f6e9ea10b04..da3e1f9e802b 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -447,6 +447,11 @@ constexpr char kBraveChangeActiveTabOnScrollEventName[] = constexpr char kBraveChangeActiveTabOnScrollEventDescription[] = "Change the active tab when scroll events occur on tab strip."; #endif // BUILDFLAG(IS_LINUX) + +constexpr char kRestrictEventSourcePoolName[] = "Restrict Event Source Pool"; +constexpr char kRestrictEventSourcePoolDescription[] = + "Limits simultaneous active WebSockets connections per eTLD+1"; + } // namespace } // namespace flag_descriptions @@ -809,6 +814,11 @@ constexpr char kBraveChangeActiveTabOnScrollEventDescription[] = flag_descriptions::kBraveSyncHistoryDiagnosticsDescription, \ kOsAll, FEATURE_VALUE_TYPE( \ brave_sync::features::kBraveSyncHistoryDiagnostics)}, \ + {"restrict-event-source-pool", \ + flag_descriptions::kRestrictEventSourcePoolName, \ + flag_descriptions::kRestrictEventSourcePoolDescription, \ + kOsAll, FEATURE_VALUE_TYPE( \ + blink::features::kRestrictEventSourcePool)}, \ BRAVE_IPFS_FEATURE_ENTRIES \ BRAVE_NATIVE_WALLET_FEATURE_ENTRIES \ BRAVE_NEWS_FEATURE_ENTRIES \ diff --git a/browser/brave_shields/eventsource_pool_limit_browsertest.cc b/browser/brave_shields/eventsource_pool_limit_browsertest.cc new file mode 100644 index 000000000000..cf85ed5b09df --- /dev/null +++ b/browser/brave_shields/eventsource_pool_limit_browsertest.cc @@ -0,0 +1,402 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include + +#include "base/path_service.h" +#include "brave/components/brave_shields/browser/brave_shields_util.h" +#include "brave/components/constants/brave_paths.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/content_mock_cert_verifier.h" +#include "extensions/buildflags/buildflags.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_connection.h" +#include "net/test/embedded_test_server/http_request.h" +#include "third_party/blink/public/common/features.h" +#include "url/gurl.h" + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "chrome/browser/extensions/chrome_test_extension_loader.h" +#include "extensions/common/extension.h" +#include "extensions/test/test_extension_dir.h" +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + +namespace { + +using net::test_server::BasicHttpResponse; +using net::test_server::EmbeddedTestServer; +using net::test_server::EmbeddedTestServerConnectionListener; +using net::test_server::HttpConnection; +using net::test_server::HttpRequest; +using net::test_server::HttpResponse; + +const int kEventSourcesPoolLimit = 250; + +constexpr char kEventSourcesOpenScript[] = R"( + if (typeof sources === "undefined") { + sources = []; + } + new Promise(resolve => { + const source = new EventSource($1); + sources.push(source); + source.addEventListener('open', () => { + resolve('open'); + }); + source.addEventListener('error', () => { + resolve('error'); + }); + }); +)"; + +constexpr char kEventSourceCloseScript[] = R"( + sources[$1].close(); +)"; + +constexpr char kRegisterSwScript[] = R"( + (async () => { + await navigator.serviceWorker.register($1, {scope: './'}); + const registration = await navigator.serviceWorker.ready; + })(); +)"; + +constexpr char kEventSourcesOpenInSwScript[] = R"( + (async () => { + const registration = await navigator.serviceWorker.ready; + const result = new Promise(resolve => { + navigator.serviceWorker.onmessage = event => { + resolve(event.data); + }; + }); + registration.active.postMessage({cmd: 'open_es', url: $1}); + return await result; + })(); +)"; + +constexpr char kEventSourceCloseInSwScript[] = R"( + (async () => { + const registration = await navigator.serviceWorker.ready; + registration.active.postMessage({cmd: 'close_es', idx: $1}); + })(); +)"; + +class EmbeddedTestServerKeepAlive : public EmbeddedTestServer { + public: + using EmbeddedTestServer::EmbeddedTestServer; + + void RemoveConnection( + HttpConnection* connection, + EmbeddedTestServerConnectionListener* listener = nullptr) { + // Don't remove connection, to keep it alive. + } +}; + +} // namespace + +class EventSourcePoolLimitBrowserTest : public InProcessBrowserTest { + public: + EventSourcePoolLimitBrowserTest() = default; + + void SetUpOnMainThread() override { + InProcessBrowserTest::SetUpOnMainThread(); + host_resolver()->AddRule("*", "127.0.0.1"); + brave::RegisterPathProvider(); + mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); + base::FilePath test_data_dir; + base::PathService::Get(brave::DIR_TEST_DATA, &test_data_dir); + https_server_.ServeFilesFromDirectory(test_data_dir); + content::SetupCrossSiteRedirector(&https_server_); + https_server_.RegisterRequestHandler( + base::BindRepeating(&EventSourcePoolLimitBrowserTest::HandleRequest, + base::Unretained(this))); + ASSERT_TRUE(https_server_.Start()); + es_url_ = https_server_.GetURL("a.com", "/source"); + } + + std::unique_ptr HandleRequest(const HttpRequest& request) { + std::string event_source_host_colon_port = + es_url_.host() + ":" + es_url_.port(); + if (request.relative_url == es_url_.path() && + request.headers.at("Host") == event_source_host_colon_port) { + auto http_response = std::make_unique(); + http_response->set_code(net::HTTP_OK); + http_response->set_content_type("text/event-stream"); + http_response->AddCustomHeader("Access-Control-Allow-Origin", "*"); + http_response->AddCustomHeader("Cache-Control", "no-cache"); + http_response->AddCustomHeader("Connection", "keep-alive"); + http_response->set_content("retry: 10000\n\n"); + return http_response; + } + return nullptr; + } + + void OpenEventSources(content::RenderFrameHost* rfh, + base::StringPiece script_template, + int count) { + const std::string& es_open_script = + content::JsReplace(script_template, es_url_); + for (int i = 0; i < count; ++i) { + EXPECT_EQ("open", content::EvalJs(rfh, es_open_script)); + } + } + + void ExpectEventSourcesAreLimited(content::RenderFrameHost* rfh, + base::StringPiece script_template) { + const std::string& es_open_script = + content::JsReplace(script_template, es_url_); + for (int i = 0; i < 5; ++i) { + EXPECT_EQ("error", content::EvalJs(rfh, es_open_script)); + } + } + + void CloseEventSources(content::RenderFrameHost* rfh, + base::StringPiece script_template, + int count) { + for (int i = 0; i < count; ++i) { + EXPECT_TRUE(content::ExecJs(rfh, content::JsReplace(script_template, i))); + } + } + + void OpenEventSourcesAndExpectLimited(content::RenderFrameHost* rfh, + base::StringPiece script_template, + int count) { + OpenEventSources(rfh, script_template, count); + ExpectEventSourcesAreLimited(rfh, script_template); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + InProcessBrowserTest::SetUpCommandLine(command_line); + mock_cert_verifier_.SetUpCommandLine(command_line); + } + + void SetUpInProcessBrowserTestFixture() override { + InProcessBrowserTest::SetUpInProcessBrowserTestFixture(); + mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); + } + + void TearDownInProcessBrowserTestFixture() override { + mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); + InProcessBrowserTest::TearDownInProcessBrowserTestFixture(); + } + + HostContentSettingsMap* content_settings() { + return HostContentSettingsMapFactory::GetForProfile(browser()->profile()); + } + + // Makes use of Cross Site Redirector + content::RenderFrameHost* GetNthChildFrameWithHost( + content::RenderFrameHost* main, + base::StringPiece host, + size_t n = 0) { + size_t child_idx = 0; + while (true) { + auto* child_rfh = content::ChildFrameAt(main, child_idx++); + if (!child_rfh) { + return nullptr; + } + if (child_rfh->GetLastCommittedOrigin().host() == host) { + if (!n) { + return child_rfh; + } + --n; + } + } + } + + protected: + content::ContentMockCertVerifier mock_cert_verifier_; + EmbeddedTestServerKeepAlive https_server_{EmbeddedTestServer::TYPE_HTTPS}; + GURL es_url_; +}; + +IN_PROC_BROWSER_TEST_F(EventSourcePoolLimitBrowserTest, + PoolIsLimitedByDefault) { + const GURL url(https_server_.GetURL("a.com", "/simple.html")); + auto* rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + + OpenEventSourcesAndExpectLimited(rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit); + CloseEventSources(rfh, kEventSourceCloseScript, 5); + OpenEventSourcesAndExpectLimited(rfh, kEventSourcesOpenScript, 5); +} + +IN_PROC_BROWSER_TEST_F(EventSourcePoolLimitBrowserTest, + PoolIsKeyedByTopFrameOrigin) { + const GURL a_com_url( + https_server_.GetURL("a.com", "/ephemeral_storage.html")); + const GURL b_com_url( + https_server_.GetURL("b.com", "/ephemeral_storage.html")); + + // Open a.com with nested b.com. + auto* a_com_rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), a_com_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + auto* b_com0_in_a_com_rfh = GetNthChildFrameWithHost(a_com_rfh, "b.com"); + + // Test EventSource limit in nested b.com. + OpenEventSourcesAndExpectLimited(b_com0_in_a_com_rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit); + + // Expect the limit is also active in another nested b.com. + auto* b_com1_in_a_com_rfh = GetNthChildFrameWithHost(a_com_rfh, "b.com", 1); + ExpectEventSourcesAreLimited(b_com1_in_a_com_rfh, kEventSourcesOpenScript); + + // Expect the limit is NOT active in the first-party a.com frame, bc the pool + // is located in the a.com renderer process. + // TODO(aedelstein@brave.com): Check why -- possible concern? + auto* a_com_in_a_com_rfh = GetNthChildFrameWithHost(a_com_rfh, "a.com"); + OpenEventSources(a_com_in_a_com_rfh, kEventSourcesOpenScript, 1); + + // Open b.com with a nested a.com. + auto* b_com_rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), b_com_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + auto* a_com_in_b_com_rfh = GetNthChildFrameWithHost(b_com_rfh, "a.com"); + + // Test EventSources limit in nested a.com. + OpenEventSourcesAndExpectLimited(a_com_in_b_com_rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit); + + // Expect the limit is STILL NOT active in the first-party a.com frame. + OpenEventSources(a_com_in_a_com_rfh, kEventSourcesOpenScript, 1); +} + +IN_PROC_BROWSER_TEST_F(EventSourcePoolLimitBrowserTest, + ServiceWorkerIsLimited) { + const GURL url(https_server_.GetURL("a.com", "/simple.html")); + + auto* rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + + const std::string& register_sw_script = content::JsReplace( + kRegisterSwScript, "service-worker-eventsource-limit.js"); + ASSERT_TRUE(content::ExecJs(rfh, register_sw_script)); + + OpenEventSourcesAndExpectLimited(rfh, kEventSourcesOpenInSwScript, + kEventSourcesPoolLimit); + CloseEventSources(rfh, kEventSourceCloseInSwScript, 5); + OpenEventSources(rfh, kEventSourcesOpenInSwScript, 5); + ExpectEventSourcesAreLimited(rfh, kEventSourcesOpenInSwScript); + // Expect no Event Sources can be created on a webpage when a limit is hit. + ExpectEventSourcesAreLimited(rfh, kEventSourcesOpenScript); +} + +// Ensures that sub-frame opaque origins are treated properly when used from +// different top-frame opaque origins. +IN_PROC_BROWSER_TEST_F(EventSourcePoolLimitBrowserTest, + SandboxedFramesAreLimited) { + const GURL a_com_url( + https_server_.GetURL("a.com", "/csp_sandboxed_frame.html")); + auto* a_com_rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), a_com_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + EXPECT_TRUE(a_com_rfh->GetLastCommittedOrigin().opaque()); + + // Ensure the limit is applied to main a.com and child c.com frames. + OpenEventSourcesAndExpectLimited(a_com_rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit); + OpenEventSourcesAndExpectLimited(content::ChildFrameAt(a_com_rfh, 0), + kEventSourcesOpenScript, + kEventSourcesPoolLimit); + + const GURL b_com_url( + https_server_.GetURL("b.com", "/csp_sandboxed_frame.html")); + auto* b_com_rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), b_com_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + EXPECT_TRUE(b_com_rfh->GetLastCommittedOrigin().opaque()); + + // Ensure the limit is applied to main b.com and child c.com frames. + OpenEventSourcesAndExpectLimited(b_com_rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit); + OpenEventSourcesAndExpectLimited(content::ChildFrameAt(b_com_rfh, 0), + kEventSourcesOpenScript, + kEventSourcesPoolLimit); +} + +IN_PROC_BROWSER_TEST_F(EventSourcePoolLimitBrowserTest, + PoolIsNotLimitedWithDisabledShields) { + const GURL url(https_server_.GetURL("a.com", "/ephemeral_storage.html")); + // Disable shields. + brave_shields::SetBraveShieldsEnabled(content_settings(), false, url); + + auto* a_com_rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + + // No limits should be active. + OpenEventSources(a_com_rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit + 5); + + // No limits should be active in a 3p frame. + auto* b_com_in_a_com_rfh = GetNthChildFrameWithHost(a_com_rfh, "b.com"); + OpenEventSources(b_com_in_a_com_rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit + 5); + + // No limits should be active in a ServiceWorker. + const std::string& register_sw_script = content::JsReplace( + kRegisterSwScript, "service-worker-eventsource-limit.js"); + ASSERT_TRUE(content::ExecJs(a_com_rfh, register_sw_script)); + OpenEventSources(a_com_rfh, kEventSourcesOpenInSwScript, + kEventSourcesPoolLimit + 5); +} + +#if BUILDFLAG(ENABLE_EXTENSIONS) +IN_PROC_BROWSER_TEST_F(EventSourcePoolLimitBrowserTest, + PoolIsNotLimitedForExtensions) { + extensions::TestExtensionDir test_extension_dir; + test_extension_dir.WriteManifest(R"({ + "name": "Test", + "manifest_version": 2, + "version": "0.1", + "permissions": ["webRequest", "webRequestBlocking", "*://a.com/*"], + "content_security_policy": + "script-src 'self' 'unsafe-eval'; object-src 'self'" + })"); + test_extension_dir.WriteFile(FILE_PATH_LITERAL("empty.html"), ""); + + extensions::ChromeTestExtensionLoader extension_loader(browser()->profile()); + scoped_refptr extension = + extension_loader.LoadExtension(test_extension_dir.UnpackedPath()); + const GURL url = extension->GetResourceURL("/empty.html"); + auto* extension_rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + ASSERT_TRUE(extension_rfh); + OpenEventSources(extension_rfh, kEventSourcesOpenScript, + kEventSourcesPoolLimit + 5); +} +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + +class EventSourcePoolLimitDisabledBrowserTest + : public EventSourcePoolLimitBrowserTest { + public: + EventSourcePoolLimitDisabledBrowserTest() { + scoped_feature_list_.InitAndDisableFeature( + blink::features::kRestrictEventSourcePool); + } + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(EventSourcePoolLimitDisabledBrowserTest, + PoolIsNotLimited) { + const GURL url(https_server_.GetURL("a.com", "/simple.html")); + auto* rfh = ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + + // No limits should be active. + OpenEventSources(rfh, kEventSourcesOpenScript, kEventSourcesPoolLimit + 5); +} diff --git a/chromium_src/third_party/blink/common/features.cc b/chromium_src/third_party/blink/common/features.cc index 8e8ef6c4bd7c..ecef7620dac2 100644 --- a/chromium_src/third_party/blink/common/features.cc +++ b/chromium_src/third_party/blink/common/features.cc @@ -80,5 +80,15 @@ BASE_FEATURE(kBraveRoundTimeStamps, "BraveRoundTimeStamps", base::FEATURE_ENABLED_BY_DEFAULT); +// Enable EventSource connection pool limit per eTLD+1. +BASE_FEATURE(kRestrictEventSourcePool, + "RestrictEventSourcePool", +#if BUILDFLAG(IS_ANDROID) + base::FEATURE_DISABLED_BY_DEFAULT +#else + base::FEATURE_ENABLED_BY_DEFAULT +#endif +); + } // namespace features } // namespace blink diff --git a/chromium_src/third_party/blink/public/common/features.h b/chromium_src/third_party/blink/public/common/features.h index 344cb48f4ebd..c0f3f878a2b7 100644 --- a/chromium_src/third_party/blink/public/common/features.h +++ b/chromium_src/third_party/blink/public/common/features.h @@ -19,6 +19,7 @@ BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kRestrictWebSocketsPool); BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kBraveBlockScreenFingerprinting); BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kBraveTorWindowsHttpsOnly); BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kBraveRoundTimeStamps); +BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kRestrictEventSourcePool); } // namespace features } // namespace blink diff --git a/chromium_src/third_party/blink/renderer/modules/eventsource/event_source.cc b/chromium_src/third_party/blink/renderer/modules/eventsource/event_source.cc new file mode 100644 index 000000000000..3d779241c60a --- /dev/null +++ b/chromium_src/third_party/blink/renderer/modules/eventsource/event_source.cc @@ -0,0 +1,85 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "third_party/blink/renderer/modules/eventsource/event_source.h" + +#include "brave/third_party/blink/renderer/core/farbling/brave_session_cache.h" +#include "third_party/blink/public/common/features.h" +#include "third_party/blink/public/common/scheme_registry.h" +#include "third_party/blink/public/platform/web_content_settings_client.h" +#include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/frame/dom_window.h" +#include "third_party/blink/renderer/core/streams/readable_byte_stream_controller.h" +#include "third_party/blink/renderer/platform/loader/fetch/resource_error.h" + +#define ConnectTimerFired(...) ConnectTimerFired_ChromiumImpl(__VA_ARGS__) +#define close() close_ChromiumImpl() +#define DidFail DidFail_ChromiumImpl +#define DidFailRedirectCheck DidFailRedirectCheck_ChromiumImpl + +#include "src/third_party/blink/renderer/modules/eventsource/event_source.cc" + +#undef ConnectTimerFired +#undef close +#undef DidFail +#undef DidFailRedirectCheck + +namespace blink { + +void EventSource::ConnectTimerFired(TimerBase* timer_base) { + BraveConnect(); +} + +void EventSource::BraveConnect() { + if (base::FeatureList::IsEnabled(blink::features::kRestrictEventSourcePool)) { + ExecutionContext* execution_context = GetExecutionContext(); + if (blink::WebContentSettingsClient* settings = + brave::GetContentSettingsClientFor(execution_context)) { + const bool is_extension = CommonSchemeRegistry::IsExtensionScheme( + execution_context->GetSecurityOrigin()->Protocol().Ascii()); + if (!is_extension && + settings->GetBraveFarblingLevel() != BraveFarblingLevel::OFF) { + event_source_in_use_tracker_ = + ResourcePoolLimiter::GetInstance().IssueResourceInUseTracker( + execution_context, + ResourcePoolLimiter::ResourceType::kEventSource); + if (!event_source_in_use_tracker_) { + AbortConnectionAttempt(); + return; + } + } + } + } + Connect(); +} + +void EventSource::MaybeResetEventSourceInUseTracker() { + if (base::FeatureList::IsEnabled(blink::features::kRestrictEventSourcePool)) { + if (event_source_in_use_tracker_ != nullptr) { + event_source_in_use_tracker_.reset(); + } + } +} + +void EventSource::close() { + close_ChromiumImpl(); + MaybeResetEventSourceInUseTracker(); +} + +void EventSource::DidFail(uint64_t identifier, const ResourceError& error) { + DidFail_ChromiumImpl(identifier, error); + if (state_ == kClosed) { + MaybeResetEventSourceInUseTracker(); + } +} + +void EventSource::DidFailRedirectCheck(uint64_t identifier) { + DidFailRedirectCheck_ChromiumImpl(identifier); + if (state_ == kClosed) { + MaybeResetEventSourceInUseTracker(); + } +} + +} // namespace blink diff --git a/chromium_src/third_party/blink/renderer/modules/eventsource/event_source.h b/chromium_src/third_party/blink/renderer/modules/eventsource/event_source.h new file mode 100644 index 000000000000..29a8eba29c24 --- /dev/null +++ b/chromium_src/third_party/blink/renderer/modules/eventsource/event_source.h @@ -0,0 +1,51 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_CHROMIUM_SRC_THIRD_PARTY_BLINK_RENDERER_MODULES_EVENTSOURCE_EVENT_SOURCE_H_ +#define BRAVE_CHROMIUM_SRC_THIRD_PARTY_BLINK_RENDERER_MODULES_EVENTSOURCE_EVENT_SOURCE_H_ + +#include "brave/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.h" + +// Avoid redefining tokens in these headers: +#include "third_party/blink/renderer/core/html/closewatcher/close_watcher.h" +#include "third_party/blink/renderer/core/loader/threadable_loader_client.h" +#include "third_party/blink/renderer/platform/bindings/script_state.h" + +#define ConnectTimerFired \ + ConnectTimerFired(TimerBase*); \ + void ConnectTimerFired_ChromiumImpl + +#define Connect \ + Connect(); \ + void BraveConnect + +#define close \ + close(); \ + void close_ChromiumImpl + +#define DidFail(...) \ + DidFail_ChromiumImpl(__VA_ARGS__); \ + void DidFail(__VA_ARGS__) + +#define DidFailRedirectCheck(...) \ + MaybeResetEventSourceInUseTracker(); \ + void DidFailRedirectCheck_ChromiumImpl(__VA_ARGS__); \ + void DidFailRedirectCheck(__VA_ARGS__) + +#define world_ \ + world_; \ + std::unique_ptr \ + event_source_in_use_tracker_ + +#include "src/third_party/blink/renderer/modules/eventsource/event_source.h" + +#undef ConnectTimerFired +#undef Connect +#undef close +#undef DidFail +#undef DidFailRedirectCheck +#undef world_ + +#endif // BRAVE_CHROMIUM_SRC_THIRD_PARTY_BLINK_RENDERER_MODULES_EVENTSOURCE_EVENT_SOURCE_H_ diff --git a/test/BUILD.gn b/test/BUILD.gn index 534282a9e206..54ff0b2286d2 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -692,6 +692,7 @@ test("brave_browser_tests") { "//brave/browser/brave_shields/cookie_expiry_browsertest.cc", "//brave/browser/brave_shields/cookie_pref_service_browsertest.cc", "//brave/browser/brave_shields/domain_block_page_browsertest.cc", + "//brave/browser/brave_shields/eventsource_pool_limit_browsertest.cc", "//brave/browser/brave_shields/websockets_pool_limit_browsertest.cc", "//brave/browser/brave_stats/brave_stats_updater_browsertest.cc", "//brave/browser/brave_wallet/brave_wallet_ethereum_chain_browsertest.cc", diff --git a/test/data/service-worker-eventsource-limit.js b/test/data/service-worker-eventsource-limit.js new file mode 100644 index 000000000000..eae43b592f41 --- /dev/null +++ b/test/data/service-worker-eventsource-limit.js @@ -0,0 +1,39 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +addEventListener('install', function (event) { + event.waitUntil(self.skipWaiting()); // Activate worker immediately +}); + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()); // Become available to all pages +}); + +sources = []; + +addEventListener('message', async event => { + if (event.data.cmd === 'open_es') { + const client = await self.clients.get(event.source.id); + source = new EventSource(event.data.url); + sources.push(source); + let openEventListener, errorEventListener; + const removeEventListeners = () => { + source.removeEventListener("open", openEventListener); + source.removeEventListener("error", errorEventListener); + }; + openEventListener = (event) => { + client.postMessage('open'); + removeEventListeners(); + }; + errorEventListener = (event) => { + client.postMessage('error'); + removeEventListeners(); + }; + source.addEventListener('open', openEventListener); + source.addEventListener('error', errorEventListener); + } else if (event.data.cmd === 'close_es') { + sources[event.data.idx].close(); + } +}); diff --git a/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.cc b/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.cc index 7cee9ed5a670..b734d08af8da 100644 --- a/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.cc +++ b/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.cc @@ -50,6 +50,8 @@ int GetResourceLimit(ResourcePoolLimiter::ResourceType resource_type) { switch (resource_type) { case ResourcePoolLimiter::ResourceType::kWebSocket: return 30; + case ResourcePoolLimiter::ResourceType::kEventSource: + return 250; } NOTREACHED(); return 0; diff --git a/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.h b/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.h index e1303354e725..4831d4dca152 100644 --- a/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.h +++ b/third_party/blink/renderer/core/resource_pool_limiter/resource_pool_limiter.h @@ -23,6 +23,7 @@ class CORE_EXPORT ResourcePoolLimiter { public: enum class ResourceType { kWebSocket, + kEventSource, }; class CORE_EXPORT ResourceInUseTracker {