// Copyright 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 "base/path_service.h" #include "base/strings/strcat.h" #include "base/test/bind_test_util.h" #include "base/test/scoped_feature_list.h" #include "build/build_config.h" #include "content/public/common/content_paths.h" #include "content/public/test/browser_test.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_utils.h" #include "content/public/test/test_navigation_observer.h" #include "content/public/test/url_loader_interceptor.h" #include "content/shell/browser/shell.h" #include "net/base/features.h" #include "net/dns/mock_host_resolver.h" #include "services/network/public/cpp/features.h" namespace content { namespace { bool SupportsSharedWorker() { #if defined(OS_ANDROID) // SharedWorkers are not enabled on Android. https://crbug.com/154571 return false; #else return true; #endif } } // namespace enum class WorkerType { kServiceWorker, kSharedWorker, }; class WorkerNetworkIsolationKeyBrowserTest : public ContentBrowserTest { public: void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); https_server_ = std::make_unique( net::EmbeddedTestServer::TYPE_HTTPS); https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); https_server_->AddDefaultHandlers(GetTestDataFilePath()); content::SetupCrossSiteRedirector(https_server_.get()); ASSERT_TRUE(https_server_->Start()); } net::EmbeddedTestServer* https_server() { return https_server_.get(); } // Register a service/shared worker |main_script_file| in the scope of // |subframe_rfh|'s origin. void RegisterWorker(RenderFrameHost* subframe_rfh, WorkerType worker_type, const std::string& main_script_file) { RegisterWorkerWithUrlParameters(subframe_rfh, worker_type, main_script_file, {}); } // Register a service/shared worker |main_script_file| in the scope of // |subframe_rfh|'s origin, that does // importScripts(|import_script_url|) and fetch(|fetch_url|). void RegisterWorkerThatDoesImportScriptsAndFetch( RenderFrameHost* subframe_rfh, WorkerType worker_type, const std::string& main_script_file, const GURL& import_script_url, const GURL& fetch_url) { RegisterWorkerWithUrlParameters( subframe_rfh, worker_type, main_script_file, {{"import_script_url", import_script_url.spec()}, {"fetch_url", fetch_url.spec()}}); } RenderFrameHost* CreateSubframe(const GURL& subframe_url) { DCHECK_EQ(shell()->web_contents()->GetURL().path(), "/workers/frame_factory.html"); content::TestNavigationObserver navigation_observer( shell()->web_contents(), /*number_of_navigations*/ 1, content::MessageLoopRunner::QuitMode::DEFERRED); std::string subframe_name = GetUniqueSubframeName(); EvalJsResult result = EvalJs( shell()->web_contents()->GetMainFrame(), JsReplace("createFrame($1, $2)", subframe_url.spec(), subframe_name)); DCHECK(result.error.empty()); navigation_observer.Wait(); RenderFrameHost* subframe_rfh = FrameMatchingPredicate( shell()->web_contents(), base::BindRepeating(&FrameMatchesName, subframe_name)); DCHECK(subframe_rfh); return subframe_rfh; } protected: void InitFeatures(bool append_frame_origin_to_network_isolation_key) { if (append_frame_origin_to_network_isolation_key) { feature_list_.InitWithFeatures( {net::features::kSplitCacheByNetworkIsolationKey, net::features::kAppendFrameOriginToNetworkIsolationKey}, {}); } else { feature_list_.InitWithFeatures( {net::features::kSplitCacheByNetworkIsolationKey}, {net::features::kAppendFrameOriginToNetworkIsolationKey}); } } private: void RegisterWorkerWithUrlParameters( RenderFrameHost* subframe_rfh, WorkerType worker_type, const std::string& main_script_file, const std::map& params) { std::string main_script_file_with_param(main_script_file); for (auto it = params.begin(); it != params.end(); ++it) { main_script_file_with_param += base::StrCat( {(it == params.begin()) ? "?" : "&", it->first, "=", it->second}); } switch (worker_type) { case WorkerType::kServiceWorker: DCHECK(subframe_rfh->GetLastCommittedURL().path() == "/workers/service_worker_setup.html"); EXPECT_EQ("ok", EvalJs(subframe_rfh, JsReplace("setup($1,$2)", main_script_file_with_param, "{\"updateViaCache\": \"all\"}"))); break; case WorkerType::kSharedWorker: EXPECT_EQ(nullptr, EvalJs(subframe_rfh, JsReplace("let worker = new SharedWorker($1)", main_script_file_with_param))); break; } } std::string GetUniqueSubframeName() { subframe_id_ += 1; return "subframe_name_" + base::NumberToString(subframe_id_); } size_t subframe_id_ = 0; base::test::ScopedFeatureList feature_list_; std::unique_ptr https_server_; }; class WorkerImportScriptsAndFetchRequestNetworkIsolationKeyBrowserTest : public WorkerNetworkIsolationKeyBrowserTest, public ::testing::WithParamInterface< std::tuple> { public: void SetUp() override { bool append_frame_origin_to_network_isolation_key; std::tie(append_frame_origin_to_network_isolation_key, std::ignore, std::ignore) = GetParam(); InitFeatures(append_frame_origin_to_network_isolation_key); ContentBrowserTest::SetUp(); } }; // Test that network isolation key is filled in correctly for service/shared // workers. The test navigates to "a.test" and creates two cross-origin iframes // that each start a worker. The frames/workers may have the same origin, so // worker1 is on "b.test" and worker2 is on either "b.test" or "c.test". The // test checks the cache status of importScripts() and a fetch() request from // the workers to another origin "d.test". When the workers had the same origin // (the same network isolation key), we expect the second importScripts() and // fetch() request to exist in the cache. When the origins are different, we // expect the second requests to not exist in the cache. IN_PROC_BROWSER_TEST_P( WorkerImportScriptsAndFetchRequestNetworkIsolationKeyBrowserTest, ImportScriptsAndFetchRequest) { bool test_same_network_isolation_key; WorkerType worker_type; std::tie(std::ignore, test_same_network_isolation_key, worker_type) = GetParam(); if (worker_type == WorkerType::kSharedWorker && !SupportsSharedWorker()) return; GURL import_script_url = https_server()->GetURL("d.test", "/workers/empty.js"); GURL fetch_url = https_server()->GetURL("d.test", "/workers/empty.html"); std::map request_completed_count; base::RunLoop cache_status_waiter; URLLoaderInterceptor interceptor( base::BindLambdaForTesting( [&](URLLoaderInterceptor::RequestParams* params) { return false; }), base::BindLambdaForTesting( [&](const GURL& request_url, const network::URLLoaderCompletionStatus& status) { if (request_url == import_script_url || request_url == fetch_url) { size_t& num_completed = request_completed_count[request_url]; num_completed += 1; if (num_completed == 1) { EXPECT_FALSE(status.exists_in_cache); } else if (num_completed == 2) { EXPECT_EQ(status.exists_in_cache, test_same_network_isolation_key); } else { NOTREACHED(); } } if (request_completed_count[import_script_url] == 2 && request_completed_count[fetch_url] == 2) { cache_status_waiter.Quit(); } }), {}); NavigateToURLBlockUntilNavigationsComplete( shell(), https_server()->GetURL("a.test", "/workers/frame_factory.html"), 1); RenderFrameHost* subframe_rfh_1 = CreateSubframe( https_server()->GetURL("b.test", "/workers/service_worker_setup.html")); RegisterWorkerThatDoesImportScriptsAndFetch(subframe_rfh_1, worker_type, "worker_with_import_and_fetch.js", import_script_url, fetch_url); RenderFrameHost* subframe_rfh_2 = CreateSubframe(https_server()->GetURL( test_same_network_isolation_key ? "b.test" : "c.test", "/workers/service_worker_setup.html")); RegisterWorkerThatDoesImportScriptsAndFetch( subframe_rfh_2, worker_type, "worker_with_import_and_fetch_2.js", import_script_url, fetch_url); cache_status_waiter.Run(); } INSTANTIATE_TEST_SUITE_P( All, WorkerImportScriptsAndFetchRequestNetworkIsolationKeyBrowserTest, ::testing::Combine(testing::Bool(), testing::Bool(), ::testing::Values(WorkerType::kServiceWorker, WorkerType::kSharedWorker))); class ServiceWorkerMainScriptRequestNetworkIsolationKeyBrowserTest : public WorkerNetworkIsolationKeyBrowserTest, public ::testing::WithParamInterface< bool /* append_frame_origin_to_network_isolation_key */> { public: void SetUp() override { bool append_frame_origin_to_network_isolation_key = GetParam(); InitFeatures(append_frame_origin_to_network_isolation_key); ContentBrowserTest::SetUp(); } }; // Test that network isolation key is filled in correctly for service worker's // main script request. The test navigates to "a.test" and creates an iframe // having origin "c.test" that registers |worker1|. The test then navigates to // "b.test" and creates an iframe also having origin "c.test". We now want to // test a second register request for |worker1| but just calling register() // would be a no-op since |worker1| is already the current worker. So we // register a new |worker2| and then |worker1| again. // // Note that the second navigation to "c.test" also triggers an update check for // |worker1|. We expect both the second register request for |worker1| and this // update request to exist in the cache. // // Note that it's sufficient not to test the cache miss when subframe origins // are different as in that case the two script urls must be different and it // also won't trigger an update. IN_PROC_BROWSER_TEST_P( ServiceWorkerMainScriptRequestNetworkIsolationKeyBrowserTest, ServiceWorkerMainScriptRequest) { size_t num_completed = 0; std::string main_script_file = "empty.js"; GURL main_script_request_url = https_server()->GetURL("c.test", "/workers/" + main_script_file); base::RunLoop cache_status_waiter; URLLoaderInterceptor interceptor( base::BindLambdaForTesting( [&](URLLoaderInterceptor::RequestParams* params) { return false; }), base::BindLambdaForTesting( [&](const GURL& request_url, const network::URLLoaderCompletionStatus& status) { if (request_url == main_script_request_url) { num_completed += 1; if (num_completed == 1) { EXPECT_FALSE(status.exists_in_cache); } else if (num_completed == 2) { EXPECT_TRUE(status.exists_in_cache); } else if (num_completed == 3) { EXPECT_TRUE(status.exists_in_cache); cache_status_waiter.Quit(); } else { NOTREACHED(); } } }), {}); // Navigate to "a.test" and create the iframe "c.test", which registers // |worker1|. NavigateToURLBlockUntilNavigationsComplete( shell(), https_server()->GetURL("a.test", "/workers/frame_factory.html"), 1); RenderFrameHost* subframe_rfh_1 = CreateSubframe( https_server()->GetURL("c.test", "/workers/service_worker_setup.html")); RegisterWorker(subframe_rfh_1, WorkerType::kServiceWorker, "empty.js"); // Navigate to "b.test" and create the another iframe on "c.test", which // registers |worker2| and then |worker1| again. NavigateToURLBlockUntilNavigationsComplete( shell(), https_server()->GetURL("b.test", "/workers/frame_factory.html"), 1); RenderFrameHost* subframe_rfh_2 = CreateSubframe( https_server()->GetURL("c.test", "/workers/service_worker_setup.html")); RegisterWorker(subframe_rfh_2, WorkerType::kServiceWorker, "empty2.js"); RegisterWorker(subframe_rfh_2, WorkerType::kServiceWorker, "empty.js"); cache_status_waiter.Run(); } INSTANTIATE_TEST_SUITE_P( All, ServiceWorkerMainScriptRequestNetworkIsolationKeyBrowserTest, testing::Bool()); using SharedWorkerMainScriptRequestNetworkIsolationKeyBrowserTest = ServiceWorkerMainScriptRequestNetworkIsolationKeyBrowserTest; // Test that network isolation key is filled in correctly for shared worker's // main script request. The test navigates to "a.test" and creates an iframe // having origin "c.test" that creates |worker1|. The test then navigates to // "b.test" and creates an iframe also having origin "c.test" that creates // |worker1| again. // // We expect the second creation request for |worker1| to exist in the cache. // // Note that it's sufficient not to test the cache miss when subframe origins // are different as in that case the two script urls must be different. IN_PROC_BROWSER_TEST_P( SharedWorkerMainScriptRequestNetworkIsolationKeyBrowserTest, SharedWorkerMainScriptRequest) { if (!SupportsSharedWorker()) return; size_t num_completed = 0; std::string main_script_file = "empty.js"; GURL main_script_request_url = https_server()->GetURL("c.test", "/workers/" + main_script_file); base::RunLoop cache_status_waiter; URLLoaderInterceptor interceptor( base::BindLambdaForTesting( [&](URLLoaderInterceptor::RequestParams* params) { return false; }), base::BindLambdaForTesting( [&](const GURL& request_url, const network::URLLoaderCompletionStatus& status) { if (request_url == main_script_request_url) { num_completed += 1; if (num_completed == 1) { EXPECT_FALSE(status.exists_in_cache); } else if (num_completed == 2) { EXPECT_TRUE(status.exists_in_cache); cache_status_waiter.Quit(); } else { NOTREACHED(); } } }), {}); // Navigate to "a.test" and create the iframe "c.test", which creates // |worker1|. NavigateToURLBlockUntilNavigationsComplete( shell(), https_server()->GetURL("a.test", "/workers/frame_factory.html"), 1); RenderFrameHost* subframe_rfh_1 = CreateSubframe( https_server()->GetURL("c.test", "/workers/service_worker_setup.html")); RegisterWorker(subframe_rfh_1, WorkerType::kSharedWorker, "empty.js"); // Navigate to "b.test" and create the another iframe on "c.test", which // creates |worker1| again. NavigateToURLBlockUntilNavigationsComplete( shell(), https_server()->GetURL("b.test", "/workers/frame_factory.html"), 1); RenderFrameHost* subframe_rfh_2 = CreateSubframe( https_server()->GetURL("c.test", "/workers/service_worker_setup.html")); RegisterWorker(subframe_rfh_2, WorkerType::kSharedWorker, "empty.js"); cache_status_waiter.Run(); } INSTANTIATE_TEST_SUITE_P( All, SharedWorkerMainScriptRequestNetworkIsolationKeyBrowserTest, testing::Bool()); } // namespace content