| // 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 <string> |
| #include <vector> |
| |
| #include "base/strings/stringprintf.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "content/browser/blob_storage/chrome_blob_storage_context.h" |
| #include "content/browser/frame_host/render_frame_host_impl.h" |
| #include "content/browser/loader/prefetch_browsertest_base.h" |
| #include "content/browser/loader/prefetched_signed_exchange_cache.h" |
| #include "content/browser/web_package/mock_signed_exchange_handler.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "net/base/features.h" |
| #include "services/network/public/cpp/features.h" |
| #include "storage/browser/blob/blob_storage_context.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| std::string GetHeaderIntegrityString(const net::SHA256HashValue& hash) { |
| std::string header_integrity_string = net::HashValue(hash).ToString(); |
| // Change "sha256/" to "sha256-". |
| header_integrity_string[6] = '-'; |
| return header_integrity_string; |
| } |
| |
| } // namespace |
| |
| struct SignedExchangeSubresourcePrefetchBrowserTestParam { |
| SignedExchangeSubresourcePrefetchBrowserTestParam( |
| bool network_service_enabled) |
| : network_service_enabled(network_service_enabled) {} |
| const bool network_service_enabled; |
| }; |
| |
| class SignedExchangeSubresourcePrefetchBrowserTest |
| : public PrefetchBrowserTestBase, |
| public testing::WithParamInterface< |
| SignedExchangeSubresourcePrefetchBrowserTestParam> { |
| public: |
| SignedExchangeSubresourcePrefetchBrowserTest() = default; |
| ~SignedExchangeSubresourcePrefetchBrowserTest() = default; |
| |
| void SetUp() override { |
| std::vector<base::Feature> enable_features; |
| std::vector<base::Feature> disabled_features; |
| enable_features.push_back(features::kSignedHTTPExchange); |
| enable_features.push_back(features::kSignedExchangeSubresourcePrefetch); |
| if (GetParam().network_service_enabled) { |
| enable_features.push_back(network::features::kNetworkService); |
| } else { |
| disabled_features.push_back(network::features::kNetworkService); |
| } |
| feature_list_.InitWithFeatures(enable_features, disabled_features); |
| PrefetchBrowserTestBase::SetUp(); |
| } |
| |
| protected: |
| static constexpr size_t kTestBlobStorageIPCThresholdBytes = 20; |
| static constexpr size_t kTestBlobStorageMaxSharedMemoryBytes = 50; |
| static constexpr size_t kTestBlobStorageMaxBlobMemorySize = 400; |
| static constexpr uint64_t kTestBlobStorageMaxDiskSpace = 500; |
| static constexpr uint64_t kTestBlobStorageMinFileSizeBytes = 10; |
| static constexpr uint64_t kTestBlobStorageMaxFileSizeBytes = 100; |
| |
| const PrefetchedSignedExchangeCache::EntryMap& GetCachedExchanges() { |
| RenderViewHost* rvh = shell()->web_contents()->GetRenderViewHost(); |
| RenderFrameHostImpl* rfh = |
| static_cast<RenderFrameHostImpl*>(rvh->GetMainFrame()); |
| scoped_refptr<PrefetchedSignedExchangeCache> cache = |
| rfh->EnsurePrefetchedSignedExchangeCache(); |
| return cache->exchanges_; |
| } |
| |
| void SetBlobLimits() { |
| scoped_refptr<ChromeBlobStorageContext> blob_context = |
| ChromeBlobStorageContext::GetFor( |
| shell()->web_contents()->GetBrowserContext()); |
| base::PostTaskWithTraits( |
| FROM_HERE, {BrowserThread::IO}, |
| base::BindOnce( |
| &SignedExchangeSubresourcePrefetchBrowserTest::SetBlobLimitsOnIO, |
| blob_context)); |
| } |
| |
| private: |
| static void SetBlobLimitsOnIO( |
| scoped_refptr<ChromeBlobStorageContext> context) { |
| storage::BlobStorageLimits limits; |
| limits.max_ipc_memory_size = kTestBlobStorageIPCThresholdBytes; |
| limits.max_shared_memory_size = kTestBlobStorageMaxSharedMemoryBytes; |
| limits.max_blob_in_memory_space = kTestBlobStorageMaxBlobMemorySize; |
| limits.desired_max_disk_space = kTestBlobStorageMaxDiskSpace; |
| limits.effective_max_disk_space = kTestBlobStorageMaxDiskSpace; |
| limits.min_page_file_size = kTestBlobStorageMinFileSizeBytes; |
| limits.max_file_size = kTestBlobStorageMaxFileSizeBytes; |
| context->context()->set_limits_for_testing(limits); |
| } |
| base::test::ScopedFeatureList feature_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SignedExchangeSubresourcePrefetchBrowserTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchedSignedExchangeCache) { |
| int target_sxg_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| |
| base::RunLoop target_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), target_sxg_path, |
| &target_sxg_fetch_count, &target_sxg_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse( |
| target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML content |
| // as "application/signed-exchange;v=b3". |
| ResponseEntry("<head><title>Prefetch Target (SXG)</title></head>", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, target_url, |
| "text/html", {}, target_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| target_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, target_sxg_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| |
| EXPECT_EQ(1, prefetch_url_loader_called_); |
| |
| const auto& cached_exchanges = GetCachedExchanges(); |
| EXPECT_EQ(1u, cached_exchanges.size()); |
| const auto it = cached_exchanges.find(target_sxg_url); |
| ASSERT_TRUE(it != cached_exchanges.end()); |
| EXPECT_EQ(target_sxg_url, it->second->outer_url()); |
| EXPECT_EQ(target_url, it->second->inner_url()); |
| EXPECT_EQ(target_header_integrity, *it->second->header_integrity()); |
| |
| // Shutdown the server. |
| EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); |
| |
| // Subsequent navigation to the target URL wouldn't hit the network for |
| // the target URL. The target content should still be read correctly. |
| // The content is loaded from PrefetchedSignedExchangeCache. |
| NavigateToURLAndWaitTitle(target_sxg_url, "Prefetch Target (SXG)"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchedSignedExchangeCache_SameUrl) { |
| int target_sxg_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.html"; |
| const char* target_path = "/target.html"; |
| |
| base::RunLoop target_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), target_sxg_path, |
| &target_sxg_fetch_count, &target_sxg_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse( |
| target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML content |
| // as "application/signed-exchange;v=b3". |
| ResponseEntry("<head><title>Prefetch Target (SXG)</title></head>", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, target_url, |
| "text/html", {}, target_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| target_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, target_sxg_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| |
| EXPECT_EQ(1, prefetch_url_loader_called_); |
| |
| const auto& cached_exchanges = GetCachedExchanges(); |
| EXPECT_EQ(1u, cached_exchanges.size()); |
| const auto it = cached_exchanges.find(target_sxg_url); |
| ASSERT_TRUE(it != cached_exchanges.end()); |
| EXPECT_EQ(target_sxg_url, it->second->outer_url()); |
| EXPECT_EQ(target_url, it->second->inner_url()); |
| EXPECT_EQ(target_header_integrity, *it->second->header_integrity()); |
| |
| // Shutdown the server. |
| EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); |
| |
| // Subsequent navigation to the target URL wouldn't hit the network for |
| // the target URL. The target content should still be read correctly. |
| // The content is loaded from PrefetchedSignedExchangeCache. |
| NavigateToURLAndWaitTitle(target_sxg_url, "Prefetch Target (SXG)"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchedSignedExchangeCache_BlobStorageLimit) { |
| SetBlobLimits(); |
| int target_sxg_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| |
| base::RunLoop target_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), target_sxg_path, |
| &target_sxg_fetch_count, &target_sxg_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| std::string content = "<head><title>Prefetch Target (SXG)</title></head>"; |
| // Make the content larger than the disk space. |
| content.resize(kTestBlobStorageMaxDiskSpace + 1, ' '); |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry(content, "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, target_url, |
| "text/html", {}, target_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| target_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, target_sxg_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| |
| EXPECT_EQ(1, prefetch_url_loader_called_); |
| |
| const auto& cached_exchanges = GetCachedExchanges(); |
| // The content of prefetched SXG is larger than the Blob storage limit. |
| // So the SXG should not be stored to the cache. |
| EXPECT_EQ(0u, cached_exchanges.size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchAlternativeSubresourceSXG) { |
| int script_sxg_fetch_count = 0; |
| int script_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| const char* script_sxg_path = "/script_js.sxg"; |
| const char* script_path = "/script.js"; |
| |
| base::RunLoop script_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script_sxg_path, |
| &script_sxg_fetch_count, &script_sxg_prefetch_waiter); |
| base::RunLoop script_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script_path, |
| &script_fetch_count, &script_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| const GURL script_sxg_url = embedded_test_server()->GetURL(script_sxg_path); |
| const GURL script_url = embedded_test_server()->GetURL(script_path); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script_header_integrity = {{0x02}}; |
| const std::string script_header_integrity_string = |
| GetHeaderIntegrityString(script_header_integrity); |
| |
| const std::string outer_link_header = base::StringPrintf( |
| "<%s>;" |
| "rel=\"alternate\";" |
| "type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"", |
| script_sxg_url.spec().c_str(), script_url.spec().c_str()); |
| const std::string inner_link_headers = base::StringPrintf( |
| "Link: " |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"," |
| "<%s>;rel=\"preload\";as=\"script\"", |
| script_url.spec().c_str(), script_header_integrity_string.c_str(), |
| script_url.spec().c_str()); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(script_path, ResponseEntry("document.title=\"from server\";", |
| "text/javascript")); |
| RegisterResponse(target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\"></script></head>", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}, |
| {"link", outer_link_header}})); |
| RegisterResponse(script_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a JS |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("document.title=\"done\";", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| target_url, "text/html", {inner_link_headers}, |
| target_header_integrity), |
| MockSignedExchangeHandlerParams( |
| script_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| script_url, "text/javascript", {}, script_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| script_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, script_sxg_fetch_count); |
| EXPECT_EQ(0, script_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script_sxg_url); |
| |
| const auto& cached_exchanges = GetCachedExchanges(); |
| EXPECT_EQ(2u, cached_exchanges.size()); |
| |
| const auto target_it = cached_exchanges.find(target_sxg_url); |
| ASSERT_TRUE(target_it != cached_exchanges.end()); |
| EXPECT_EQ(target_sxg_url, target_it->second->outer_url()); |
| EXPECT_EQ(target_url, target_it->second->inner_url()); |
| EXPECT_EQ(target_header_integrity, *target_it->second->header_integrity()); |
| |
| const auto script_it = cached_exchanges.find(script_sxg_url); |
| ASSERT_TRUE(script_it != cached_exchanges.end()); |
| EXPECT_EQ(script_sxg_url, script_it->second->outer_url()); |
| EXPECT_EQ(script_url, script_it->second->inner_url()); |
| EXPECT_EQ(script_header_integrity, *script_it->second->header_integrity()); |
| |
| // Subsequent navigation to the target URL wouldn't hit the network for |
| // the target URL. The target content should still be read correctly. |
| // The content is loaded from PrefetchedSignedExchangeCache. And the script |
| // is also loaded from PrefetchedSignedExchangeCache. |
| NavigateToURLAndWaitTitle(target_sxg_url, "done"); |
| |
| EXPECT_EQ(1, script_sxg_fetch_count); |
| EXPECT_EQ(0, script_fetch_count); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchAlternativeSubresourceSXG_MultipleResources) { |
| int script1_sxg_fetch_count = 0; |
| int script1_fetch_count = 0; |
| int script2_sxg_fetch_count = 0; |
| int script2_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| const char* script1_sxg_path = "/script1_js.sxg"; |
| const char* script1_path = "/script1.js"; |
| const char* script2_sxg_path = "/script2_js.sxg"; |
| const char* script2_path = "/script2.js"; |
| |
| base::RunLoop script1_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script1_sxg_path, |
| &script1_sxg_fetch_count, |
| &script1_sxg_prefetch_waiter); |
| base::RunLoop script1_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script1_path, |
| &script1_fetch_count, &script1_prefetch_waiter); |
| base::RunLoop script2_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script2_sxg_path, |
| &script2_sxg_fetch_count, |
| &script2_sxg_prefetch_waiter); |
| base::RunLoop script2_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script2_path, |
| &script2_fetch_count, &script2_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| const GURL script1_sxg_url = embedded_test_server()->GetURL(script1_sxg_path); |
| const GURL script1_url = embedded_test_server()->GetURL(script1_path); |
| const GURL script2_sxg_url = embedded_test_server()->GetURL(script2_sxg_path); |
| const GURL script2_url = embedded_test_server()->GetURL(script2_path); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script1_header_integrity = {{0x02}}; |
| const std::string script1_header_integrity_string = |
| GetHeaderIntegrityString(script1_header_integrity); |
| const net::SHA256HashValue script2_header_integrity = {{0x03}}; |
| const std::string script2_header_integrity_string = |
| GetHeaderIntegrityString(script2_header_integrity); |
| |
| const std::string outer_link_header = base::StringPrintf( |
| "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"," |
| "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"", |
| script1_sxg_url.spec().c_str(), script1_url.spec().c_str(), |
| script2_sxg_url.spec().c_str(), script2_url.spec().c_str()); |
| const std::string inner_link_headers = base::StringPrintf( |
| "Link: " |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"," |
| "<%s>;rel=\"preload\";as=\"script\"," |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"," |
| "<%s>;rel=\"preload\";as=\"script\"", |
| script1_url.spec().c_str(), script1_header_integrity_string.c_str(), |
| script1_url.spec().c_str(), script2_url.spec().c_str(), |
| script2_header_integrity_string.c_str(), script2_url.spec().c_str()); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(script1_path, ResponseEntry("var test_title=\"from\";", |
| "text/javascript")); |
| RegisterResponse(script2_path, |
| ResponseEntry("document.title=test_title+\"server\";", |
| "text/javascript")); |
| RegisterResponse( |
| target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script1.js\"></script>" |
| "<script src=\"./script2.js\"></script></head>", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}, |
| {"link", outer_link_header}})); |
| RegisterResponse(script1_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a JS |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("var test_title=\"done\";", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| RegisterResponse(script2_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a JS |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("document.title=test_title;", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| MockSignedExchangeHandlerFactory factory({ |
| MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| target_url, "text/html", {inner_link_headers}, |
| target_header_integrity), |
| MockSignedExchangeHandlerParams( |
| script1_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| script1_url, "text/javascript", {}, script1_header_integrity), |
| MockSignedExchangeHandlerParams( |
| script2_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| script2_url, "text/javascript", {}, script2_header_integrity), |
| }); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| script1_sxg_prefetch_waiter.Run(); |
| script2_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, script1_sxg_fetch_count); |
| EXPECT_EQ(0, script1_fetch_count); |
| EXPECT_EQ(1, script2_sxg_fetch_count); |
| EXPECT_EQ(0, script2_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script1_sxg_url); |
| WaitUntilLoaded(script2_sxg_url); |
| |
| const auto& cached_exchanges = GetCachedExchanges(); |
| EXPECT_EQ(3u, cached_exchanges.size()); |
| |
| const auto target_it = cached_exchanges.find(target_sxg_url); |
| ASSERT_TRUE(target_it != cached_exchanges.end()); |
| EXPECT_EQ(target_sxg_url, target_it->second->outer_url()); |
| EXPECT_EQ(target_url, target_it->second->inner_url()); |
| EXPECT_EQ(target_header_integrity, *target_it->second->header_integrity()); |
| |
| const auto script1_it = cached_exchanges.find(script1_sxg_url); |
| ASSERT_TRUE(script1_it != cached_exchanges.end()); |
| EXPECT_EQ(script1_sxg_url, script1_it->second->outer_url()); |
| EXPECT_EQ(script1_url, script1_it->second->inner_url()); |
| EXPECT_EQ(script1_header_integrity, *script1_it->second->header_integrity()); |
| |
| const auto script2_it = cached_exchanges.find(script2_sxg_url); |
| ASSERT_TRUE(script2_it != cached_exchanges.end()); |
| EXPECT_EQ(script2_sxg_url, script2_it->second->outer_url()); |
| EXPECT_EQ(script2_url, script2_it->second->inner_url()); |
| EXPECT_EQ(script2_header_integrity, *script2_it->second->header_integrity()); |
| |
| // Subsequent navigation to the target URL wouldn't hit the network for |
| // the target URL. The target content should still be read correctly. |
| // The content is loaded from PrefetchedSignedExchangeCache. And the scripts |
| // are also loaded from PrefetchedSignedExchangeCache. |
| NavigateToURLAndWaitTitle(target_sxg_url, "done"); |
| |
| EXPECT_EQ(1, script1_sxg_fetch_count); |
| EXPECT_EQ(0, script1_fetch_count); |
| EXPECT_EQ(1, script2_sxg_fetch_count); |
| EXPECT_EQ(0, script2_fetch_count); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchAlternativeSubresourceSXG_SameUrl) { |
| int script_sxg_fetch_count = 0; |
| int script_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.html"; |
| const char* target_path = "/target.html"; |
| const char* script_sxg_path = "/script.js"; |
| const char* script_path = "/script.js"; |
| |
| base::RunLoop script_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script_sxg_path, |
| &script_sxg_fetch_count, &script_sxg_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| const GURL script_sxg_url = embedded_test_server()->GetURL(script_sxg_path); |
| const GURL script_url = embedded_test_server()->GetURL(script_path); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script_header_integrity = {{0x02}}; |
| const std::string script_header_integrity_string = |
| GetHeaderIntegrityString(script_header_integrity); |
| |
| const std::string outer_link_header = base::StringPrintf( |
| "<%s>;" |
| "rel=\"alternate\";" |
| "type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"", |
| script_sxg_url.spec().c_str(), script_url.spec().c_str()); |
| const std::string inner_link_headers = base::StringPrintf( |
| "Link: " |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"," |
| "<%s>;rel=\"preload\";as=\"script\"", |
| script_url.spec().c_str(), script_header_integrity_string.c_str(), |
| script_url.spec().c_str()); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(script_path, ResponseEntry("document.title=\"from server\";", |
| "text/javascript")); |
| RegisterResponse(target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\"></script></head>", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}, |
| {"link", outer_link_header}})); |
| RegisterResponse(script_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a JS |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("document.title=\"done\";", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| target_url, "text/html", {inner_link_headers}, |
| target_header_integrity), |
| MockSignedExchangeHandlerParams( |
| script_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| script_url, "text/javascript", {}, script_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| script_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, script_sxg_fetch_count); |
| EXPECT_EQ(0, script_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script_sxg_url); |
| |
| const auto& cached_exchanges = GetCachedExchanges(); |
| EXPECT_EQ(2u, cached_exchanges.size()); |
| |
| const auto target_it = cached_exchanges.find(target_sxg_url); |
| ASSERT_TRUE(target_it != cached_exchanges.end()); |
| EXPECT_EQ(target_sxg_url, target_it->second->outer_url()); |
| EXPECT_EQ(target_url, target_it->second->inner_url()); |
| EXPECT_EQ(target_header_integrity, *target_it->second->header_integrity()); |
| |
| const auto script_it = cached_exchanges.find(script_sxg_url); |
| ASSERT_TRUE(script_it != cached_exchanges.end()); |
| EXPECT_EQ(script_sxg_url, script_it->second->outer_url()); |
| EXPECT_EQ(script_url, script_it->second->inner_url()); |
| EXPECT_EQ(script_header_integrity, *script_it->second->header_integrity()); |
| |
| // Subsequent navigation to the target URL wouldn't hit the network for |
| // the target URL. The target content should still be read correctly. |
| // The content is loaded from PrefetchedSignedExchangeCache. And the script |
| // is also loaded from PrefetchedSignedExchangeCache. |
| NavigateToURLAndWaitTitle(target_sxg_url, "done"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchAlternativeSubresourceSXG_IntegrityMismatch) { |
| int script_sxg_fetch_count = 0; |
| int script_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| const char* script_path = "/script.js"; |
| const char* script_sxg_path = "/script_js.sxg"; |
| |
| base::RunLoop script_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script_sxg_path, |
| &script_sxg_fetch_count, &script_sxg_prefetch_waiter); |
| base::RunLoop script_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script_path, |
| &script_fetch_count, &script_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| const GURL script_sxg_url = embedded_test_server()->GetURL(script_sxg_path); |
| const GURL script_url = embedded_test_server()->GetURL(script_path); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script_header_integrity = {{0x02}}; |
| const net::SHA256HashValue wrong_script_header_integrity = {{0x03}}; |
| // Use the wrong header integrity value for "allowed-alt-sxg" link header to |
| // trigger the integrity mismatch fallback logic. |
| const std::string script_header_integrity_string = |
| GetHeaderIntegrityString(wrong_script_header_integrity); |
| |
| const std::string outer_link_header = base::StringPrintf( |
| "<%s>;" |
| "rel=\"alternate\";" |
| "type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"", |
| script_sxg_url.spec().c_str(), script_url.spec().c_str()); |
| const std::string inner_link_headers = base::StringPrintf( |
| "Link: " |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"," |
| "<%s>;rel=\"preload\";as=\"script\"", |
| script_url.spec().c_str(), script_header_integrity_string.c_str(), |
| script_url.spec().c_str()); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(script_path, ResponseEntry("document.title=\"from server\";", |
| "text/javascript")); |
| RegisterResponse(target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\"></script></head>", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}, |
| {"link", outer_link_header}})); |
| RegisterResponse(script_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a JS |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("document.title=\"done\";", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| target_url, "text/html", {inner_link_headers}, |
| target_header_integrity), |
| MockSignedExchangeHandlerParams( |
| script_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| script_url, "text/javascript", {}, script_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| script_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, script_sxg_fetch_count); |
| EXPECT_EQ(0, script_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script_sxg_url); |
| |
| // The value of "header-integrity" in "allowed-alt-sxg" link header of the |
| // inner response doesn't match the actual header integrity of script_js.sxg. |
| // So the script request must go to the server. |
| NavigateToURLAndWaitTitle(target_sxg_url, "from server"); |
| |
| EXPECT_EQ(1, script_fetch_count); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| SignedExchangeSubresourcePrefetchBrowserTest, |
| PrefetchAlternativeSubresourceSXG_MultipleResources_IntegrityMismatch) { |
| int script1_sxg_fetch_count = 0; |
| int script1_fetch_count = 0; |
| int script2_sxg_fetch_count = 0; |
| int script2_fetch_count = 0; |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| const char* script1_sxg_path = "/script1_js.sxg"; |
| const char* script1_path = "/script1.js"; |
| const char* script2_sxg_path = "/script2_js.sxg"; |
| const char* script2_path = "/script2.js"; |
| |
| base::RunLoop script1_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script1_sxg_path, |
| &script1_sxg_fetch_count, |
| &script1_sxg_prefetch_waiter); |
| base::RunLoop script1_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script1_path, |
| &script1_fetch_count, &script1_prefetch_waiter); |
| base::RunLoop script2_sxg_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script2_sxg_path, |
| &script2_sxg_fetch_count, |
| &script2_sxg_prefetch_waiter); |
| base::RunLoop script2_prefetch_waiter; |
| RegisterRequestMonitor(embedded_test_server(), script2_path, |
| &script2_fetch_count, &script2_prefetch_waiter); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, prefetch_url_loader_called_); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| const GURL script1_sxg_url = embedded_test_server()->GetURL(script1_sxg_path); |
| const GURL script1_url = embedded_test_server()->GetURL(script1_path); |
| const GURL script2_sxg_url = embedded_test_server()->GetURL(script2_sxg_path); |
| const GURL script2_url = embedded_test_server()->GetURL(script2_path); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script1_header_integrity = {{0x02}}; |
| const std::string script1_header_integrity_string = |
| GetHeaderIntegrityString(script1_header_integrity); |
| const net::SHA256HashValue script2_header_integrity = {{0x03}}; |
| const net::SHA256HashValue wrong_script2_header_integrity = {{0x04}}; |
| // Use the wrong header integrity value for "allowed-alt-sxg" link header to |
| // trigger the integrity mismatch fallback logic. |
| const std::string script2_header_integrity_string = |
| GetHeaderIntegrityString(wrong_script2_header_integrity); |
| |
| const std::string outer_link_header = base::StringPrintf( |
| "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"," |
| "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"", |
| script1_sxg_url.spec().c_str(), script1_url.spec().c_str(), |
| script2_sxg_url.spec().c_str(), script2_url.spec().c_str()); |
| const std::string inner_link_headers = base::StringPrintf( |
| "Link: " |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"," |
| "<%s>;rel=\"preload\";as=\"script\"," |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"," |
| "<%s>;rel=\"preload\";as=\"script\"", |
| script1_url.spec().c_str(), script1_header_integrity_string.c_str(), |
| script1_url.spec().c_str(), script2_url.spec().c_str(), |
| script2_header_integrity_string.c_str(), script2_url.spec().c_str()); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(script1_path, ResponseEntry("var test_title=\"from\";", |
| "text/javascript")); |
| RegisterResponse(script2_path, |
| ResponseEntry("document.title=test_title+\" server\";", |
| "text/javascript")); |
| RegisterResponse( |
| target_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a HTML |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script1.js\"></script>" |
| "<script src=\"./script2.js\"></script></head>", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}, |
| {"link", outer_link_header}})); |
| RegisterResponse(script1_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a JS |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("var test_title=\"done\";", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| RegisterResponse(script2_sxg_path, |
| // We mock the SignedExchangeHandler, so just return a JS |
| // content as "application/signed-exchange;v=b3". |
| ResponseEntry("document.title=test_title;", |
| "application/signed-exchange;v=b3", |
| {{"x-content-type-options", "nosniff"}})); |
| MockSignedExchangeHandlerFactory factory({ |
| MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| target_url, "text/html", {inner_link_headers}, |
| target_header_integrity), |
| MockSignedExchangeHandlerParams( |
| script1_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| script1_url, "text/javascript", {}, script1_header_integrity), |
| MockSignedExchangeHandlerParams( |
| script2_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| script2_url, "text/javascript", {}, script2_header_integrity), |
| }); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| script1_sxg_prefetch_waiter.Run(); |
| script2_sxg_prefetch_waiter.Run(); |
| EXPECT_EQ(1, script1_sxg_fetch_count); |
| EXPECT_EQ(0, script1_fetch_count); |
| EXPECT_EQ(1, script2_sxg_fetch_count); |
| EXPECT_EQ(0, script2_fetch_count); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script1_sxg_url); |
| WaitUntilLoaded(script2_sxg_url); |
| |
| const auto& cached_exchanges = GetCachedExchanges(); |
| EXPECT_EQ(3u, cached_exchanges.size()); |
| |
| // The value of "header-integrity" in "allowed-alt-sxg" link header of the |
| // inner response doesn't match the actual header integrity of script2_js.sxg. |
| // So the all script requests must go to the server. |
| NavigateToURLAndWaitTitle(target_sxg_url, "from server"); |
| |
| EXPECT_EQ(1, script1_sxg_fetch_count); |
| EXPECT_EQ(1, script1_fetch_count); |
| EXPECT_EQ(1, script2_sxg_fetch_count); |
| EXPECT_EQ(1, script2_fetch_count); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| SignedExchangeSubresourcePrefetchBrowserTest, |
| SignedExchangeSubresourcePrefetchBrowserTest, |
| testing::Values(SignedExchangeSubresourcePrefetchBrowserTestParam(false), |
| SignedExchangeSubresourcePrefetchBrowserTestParam(true))); |
| } // namespace content |