| // 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/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/no_destructor.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task_runner.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/scoped_mock_clock_override.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time_override.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/navigation_url_loader_impl.h" |
| #include "content/browser/loader/prefetch_browsertest_base.h" |
| #include "content/browser/web_package/mock_signed_exchange_handler.h" |
| #include "content/browser/web_package/prefetched_signed_exchange_cache.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.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/common/content_paths.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 "content/shell/common/shell_switches.h" |
| #include "net/base/features.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/http/http_cache.h" |
| #include "services/network/cross_origin_read_blocking.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; |
| } |
| |
| PrefetchedSignedExchangeCache::EntryMap GetCachedExchanges(Shell* shell) { |
| RenderViewHost* rvh = shell->web_contents()->GetRenderViewHost(); |
| RenderFrameHostImpl* rfh = |
| static_cast<RenderFrameHostImpl*>(rvh->GetMainFrame()); |
| scoped_refptr<PrefetchedSignedExchangeCache> cache = |
| rfh->EnsurePrefetchedSignedExchangeCache(); |
| PrefetchedSignedExchangeCache::EntryMap results; |
| if (NavigationURLLoaderImpl::IsNavigationLoaderOnUIEnabled()) { |
| for (const auto& exchanges_it : cache->GetExchanges()) { |
| results[exchanges_it.first] = exchanges_it.second->Clone(); |
| } |
| } else { |
| base::RunLoop run_loop; |
| base::PostTaskWithTraitsAndReply( |
| FROM_HERE, {BrowserThread::IO}, |
| base::BindOnce( |
| [](scoped_refptr<PrefetchedSignedExchangeCache> cache, |
| PrefetchedSignedExchangeCache::EntryMap* results) { |
| for (const auto& exchanges_it : cache->GetExchanges()) { |
| (*results)[exchanges_it.first] = exchanges_it.second->Clone(); |
| } |
| }, |
| cache, &results), |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| return results; |
| } |
| |
| PrefetchBrowserTestBase::ResponseEntry CreateSignedExchangeResponseEntry( |
| const std::string& content, |
| const std::vector<std::pair<std::string, std::string>>& additional_headers = |
| {}) { |
| std::vector<std::pair<std::string, std::string>> headers = { |
| {"x-content-type-options", "nosniff"}}; |
| |
| for (const auto& additional_header : additional_headers) { |
| headers.emplace_back( |
| std::make_pair(additional_header.first, additional_header.second)); |
| } |
| |
| // We mock the SignedExchangeHandler, so just return the content as |
| // "application/signed-exchange;v=b3". |
| return PrefetchBrowserTestBase::ResponseEntry( |
| content, "application/signed-exchange;v=b3", headers); |
| } |
| |
| std::string CreateAlternateLinkHeader(const GURL& sxg_url, |
| const GURL& inner_url) { |
| return base::StringPrintf( |
| "<%s>;" |
| "rel=\"alternate\";" |
| "type=\"application/signed-exchange;v=b3\";" |
| "anchor=\"%s\"", |
| sxg_url.spec().c_str(), inner_url.spec().c_str()); |
| } |
| |
| std::string CreateAllowedAltSxgLinkHeader( |
| const GURL& inner_url, |
| const net::SHA256HashValue& header_integrity) { |
| return base::StringPrintf( |
| "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\"", |
| inner_url.spec().c_str(), |
| GetHeaderIntegrityString(header_integrity).c_str()); |
| } |
| |
| std::string CreatePreloadLinkHeader(const GURL& url, const char* as) { |
| return base::StringPrintf("<%s>;rel=\"preload\";as=\"%s\"", |
| url.spec().c_str(), as); |
| } |
| |
| // This class supplies a mock time which flies faster than the actual time |
| // using ScopedTimeClockOverrides. This is used to check the cache expiration |
| // logic. |
| class MockClock { |
| public: |
| // Returns a MockClock which will not be deleted. If the test use this class, |
| // this method should be called once while single-threaded to initialize |
| // ScopedTimeClockOverrides to avoid threading issues. |
| static MockClock& Get() { |
| static base::NoDestructor<MockClock> mock_clock; |
| return *mock_clock; |
| } |
| |
| void Advance(base::TimeDelta delta) { |
| DCHECK_GE(delta, base::TimeDelta()); |
| base::AutoLock lock(lock_); |
| offset_ += delta; |
| } |
| |
| private: |
| friend base::NoDestructor<MockClock>; |
| |
| static base::Time MockedNow() { |
| return base::subtle::TimeNowIgnoringOverride() + mock_clock_->GetOffset(); |
| } |
| |
| MockClock() { |
| mock_clock_ = this; |
| time_override_ = std::make_unique<base::subtle::ScopedTimeClockOverrides>( |
| &MockClock::MockedNow, nullptr, nullptr); |
| } |
| |
| ~MockClock() {} |
| |
| base::TimeDelta GetOffset() { |
| base::AutoLock lock(lock_); |
| return offset_; |
| } |
| |
| static MockClock* mock_clock_; |
| |
| std::unique_ptr<base::subtle::ScopedTimeClockOverrides> time_override_; |
| base::Lock lock_; |
| base::TimeDelta offset_ GUARDED_BY(lock_); |
| |
| DISALLOW_COPY_AND_ASSIGN(MockClock); |
| }; |
| |
| MockClock* MockClock::mock_clock_ = nullptr; |
| |
| class NavigationHandleSXGAttributeObserver : public WebContentsObserver { |
| public: |
| explicit NavigationHandleSXGAttributeObserver(WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| |
| // WebContentsObserver implementation. |
| void ReadyToCommitNavigation(NavigationHandle* navigation_handle) override { |
| had_prefetched_alt_sxg_ = |
| navigation_handle->HasPrefetchedAlternativeSubresourceSignedExchange(); |
| } |
| |
| const base::Optional<bool>& had_prefetched_alt_sxg() const { |
| return had_prefetched_alt_sxg_; |
| } |
| |
| private: |
| base::Optional<bool> had_prefetched_alt_sxg_; |
| |
| DISALLOW_COPY_AND_ASSIGN(NavigationHandleSXGAttributeObserver); |
| }; |
| |
| } // namespace |
| |
| class SignedExchangePrefetchBrowserTest |
| : public testing::WithParamInterface< |
| std::tuple<bool /* network_service_enabled */, |
| bool /* sxg_prefetch_cache_for_navigations_enabled */, |
| bool /* sxg_subresource_prefetch_enabled */>>, |
| public PrefetchBrowserTestBase { |
| public: |
| SignedExchangePrefetchBrowserTest() { |
| // Call MockClock::Get() here to initialize ScopedMockClockOverride which |
| // should be created while single-threaded. |
| MockClock::Get(); |
| } |
| ~SignedExchangePrefetchBrowserTest() = default; |
| |
| void SetUp() override { |
| bool network_service_enabled; |
| bool sxg_prefetch_cache_for_navigations_enabled; |
| bool sxg_subresource_prefetch_enabled; |
| std::tie(network_service_enabled, |
| sxg_prefetch_cache_for_navigations_enabled, |
| sxg_subresource_prefetch_enabled) = GetParam(); |
| |
| std::vector<base::Feature> enable_features; |
| std::vector<base::Feature> disabled_features; |
| enable_features.push_back(features::kSignedHTTPExchange); |
| if (network_service_enabled) { |
| enable_features.push_back(network::features::kNetworkService); |
| // Need to run the network service in process for testing cache expirity |
| // (PrefetchMainResourceSXG_ExceedPrefetchReuseMins) using MockClock. |
| enable_features.push_back(features::kNetworkServiceInProcess); |
| } else { |
| disabled_features.push_back(network::features::kNetworkService); |
| } |
| if (sxg_prefetch_cache_for_navigations_enabled) { |
| enable_features.push_back( |
| features::kSignedExchangePrefetchCacheForNavigations); |
| } else { |
| disabled_features.push_back( |
| features::kSignedExchangePrefetchCacheForNavigations); |
| } |
| if (sxg_subresource_prefetch_enabled) { |
| enable_features.push_back(features::kSignedExchangeSubresourcePrefetch); |
| } else { |
| disabled_features.push_back(features::kSignedExchangeSubresourcePrefetch); |
| } |
| feature_list_.InitWithFeatures(enable_features, disabled_features); |
| PrefetchBrowserTestBase::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| PrefetchBrowserTestBase::SetUpOnMainThread(); |
| } |
| |
| 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; |
| |
| static bool IsSignedExchangePrefetchCacheEnabled() { |
| return base::FeatureList::IsEnabled( |
| features::kSignedExchangePrefetchCacheForNavigations) || |
| base::FeatureList::IsEnabled( |
| features::kSignedExchangeSubresourcePrefetch); |
| } |
| |
| void SetBlobLimits() { |
| scoped_refptr<ChromeBlobStorageContext> blob_context = |
| ChromeBlobStorageContext::GetFor( |
| shell()->web_contents()->GetBrowserContext()); |
| base::PostTaskWithTraits( |
| FROM_HERE, {BrowserThread::IO}, |
| base::BindOnce(&SignedExchangePrefetchBrowserTest::SetBlobLimitsOnIO, |
| blob_context)); |
| } |
| |
| void RunPrefetchMainResourceSXGTest(const std::string& prefetch_page_hostname, |
| const std::string& prefetch_page_path, |
| const std::string& sxg_hostname, |
| const std::string& sxg_path, |
| const std::string& inner_url_hostname, |
| const std::string& inner_url_path) { |
| const net::SHA256HashValue header_integrity = {{0x01}}; |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| auto sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), sxg_path); |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| prefetch_page_hostname, prefetch_page_path, sxg_hostname, sxg_path, |
| inner_url_hostname, inner_url_path, header_integrity, content, |
| {} /* sxg_outer_headers */); |
| |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| |
| const GURL sxg_url = embedded_test_server()->GetURL(sxg_hostname, sxg_path); |
| const GURL inner_url = |
| embedded_test_server()->GetURL(inner_url_hostname, inner_url_path); |
| |
| if (!IsSignedExchangePrefetchCacheEnabled()) { |
| EXPECT_TRUE(GetCachedExchanges(shell()).empty()); |
| |
| // Shutdown the server. |
| EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); |
| |
| // Need to setup MockSignedExchangeHandlerFactory because the SXG is |
| // loaded from HTTPCache. |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, inner_url, |
| "text/html", {}, header_integrity, |
| base::Time() /* signature_expire_time */)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| base::HistogramTester histograms; |
| // 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 HTTPCache. |
| NavigateToURLAndWaitTitle(sxg_url, "Prefetch Target (SXG)"); |
| |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| histograms.ExpectTotalCount("PrefetchedSignedExchangeCache.Count", 0); |
| histograms.ExpectTotalCount("PrefetchedSignedExchangeCache.BodySize", 0); |
| histograms.ExpectTotalCount("PrefetchedSignedExchangeCache.BodySizeTotal", |
| 0); |
| histograms.ExpectTotalCount( |
| "PrefetchedSignedExchangeCache.HeadersSizeTotal", 0); |
| return; |
| } |
| |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| EXPECT_EQ(1u, cached_exchanges.size()); |
| const auto it = cached_exchanges.find(sxg_url); |
| ASSERT_TRUE(it != cached_exchanges.end()); |
| const std::unique_ptr<const PrefetchedSignedExchangeCache::Entry>& |
| exchange = it->second; |
| EXPECT_EQ(sxg_url, exchange->outer_url()); |
| EXPECT_EQ(inner_url, exchange->inner_url()); |
| EXPECT_EQ(header_integrity, *exchange->header_integrity()); |
| const int64_t headers_size_total = |
| exchange->outer_response()->headers->raw_headers().size() + |
| exchange->inner_response()->headers->raw_headers().size(); |
| |
| // Shutdown the server. |
| EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); |
| |
| base::HistogramTester histograms; |
| // 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(sxg_url, "Prefetch Target (SXG)"); |
| |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.Count", 1, 1); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.BodySize", |
| content.size(), 1); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.BodySizeTotal", |
| content.size(), 1); |
| histograms.ExpectBucketCount( |
| "PrefetchedSignedExchangeCache.HeadersSizeTotal", headers_size_total, |
| 1); |
| } |
| |
| void LoadPrefetchMainResourceSXGTestPage( |
| const std::string& prefetch_page_hostname, |
| const std::string& prefetch_page_path, |
| const std::string& sxg_hostname, |
| const std::string& sxg_path, |
| const std::string& inner_url_hostname, |
| const std::string& inner_url_path, |
| const net::SHA256HashValue& header_integrity, |
| const std::string& content, |
| const std::vector<std::pair<std::string, std::string>>& sxg_outer_headers, |
| const std::vector<std::string>& sxg_inner_headers = {}, |
| const base::Time& signature_expire_time = base::Time()) { |
| auto sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), sxg_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL prefetch_page_url = embedded_test_server()->GetURL( |
| prefetch_page_hostname, prefetch_page_path); |
| const GURL sxg_url = embedded_test_server()->GetURL(sxg_hostname, sxg_path); |
| const GURL inner_url = |
| embedded_test_server()->GetURL(inner_url_hostname, inner_url_path); |
| |
| RegisterResponse(prefetch_page_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", |
| sxg_url.spec().c_str()))); |
| RegisterResponse(sxg_path, CreateSignedExchangeResponseEntry( |
| content, sxg_outer_headers)); |
| |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, inner_url, |
| "text/html", sxg_inner_headers, header_integrity, |
| signature_expire_time)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| EXPECT_EQ(0, sxg_request_counter->GetRequestCount()); |
| |
| NavigateToURL(shell(), prefetch_page_url); |
| |
| WaitUntilLoaded(sxg_url); |
| |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| } |
| |
| 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(SignedExchangePrefetchBrowserTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_SameOrigin) { |
| RunPrefetchMainResourceSXGTest("example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* sxg_hostname */, |
| "/target.sxg" /* sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_CrossOrigin) { |
| RunPrefetchMainResourceSXGTest( |
| "aggregator.example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "distoributor.example.com" /* sxg_hostname */, |
| "/target.sxg" /* sxg_path */, |
| "publisher.example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_SameURL) { |
| RunPrefetchMainResourceSXGTest("example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* sxg_hostname */, |
| "/target.html" /* sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_BlobStorageLimit) { |
| SetBlobLimits(); |
| |
| std::string content = "<head><title>Prefetch Target (SXG)</title></head>"; |
| // Make the content larger than the disk space. |
| content.resize(kTestBlobStorageMaxDiskSpace + 1, ' '); |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* sxg_hostname */, "/target.sxg" /* sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {} /* sxg_outer_headers */, {} /* sxg_inner_headers */); |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| // 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( |
| SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_BlobStorageLimitWithContentLength) { |
| // BlobBuilderFromStream's behavior is different when "content-length" |
| // header is set. So we have BlobStorageLimit test with "content-length". |
| SetBlobLimits(); |
| |
| std::string content = "<head><title>Prefetch Target (SXG)</title></head>"; |
| // Make the content larger than the disk space. |
| content.resize(kTestBlobStorageMaxDiskSpace + 1, ' '); |
| LoadPrefetchMainResourceSXGTestPage( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* sxg_hostname */, "/target.sxg" /* sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {} /* sxg_outer_headers */, |
| {base::StringPrintf("content-length: %" PRIuS, |
| content.size())} /* sxg_inner_headers */); |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| // 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(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_CacheControlNoStore) { |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* sxg_hostname */, "/target.sxg" /* sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {{"cache-control", "no-store"}} /* sxg_outer_headers */, |
| {} /* sxg_inner_headers */); |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| // The signed exchange which response header has "cache-control: no-store" |
| // header should not be stored to the cache. |
| EXPECT_EQ(0u, cached_exchanges.size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_VaryAsteriskHeader) { |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* sxg_hostname */, "/target.sxg" /* sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {{"vary", "*"}} /* sxg_outer_headers */, {} /* sxg_inner_headers */); |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| // The signed exchange which response header has "vary: *" header should not |
| // be stored to the cache. |
| EXPECT_EQ(0u, cached_exchanges.size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_VaryAcceptEncodingHeader) { |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* sxg_hostname */, "/target.sxg" /* sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* inner_url_path */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {{"vary", "accept-encoding"}} /* sxg_outer_headers */, |
| {} /* sxg_inner_headers */); |
| // The signed exchange which response header has "vary: accept-encoding" |
| // header should be stored to the cache. |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| EXPECT_EQ(IsSignedExchangePrefetchCacheEnabled() ? 1u : 0u, |
| cached_exchanges.size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_ExceedPrefetchReuseMins) { |
| const char* hostname = "example.com"; |
| const char* sxg_path = "/target.sxg"; |
| const char* inner_url_path = "/target.html"; |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| auto sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), sxg_path); |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| hostname, "/prefetch.html" /* prefetch_page_path */, hostname, sxg_path, |
| hostname, inner_url_path, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {} /* sxg_outer_headers */, {} /* sxg_inner_headers */); |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| |
| const GURL sxg_url = embedded_test_server()->GetURL(hostname, sxg_path); |
| const GURL inner_url = |
| embedded_test_server()->GetURL(hostname, inner_url_path); |
| |
| EXPECT_EQ(IsSignedExchangePrefetchCacheEnabled() ? 1u : 0u, |
| GetCachedExchanges(shell()).size()); |
| |
| MockClock::Get().Advance(base::TimeDelta::FromSeconds( |
| net::HttpCache::kPrefetchReuseMins * 60 + 1)); |
| |
| // Need to setup MockSignedExchangeHandlerFactory because the SXG is loaded |
| // from the server again. |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, inner_url, |
| "text/html", {} /* sxg_inner_headers */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURLAndWaitTitle(sxg_url, "Prefetch Target (SXG)"); |
| |
| // SXG must be fetched again. |
| EXPECT_EQ(2, sxg_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_CacheControlPublic) { |
| const char* hostname = "example.com"; |
| const char* sxg_path = "/target.sxg"; |
| const char* inner_url_path = "/target.html"; |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| auto sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), sxg_path); |
| const auto header_integrity = net::SHA256HashValue({{0x01}}); |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| hostname, "/prefetch.html" /* prefetch_page_path */, hostname, sxg_path, |
| hostname, inner_url_path, header_integrity, content, |
| {{"cache-control", |
| base::StringPrintf("public, max-age=%d", |
| net::HttpCache::kPrefetchReuseMins * 3 * |
| 60)}} /* sxg_outer_headers */, |
| {} /* sxg_inner_headers */); |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| |
| const GURL sxg_url = embedded_test_server()->GetURL(hostname, sxg_path); |
| const GURL inner_url = |
| embedded_test_server()->GetURL(hostname, inner_url_path); |
| |
| EXPECT_EQ(IsSignedExchangePrefetchCacheEnabled() ? 1u : 0u, |
| GetCachedExchanges(shell()).size()); |
| |
| MockClock::Get().Advance(base::TimeDelta::FromSeconds( |
| net::HttpCache::kPrefetchReuseMins * 2 * 60)); |
| |
| if (IsSignedExchangePrefetchCacheEnabled()) { |
| NavigateToURLAndWaitTitle(sxg_url, "Prefetch Target (SXG)"); |
| } else { |
| // Need to setup MockSignedExchangeHandlerFactory because the SXG is loaded |
| // from HTTPCache. |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, inner_url, |
| "text/html", {}, header_integrity, |
| base::Time() /* signature_expire_time */)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| NavigateToURLAndWaitTitle(sxg_url, "Prefetch Target (SXG)"); |
| } |
| |
| // SXG must Not be fetched again. |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_CacheControlPublicExpire) { |
| const char* hostname = "example.com"; |
| const char* sxg_path = "/target.sxg"; |
| const char* inner_url_path = "/target.html"; |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| auto sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), sxg_path); |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| hostname, "/prefetch.html" /* prefetch_page_path */, hostname, sxg_path, |
| hostname, inner_url_path, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {{"cache-control", |
| base::StringPrintf("public, max-age=%d", |
| net::HttpCache::kPrefetchReuseMins * 3 * |
| 60)}} /* sxg_outer_headers */, |
| {} /* sxg_inner_headers */); |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| |
| const GURL sxg_url = embedded_test_server()->GetURL(hostname, sxg_path); |
| const GURL inner_url = |
| embedded_test_server()->GetURL(hostname, inner_url_path); |
| |
| EXPECT_EQ(IsSignedExchangePrefetchCacheEnabled() ? 1u : 0u, |
| GetCachedExchanges(shell()).size()); |
| |
| MockClock::Get().Advance(base::TimeDelta::FromSeconds( |
| net::HttpCache::kPrefetchReuseMins * 3 * 60 + 1)); |
| |
| // Need to setup MockSignedExchangeHandlerFactory because the SXG is loaded |
| // from the server again. |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, inner_url, |
| "text/html", {} /* sxg_inner_headers */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURLAndWaitTitle(sxg_url, "Prefetch Target (SXG)"); |
| |
| // SXG must be fetched again, because the cache has been expired. |
| EXPECT_EQ(2, sxg_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchMainResourceSXG_SignatureExpire) { |
| const char* hostname = "example.com"; |
| const char* sxg_path = "/target.sxg"; |
| const char* inner_url_path = "/target.html"; |
| const std::string content = |
| "<head><title>Prefetch Target (SXG)</title></head>"; |
| auto sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), sxg_path); |
| |
| LoadPrefetchMainResourceSXGTestPage( |
| hostname, "/prefetch.html" /* prefetch_page_path */, hostname, sxg_path, |
| hostname, inner_url_path, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */, content, |
| {} /* sxg_outer_headers */, {} /* sxg_inner_headers */, |
| base::Time::Now() + |
| base::TimeDelta::FromMinutes(net::HttpCache::kPrefetchReuseMins * 2)); |
| EXPECT_EQ(1, sxg_request_counter->GetRequestCount()); |
| |
| const GURL sxg_url = embedded_test_server()->GetURL(hostname, sxg_path); |
| const GURL inner_url = |
| embedded_test_server()->GetURL(hostname, inner_url_path); |
| |
| EXPECT_EQ(IsSignedExchangePrefetchCacheEnabled() ? 1u : 0u, |
| GetCachedExchanges(shell()).size()); |
| |
| MockClock::Get().Advance( |
| base::TimeDelta::FromMinutes(net::HttpCache::kPrefetchReuseMins * 3)); |
| |
| // Setup MockSignedExchangeHandlerFactory which triggers signature |
| // verificvation error fallback. |
| MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams( |
| sxg_url, SignedExchangeLoadResult::kSignatureVerificationError, |
| net::ERR_INVALID_SIGNED_EXCHANGE, inner_url, "", |
| {} /* sxg_inner_headers */, |
| net::SHA256HashValue({{0x01}}) /* header_integrity */)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| RegisterResponse(inner_url_path, ResponseEntry("<title>from server</title>")); |
| |
| NavigateToURLAndWaitTitle(sxg_url, "from server"); |
| |
| // SXG must be fetched again, because: |
| // - The entry in PrefetchedSignedExchangeCache is expired (signature expire |
| // time). |
| // - The entry in HTTPCache is expired (more than kPrefetchReuseMins minutes |
| // passed). Note: The prefetched resource can skip cache revalidation for |
| // kPrefetchReuseMins minutes. |
| EXPECT_EQ(2, sxg_request_counter->GetRequestCount()); |
| } |
| |
| // This test is almost same as SignedExchangeSubresourcePrefetchBrowserTest's |
| // MainResourceSXGAndScriptSXG_SameOrigin. The only difference is that this test |
| // is executed without SignedExchangeSubresourcePrefetch but with |
| // SignedExchangePrefetchCacheForNavigations when |
| // |sxg_subresource_prefetch_enabled| is false to check the behavior of |
| // SignedExchangePrefetchCacheForNavigations feature. |
| IN_PROC_BROWSER_TEST_P(SignedExchangePrefetchBrowserTest, |
| PrefetchAlternativeSubresourceSXG) { |
| const char* prefetch_page_path = "/prefetch.html"; |
| const char* page_sxg_path = "/target.sxg"; |
| const char* page_inner_url_path = "/target.html"; |
| const char* script_sxg_path = "/script_js.sxg"; |
| const char* script_inner_url_path = "/script.js"; |
| |
| auto page_sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), page_sxg_path); |
| auto script_sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script_sxg_path); |
| auto script_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script_inner_url_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL prefetch_page_url = |
| embedded_test_server()->GetURL(prefetch_page_path); |
| const GURL sxg_page_url = embedded_test_server()->GetURL(page_sxg_path); |
| const GURL inner_url_page_url = |
| embedded_test_server()->GetURL(page_inner_url_path); |
| const GURL sxg_script_url = embedded_test_server()->GetURL(script_sxg_path); |
| const GURL inner_url_script_url = |
| embedded_test_server()->GetURL(script_inner_url_path); |
| |
| const net::SHA256HashValue page_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script_header_integrity = {{0x02}}; |
| |
| const std::string outer_link_header = |
| CreateAlternateLinkHeader(sxg_script_url, inner_url_script_url); |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(inner_url_script_url, |
| script_header_integrity), |
| CreatePreloadLinkHeader(inner_url_script_url, "script")}, |
| ","); |
| const std::string page_sxg_content = |
| "<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\"></script></head>"; |
| const std::string script_sxg_content = "document.title=\"done\";"; |
| RegisterResponse(prefetch_page_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", |
| sxg_page_url.spec().c_str()))); |
| RegisterResponse( |
| script_inner_url_path, |
| ResponseEntry("document.title=\"from server\";", "text/javascript", |
| {{"cache-control", "public, max-age=600"}})); |
| RegisterResponse(page_sxg_path, |
| CreateSignedExchangeResponseEntry( |
| page_sxg_content, {{"link", outer_link_header}})); |
| RegisterResponse(script_sxg_path, |
| CreateSignedExchangeResponseEntry(script_sxg_content)); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| sxg_page_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| inner_url_page_url, "text/html", {inner_link_headers}, |
| page_header_integrity), |
| MockSignedExchangeHandlerParams( |
| sxg_script_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| inner_url_script_url, "text/javascript", {}, |
| script_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| EXPECT_EQ(0, GetPrefetchURLLoaderCallCount()); |
| NavigateToURL(shell(), prefetch_page_url); |
| |
| WaitUntilLoaded(sxg_page_url); |
| if (base::FeatureList::IsEnabled( |
| features::kSignedExchangeSubresourcePrefetch)) { |
| WaitUntilLoaded(sxg_script_url); |
| } else { |
| WaitUntilLoaded(inner_url_script_url); |
| } |
| |
| EXPECT_EQ(1, page_sxg_request_counter->GetRequestCount()); |
| if (base::FeatureList::IsEnabled( |
| features::kSignedExchangeSubresourcePrefetch)) { |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| EXPECT_EQ(2, GetPrefetchURLLoaderCallCount()); |
| } else { |
| EXPECT_EQ(0, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, GetPrefetchURLLoaderCallCount()); |
| } |
| |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| int64_t expected_body_size_total = 0u; |
| int64_t expected_headers_size_total = 0u; |
| |
| if (IsSignedExchangePrefetchCacheEnabled()) { |
| if (base::FeatureList::IsEnabled( |
| features::kSignedExchangeSubresourcePrefetch)) { |
| EXPECT_EQ(2u, cached_exchanges.size()); |
| |
| const auto script_it = cached_exchanges.find(sxg_script_url); |
| ASSERT_TRUE(script_it != cached_exchanges.end()); |
| const std::unique_ptr<const PrefetchedSignedExchangeCache::Entry>& |
| script_exchange = script_it->second; |
| EXPECT_EQ(sxg_script_url, script_exchange->outer_url()); |
| EXPECT_EQ(inner_url_script_url, script_exchange->inner_url()); |
| EXPECT_EQ(script_header_integrity, *script_exchange->header_integrity()); |
| expected_body_size_total += script_sxg_content.size(); |
| expected_headers_size_total += |
| script_exchange->outer_response()->headers->raw_headers().size() + |
| script_exchange->inner_response()->headers->raw_headers().size(); |
| } else { |
| DCHECK(base::FeatureList::IsEnabled( |
| features::kSignedExchangePrefetchCacheForNavigations)); |
| EXPECT_EQ(1u, cached_exchanges.size()); |
| } |
| const auto page_it = cached_exchanges.find(sxg_page_url); |
| ASSERT_TRUE(page_it != cached_exchanges.end()); |
| const std::unique_ptr<const PrefetchedSignedExchangeCache::Entry>& |
| page_exchange = page_it->second; |
| EXPECT_EQ(sxg_page_url, page_exchange->outer_url()); |
| EXPECT_EQ(inner_url_page_url, page_exchange->inner_url()); |
| EXPECT_EQ(page_header_integrity, *page_exchange->header_integrity()); |
| expected_body_size_total += page_sxg_content.size(); |
| expected_headers_size_total += |
| page_exchange->outer_response()->headers->raw_headers().size() + |
| page_exchange->inner_response()->headers->raw_headers().size(); |
| } else { |
| EXPECT_EQ(0u, cached_exchanges.size()); |
| } |
| |
| base::HistogramTester histograms; |
| |
| if (base::FeatureList::IsEnabled( |
| features::kSignedExchangeSubresourcePrefetch)) { |
| // 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(sxg_page_url, "done"); |
| } else { |
| // 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 when |
| // SignedExchangePrefetchCacheForNavigations is enabled, otherwise from |
| // HTTPCache. But the script is loaded from the server. |
| NavigateToURLAndWaitTitle(sxg_page_url, "from server"); |
| } |
| |
| EXPECT_EQ(1, page_sxg_request_counter->GetRequestCount()); |
| |
| if (IsSignedExchangePrefetchCacheEnabled()) { |
| if (base::FeatureList::IsEnabled( |
| features::kSignedExchangeSubresourcePrefetch)) { |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.Count", 2, 1); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.BodySize", |
| page_sxg_content.size(), 1); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.BodySize", |
| script_sxg_content.size(), 1); |
| } else { |
| DCHECK(base::FeatureList::IsEnabled( |
| features::kSignedExchangePrefetchCacheForNavigations)); |
| EXPECT_EQ(0, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script_request_counter->GetRequestCount()); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.Count", 1, 1); |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.BodySize", |
| page_sxg_content.size(), 1); |
| } |
| histograms.ExpectBucketCount("PrefetchedSignedExchangeCache.BodySizeTotal", |
| expected_body_size_total, 1); |
| histograms.ExpectBucketCount( |
| "PrefetchedSignedExchangeCache.HeadersSizeTotal", |
| expected_headers_size_total, 1); |
| } else { |
| histograms.ExpectTotalCount("PrefetchedSignedExchangeCache.Count", 0); |
| histograms.ExpectTotalCount("PrefetchedSignedExchangeCache.BodySize", 0); |
| histograms.ExpectTotalCount("PrefetchedSignedExchangeCache.BodySizeTotal", |
| 0); |
| histograms.ExpectTotalCount( |
| "PrefetchedSignedExchangeCache.HeadersSizeTotal", 0); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(SignedExchangePrefetchBrowserTest, |
| SignedExchangePrefetchBrowserTest, |
| ::testing::Combine(::testing::Bool(), |
| ::testing::Bool(), |
| ::testing::Bool())); |
| |
| class SignedExchangeSubresourcePrefetchBrowserTest |
| : public testing::WithParamInterface<bool /* network_service_enabled */>, |
| public PrefetchBrowserTestBase { |
| public: |
| SignedExchangeSubresourcePrefetchBrowserTest() { |
| // Call MockClock::Get() here to initialize ScopedMockClockOverride which |
| // should be created while single-threaded. |
| MockClock::Get(); |
| } |
| ~SignedExchangeSubresourcePrefetchBrowserTest() = default; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| PrefetchBrowserTestBase::SetUpCommandLine(command_line); |
| // Needed to call internals.scheduleBlinkGC(). |
| command_line->AppendSwitch(switches::kExposeInternalsForTesting); |
| } |
| |
| void SetUp() override { |
| const bool network_service_enabled = GetParam(); |
| |
| 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 (network_service_enabled) { |
| enable_features.push_back(network::features::kNetworkService); |
| // Need to run the network service in process for testing cache expirity |
| // (PrefetchMainResourceSXG_ExceedPrefetchReuseMins) using MockClock. |
| enable_features.push_back(features::kNetworkServiceInProcess); |
| } else { |
| disabled_features.push_back(network::features::kNetworkService); |
| } |
| feature_list_.InitWithFeatures(enable_features, disabled_features); |
| PrefetchBrowserTestBase::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| PrefetchBrowserTestBase::SetUpOnMainThread(); |
| } |
| |
| protected: |
| // This test prefetches a main SXG which has appropriate subresrouce signed |
| // exchange related headers, and navigate to the main SXG. If the subresource |
| // signed exchange was prefetched and used, |expected_title| should be "done", |
| // otherwise |expected_title| should be "from server" which is set by the |
| // subresource script. |
| // When |expected_script_fetch_count| is -1 this test doesn't check the script |
| // fetch count. |
| void RunPrefetchMainResourceSXGAndScriptSXGTest( |
| const std::string& prefetch_page_hostname, |
| const std::string& prefetch_page_path, |
| const std::string& page_sxg_hostname, |
| const std::string& page_sxg_path, |
| const std::string& script_sxg_hostname, |
| const std::string& script_sxg_path, |
| const std::string& inner_url_hostname, |
| const std::string& page_inner_url_path, |
| const std::string& script_inner_url_path, |
| const std::vector<std::pair<std::string, std::string>>& |
| script_sxg_outer_headers, |
| int64_t elapsed_time_after_prefetch, |
| const std::string& expected_title, |
| bool script_sxg_should_be_stored, |
| int expected_script_fetch_count) { |
| // When |expected_script_fetch_count| is -1, we don't check the script fetch |
| // count. |
| const bool check_script_fetch_count = expected_script_fetch_count != -1; |
| |
| auto script_sxg_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script_sxg_path); |
| auto script_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script_inner_url_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL prefetch_page_url = embedded_test_server()->GetURL( |
| prefetch_page_hostname, prefetch_page_path); |
| const GURL sxg_page_url = |
| embedded_test_server()->GetURL(page_sxg_hostname, page_sxg_path); |
| const GURL inner_url_page_url = |
| embedded_test_server()->GetURL(inner_url_hostname, page_inner_url_path); |
| const GURL sxg_script_url = |
| embedded_test_server()->GetURL(script_sxg_hostname, script_sxg_path); |
| const GURL inner_url_script_url = embedded_test_server()->GetURL( |
| inner_url_hostname, script_inner_url_path); |
| |
| const net::SHA256HashValue page_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script_header_integrity = {{0x02}}; |
| |
| const std::string outer_link_header = |
| CreateAlternateLinkHeader(sxg_script_url, inner_url_script_url); |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(inner_url_script_url, |
| script_header_integrity), |
| CreatePreloadLinkHeader(inner_url_script_url, "script")}, |
| ","); |
| |
| RegisterResponse(prefetch_page_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", |
| sxg_page_url.spec().c_str()))); |
| RegisterResponse( |
| script_inner_url_path, |
| ResponseEntry("document.title=\"from server\";", "text/javascript", |
| {{"cache-control", "public, max-age=600"}})); |
| RegisterResponse(page_sxg_path, |
| CreateSignedExchangeResponseEntry( |
| "<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\"></script></head>", |
| {{"link", outer_link_header}, |
| {"cache-control", |
| base::StringPrintf( |
| "public, max-age=%d", |
| net::HttpCache::kPrefetchReuseMins * 3 * 60)}})); |
| RegisterResponse(script_sxg_path, |
| CreateSignedExchangeResponseEntry( |
| "document.title=\"done\";", script_sxg_outer_headers)); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| sxg_page_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| inner_url_page_url, "text/html", {inner_link_headers}, |
| page_header_integrity), |
| MockSignedExchangeHandlerParams( |
| sxg_script_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| inner_url_script_url, "text/javascript", {}, |
| script_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| EXPECT_EQ(0, GetPrefetchURLLoaderCallCount()); |
| NavigateToURL(shell(), prefetch_page_url); |
| |
| WaitUntilLoaded(sxg_page_url); |
| WaitUntilLoaded(sxg_script_url); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| if (check_script_fetch_count) { |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| } |
| EXPECT_EQ(2, GetPrefetchURLLoaderCallCount()); |
| |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| EXPECT_EQ(script_sxg_should_be_stored ? 2u : 1u, cached_exchanges.size()); |
| |
| const auto target_it = cached_exchanges.find(sxg_page_url); |
| ASSERT_TRUE(target_it != cached_exchanges.end()); |
| EXPECT_EQ(sxg_page_url, target_it->second->outer_url()); |
| EXPECT_EQ(inner_url_page_url, target_it->second->inner_url()); |
| EXPECT_EQ(page_header_integrity, *target_it->second->header_integrity()); |
| |
| if (script_sxg_should_be_stored) { |
| const auto script_it = cached_exchanges.find(sxg_script_url); |
| ASSERT_TRUE(script_it != cached_exchanges.end()); |
| EXPECT_EQ(sxg_script_url, script_it->second->outer_url()); |
| EXPECT_EQ(inner_url_script_url, script_it->second->inner_url()); |
| EXPECT_EQ(script_header_integrity, |
| *script_it->second->header_integrity()); |
| } |
| |
| MockClock::Get().Advance( |
| base::TimeDelta::FromSeconds(elapsed_time_after_prefetch)); |
| |
| // 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. |
| // |
| // When |expected_title| == "done": |
| // The script is also loaded from PrefetchedSignedExchangeCache. |
| // When |expected_title| == "from server": |
| // The script is loaded from the server. |
| NavigateToURLAndWaitTitle(sxg_page_url, expected_title); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| if (check_script_fetch_count) { |
| EXPECT_EQ(expected_script_fetch_count, |
| script_request_counter->GetRequestCount()); |
| } |
| } |
| |
| // Test that CORB logic works well with prefetched signed exchange |
| // subresources. This test loads a main SXG which signed by |
| // "publisher.example.com", and loads a SXG subresource using a <script> tag. |
| // |
| // When |cross_origin| is set, the SXG subresource is signed by |
| // "crossorigin.example.com", otherwise sined by "publisher.example.com". |
| // |content| is the inner body content of the SXG subresource. |
| // |mime_type| is the MIME tyepe of the inner response of the SXG subresource. |
| // When |has_nosniff| is set, the inner response header of the SXG subresource |
| // has "x-content-type-options: nosniff" header. |
| // |expected_title| is the expected title after loading the SXG page. If the |
| // content load should not be blocked, this should be the title which is set |
| // by executing |content|, otherwise this should be "original title". |
| // |expected_action| is the expected CrossOriginReadBlocking::Action which is |
| // recorded in the histogram. |
| void RunCrossOriginReadBlockingTest( |
| bool cross_origin, |
| const std::string& content, |
| const std::string& mime_type, |
| bool has_nosniff, |
| const std::string& expected_title, |
| network::CrossOriginReadBlocking::Action expected_action) { |
| 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"; |
| |
| auto script_sxg_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script_sxg_path); |
| auto script_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = |
| embedded_test_server()->GetURL("publisher.example.com", target_path); |
| const GURL script_sxg_url = embedded_test_server()->GetURL(script_sxg_path); |
| const GURL script_url = embedded_test_server()->GetURL( |
| cross_origin ? "crossorigin.example.com" : "publisher.example.com", |
| script_path); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script_header_integrity = {{0x02}}; |
| |
| const std::string outer_link_header = |
| CreateAlternateLinkHeader(script_sxg_url, script_url); |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(script_url, script_header_integrity), |
| CreatePreloadLinkHeader(script_url, "script")}, |
| ","); |
| 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, |
| CreateSignedExchangeResponseEntry( |
| base::StringPrintf("<head><script src=\"%s\"></script>" |
| "<title>original title</title>" |
| "</head>", |
| script_url.spec().c_str()), |
| {{"link", outer_link_header}})); |
| RegisterResponse(script_sxg_path, |
| CreateSignedExchangeResponseEntry(content)); |
| std::vector<std::string> script_inner_response_headers; |
| if (has_nosniff) { |
| script_inner_response_headers.emplace_back( |
| "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, mime_type, script_inner_response_headers, |
| script_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script_sxg_url); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| |
| EXPECT_EQ(2u, GetCachedExchanges(shell()).size()); |
| |
| { |
| base::HistogramTester histograms; |
| NavigateToURLAndWaitTitle(target_sxg_url, expected_title); |
| histograms.ExpectBucketCount( |
| "SiteIsolation.XSD.Browser.Action", |
| network::CrossOriginReadBlocking::Action::kResponseStarted, 1); |
| histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action", |
| expected_action, 1); |
| } |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SignedExchangeSubresourcePrefetchBrowserTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_SameOrigin) { |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.sxg" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {} /* script_sxg_outer_headers */, 0 /* elapsed_time_after_prefetch */, |
| "done" /* expected_title */, true /* script_sxg_should_be_stored */, |
| 0 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_SameUrl) { |
| // This test checks the behavior of subresource signed exchange prefetching |
| // when the inner URL and the outer URL of signed exchanges are same. |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.html" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.js" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {} /* script_sxg_outer_headers */, 0 /* elapsed_time_after_prefetch */, |
| "done" /* expected_title */, true /* script_sxg_should_be_stored */, |
| // Note that we don't check the script fetch count in this test. |
| -1 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_CrossOrigin) { |
| // This test checks the behavior of subresource signed exchange prefetching |
| // when the inner URL's origin (= publisher) and the outer URL's origin |
| // (= distoributor) of signed exchanges is different from the prefetching page |
| // (= aggregator). |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "aggregator.example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "distoributor.example.com" /* page_sxg_hostname */, |
| "/target.sxg" /* page_sxg_path */, |
| "distoributor.example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "publisher.example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {} /* script_sxg_outer_headers */, 0 /* elapsed_time_after_prefetch */, |
| "done" /* expected_title */, true /* script_sxg_should_be_stored */, |
| 0 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_DifferentOriginScriptSXG) { |
| // When the subresource SXG was served from different origin from the main |
| // SXG, the subresource should be loaded from the server. |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "aggregator.example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "distoributor1.example.com" /* page_sxg_hostname */, |
| "/target.sxg" /* page_sxg_path */, |
| "distoributor2.example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "publisher.example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {} /* script_sxg_outer_headers */, 0 /* elapsed_time_after_prefetch */, |
| "from server" /* expected_title */, |
| true /* script_sxg_should_be_stored */, |
| 1 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_CacheControlNoStore) { |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.sxg" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {{"cache-control", "no-store"}} /* script_sxg_outer_headers */, |
| 0 /* elapsed_time_after_prefetch */, "from server" /* expected_title */, |
| false /* script_sxg_should_be_stored */, |
| 1 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_VaryAsteriskHeader) { |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.sxg" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {{"vary", "*"}} /* script_sxg_outer_headers */, |
| 0 /* elapsed_time_after_prefetch */, "from server" /* expected_title */, |
| false /* script_sxg_should_be_stored */, |
| 1 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_VaryAcceptEncodingHeader) { |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.sxg" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {{"vary", "accept-encoding"}} /* script_sxg_outer_headers */, |
| 0 /* elapsed_time_after_prefetch */, "done" /* expected_title */, |
| true /* script_sxg_should_be_stored */, |
| 0 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_ExceedPrefetchReuseMins) { |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.sxg" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {} /* script_sxg_outer_headers */, |
| net::HttpCache::kPrefetchReuseMins * 60 + 1 |
| /* elapsed_time_after_prefetch */, |
| "from server" /* expected_title */, |
| true /* script_sxg_should_be_stored */, |
| 1 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_CacheControlPublic) { |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.sxg" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {{"cache-control", |
| base::StringPrintf("public, max-age=%d", |
| net::HttpCache::kPrefetchReuseMins * 3 * |
| 60)}} /* script_sxg_outer_headers */, |
| net::HttpCache::kPrefetchReuseMins * 2 * 60 |
| /* elapsed_time_after_prefetch */, |
| "done" /* expected_title */, true /* script_sxg_should_be_stored */, |
| 0 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MainResourceSXGAndScriptSXG_CacheControlPublicExpire) { |
| RunPrefetchMainResourceSXGAndScriptSXGTest( |
| "example.com" /* prefetch_page_hostname */, |
| "/prefetch.html" /* prefetch_page_path */, |
| "example.com" /* page_sxg_hostname */, "/target.sxg" /* page_sxg_path */, |
| "example.com" /* script_sxg_hostname */, |
| "/script.sxg" /* script_sxg_path */, |
| "example.com" /* inner_url_hostname */, |
| "/target.html" /* page_inner_url_path */, |
| "/script.js" /* script_inner_url_path */, |
| {{"cache-control", |
| base::StringPrintf("public, max-age=%d", |
| net::HttpCache::kPrefetchReuseMins * 3 * |
| 60)}} /* script_sxg_outer_headers */, |
| net::HttpCache::kPrefetchReuseMins * 3 * 60 + 1 |
| /* elapsed_time_after_prefetch */, |
| "from server" /* expected_title */, |
| true /* script_sxg_should_be_stored */, |
| 1 /* expected_script_fetch_count */); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| ImageSrcsetAndSizes) { |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| const char* image1_sxg_path = "/image1_png.sxg"; |
| const char* image1_path = "/image1.png"; |
| const char* image2_sxg_path = "/image2_png.sxg"; |
| const char* image2_path = "/image2.png"; |
| |
| auto image1_sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), image1_sxg_path); |
| auto image2_sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), image2_sxg_path); |
| auto image1_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), image1_path); |
| auto image2_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), image2_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| const GURL image1_sxg_url = embedded_test_server()->GetURL(image1_sxg_path); |
| const GURL image1_url = embedded_test_server()->GetURL(image1_path); |
| const GURL image2_sxg_url = embedded_test_server()->GetURL(image2_sxg_path); |
| const GURL image2_url = embedded_test_server()->GetURL(image2_path); |
| |
| std::string image_contents; |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::FilePath path; |
| ASSERT_TRUE(base::PathService::Get(content::DIR_TEST_DATA, &path)); |
| path = path.AppendASCII("loader/empty16x16.png"); |
| ASSERT_TRUE(base::PathExists(path)); |
| ASSERT_TRUE(base::ReadFileToString(path, &image_contents)); |
| } |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| const net::SHA256HashValue image1_header_integrity = {{0x02}}; |
| const net::SHA256HashValue image2_header_integrity = {{0x03}}; |
| |
| const std::string outer_link_header = |
| base::JoinString({CreateAlternateLinkHeader(image1_sxg_url, image1_url), |
| CreateAlternateLinkHeader(image2_sxg_url, image2_url)}, |
| ","); |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(image1_url, image1_header_integrity), |
| CreateAllowedAltSxgLinkHeader(image2_url, image2_header_integrity), |
| base::StringPrintf("<%s>;rel=\"preload\";as=\"image\";" |
| // imagesrcset says the size of image1 is 320, and |
| // the size of image2 is 160. |
| "imagesrcset=\"%s 320w, %s 160w\";" |
| // imagesizes says the size of the image is 320. |
| // So image1 is selected. |
| "imagesizes=\"320px\"", |
| image2_url.spec().c_str(), |
| image1_url.spec().c_str(), |
| image2_url.spec().c_str())}, |
| ","); |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(image1_path, ResponseEntry(image_contents, "image/png")); |
| RegisterResponse(image2_path, ResponseEntry(image_contents, "image/png")); |
| RegisterResponse(target_sxg_path, |
| CreateSignedExchangeResponseEntry( |
| base::StringPrintf( |
| "<head>" |
| "<title>Prefetch Target (SXG)</title>" |
| "</head>" |
| "<img src=\"%s\" onload=\"document.title='done'\">", |
| image1_url.spec().c_str()), |
| {{"link", outer_link_header}})); |
| RegisterResponse(image1_sxg_path, |
| CreateSignedExchangeResponseEntry(image_contents)); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| target_url, "text/html", {inner_link_headers}, |
| target_header_integrity), |
| MockSignedExchangeHandlerParams( |
| image1_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| image1_url, "image/png", {}, image1_header_integrity), |
| MockSignedExchangeHandlerParams( |
| image2_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| image2_url, "image/png", {}, image2_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(image1_sxg_url); |
| |
| EXPECT_EQ(1, image1_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, image1_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, image2_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, image2_request_counter->GetRequestCount()); |
| |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| 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 image1_it = cached_exchanges.find(image1_sxg_url); |
| ASSERT_TRUE(image1_it != cached_exchanges.end()); |
| EXPECT_EQ(image1_sxg_url, image1_it->second->outer_url()); |
| EXPECT_EQ(image1_url, image1_it->second->inner_url()); |
| EXPECT_EQ(image1_header_integrity, *image1_it->second->header_integrity()); |
| |
| NavigateToURLAndWaitTitle(target_sxg_url, "done"); |
| |
| EXPECT_EQ(1, image1_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, image1_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, image2_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, image2_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MultipleResources) { |
| 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"; |
| |
| auto script1_sxg_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script1_sxg_path); |
| auto script2_sxg_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script2_sxg_path); |
| auto script1_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script1_path); |
| auto script2_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script2_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, GetPrefetchURLLoaderCallCount()); |
| |
| 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 net::SHA256HashValue script2_header_integrity = {{0x03}}; |
| |
| const std::string outer_link_header = base::JoinString( |
| {CreateAlternateLinkHeader(script1_sxg_url, script1_url), |
| CreateAlternateLinkHeader(script2_sxg_url, script2_url)}, |
| ","); |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(script1_url, script1_header_integrity), |
| CreateAllowedAltSxgLinkHeader(script2_url, script2_header_integrity), |
| CreatePreloadLinkHeader(script1_url, "script"), |
| CreatePreloadLinkHeader(script2_url, "script")}, |
| ","); |
| 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, |
| CreateSignedExchangeResponseEntry( |
| "<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script1.js\"></script>" |
| "<script src=\"./script2.js\"></script></head>", |
| {{"link", outer_link_header}})); |
| RegisterResponse(script1_sxg_path, CreateSignedExchangeResponseEntry( |
| "var test_title=\"done\";")); |
| RegisterResponse(script2_sxg_path, CreateSignedExchangeResponseEntry( |
| "document.title=test_title;")); |
| 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)); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script1_sxg_url); |
| WaitUntilLoaded(script2_sxg_url); |
| |
| EXPECT_EQ(1, script1_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script1_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script2_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script2_request_counter->GetRequestCount()); |
| |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| 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_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script1_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script2_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script2_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| IntegrityMismatch) { |
| 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"; |
| |
| auto script_sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script_sxg_path); |
| auto script_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, GetPrefetchURLLoaderCallCount()); |
| |
| 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}}; |
| |
| const std::string outer_link_header = |
| CreateAlternateLinkHeader(script_sxg_url, script_url); |
| // Use the wrong header integrity value for "allowed-alt-sxg" link header to |
| // trigger the integrity mismatch fallback logic. |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString({CreateAllowedAltSxgLinkHeader( |
| script_url, wrong_script_header_integrity), |
| CreatePreloadLinkHeader(script_url, "script")}, |
| ","); |
| |
| 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, |
| CreateSignedExchangeResponseEntry( |
| "<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\"></script></head>", |
| {{"link", outer_link_header}})); |
| RegisterResponse(script_sxg_path, CreateSignedExchangeResponseEntry( |
| "document.title=\"done\";")); |
| 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)); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script_sxg_url); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| |
| // 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_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| MultipleResources_IntegrityMismatch) { |
| 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"; |
| |
| auto script1_sxg_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script1_sxg_path); |
| auto script2_sxg_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script2_sxg_path); |
| auto script1_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script1_path); |
| auto script2_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script2_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, GetPrefetchURLLoaderCallCount()); |
| |
| 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 net::SHA256HashValue script2_header_integrity = {{0x03}}; |
| const net::SHA256HashValue wrong_script2_header_integrity = {{0x04}}; |
| |
| const std::string outer_link_header = base::JoinString( |
| {CreateAlternateLinkHeader(script1_sxg_url, script1_url), |
| CreateAlternateLinkHeader(script2_sxg_url, script2_url)}, |
| ","); |
| // Use the wrong header integrity value for "allowed-alt-sxg" link header for |
| // script2 to trigger the integrity mismatch fallback logic. |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(script1_url, script1_header_integrity), |
| CreateAllowedAltSxgLinkHeader(script2_url, |
| wrong_script2_header_integrity), |
| CreatePreloadLinkHeader(script1_url, "script"), |
| CreatePreloadLinkHeader(script2_url, "script")}, |
| ","); |
| |
| 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, |
| CreateSignedExchangeResponseEntry( |
| "<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script1.js\"></script>" |
| "<script src=\"./script2.js\"></script></head>", |
| {{"link", outer_link_header}})); |
| RegisterResponse(script1_sxg_path, CreateSignedExchangeResponseEntry( |
| "var test_title=\"done\";")); |
| RegisterResponse(script2_sxg_path, CreateSignedExchangeResponseEntry( |
| "document.title=test_title;")); |
| 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)); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script1_sxg_url); |
| WaitUntilLoaded(script2_sxg_url); |
| |
| EXPECT_EQ(1, script1_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script1_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script2_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script2_request_counter->GetRequestCount()); |
| |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| 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_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script1_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script2_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script2_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, CORS) { |
| std::unique_ptr<net::EmbeddedTestServer> data_server = |
| std::make_unique<net::EmbeddedTestServer>( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| |
| const char* prefetch_path = "/prefetch.html"; |
| const char* target_sxg_path = "/target.sxg"; |
| const char* target_path = "/target.html"; |
| |
| RegisterRequestHandler(embedded_test_server()); |
| RegisterRequestHandler(data_server.get()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ASSERT_TRUE(data_server->Start()); |
| |
| std::string test_server_origin = embedded_test_server()->base_url().spec(); |
| // Remove the last "/"" |
| test_server_origin.pop_back(); |
| |
| struct { |
| // Set in the main SXG's inner response header. |
| // Example: |
| // Link: <http://***/**.data>;rel="preload";as="fetch";crossorigin |
| // ^^^^^^^^^^^ |
| const char* const crossorigin_preload_header; |
| // Set in the data SXG's inner response header. |
| // Example: |
| // Access-Control-Allow-Origin: * |
| // ^ |
| const char* const access_control_allow_origin_header; |
| // Set "Access-Control-Allow-Credentials: true" link header in the data |
| // SXG's inner response header. |
| const bool has_access_control_allow_credentials_true_header; |
| // The credentials attribute of Fetch API's request while fetching the data. |
| const char* const request_credentials; |
| // If the data is served from the SXG the result must be "sxg". If the data |
| // is served from the server without SXG, the result must be "server". If |
| // failed to fetch the data, the result must be "failed". |
| const char* const expected_result; |
| } kTestCases[] = { |
| // - If crossorigin is not set in the preload header, cross origin fetch |
| // goes to the server. It is because the mode of the preload request |
| // ("no-cors") and the mode of the fetch request ("cors") doesn't match. |
| {nullptr, nullptr, false, "omit", "server"}, |
| {nullptr, nullptr, false, "include", "server"}, |
| {nullptr, nullptr, false, "same-origin", "server"}, |
| |
| // - When "crossorigin" is set in the preload header, the mode of the |
| // preload request is "cors", and the credentials mode is "same-origin". |
| // - When the credentials mode of the fetch request doesn't match, the |
| // fetch request goes to the server. |
| {"crossorigin", nullptr, false, "omit", "server"}, |
| {"crossorigin", nullptr, false, "include", "server"}, |
| // - When the credentials mode of the fetch request match with the |
| // credentials mode of the preload request, the prefetched signed |
| // exchange is used. |
| // - When the inner response doesn't have ACAO header, fails to load. |
| {"crossorigin", nullptr, false, "same-origin", "failed"}, |
| // - When the inner response has "ACAO: *" header, succeeds to load. |
| {"crossorigin", "*", false, "same-origin", "sxg"}, |
| // - When the inner response has "ACAO: <origin>" header, succeeds to |
| // load. |
| {"crossorigin", test_server_origin.c_str(), false, "same-origin", "sxg"}, |
| |
| // - crossorigin="anonymous" must be treated as same as just having |
| // "crossorigin". |
| {"crossorigin=\"anonymous\"", nullptr, false, "omit", "server"}, |
| {"crossorigin=\"anonymous\"", nullptr, false, "include", "server"}, |
| {"crossorigin=\"anonymous\"", nullptr, false, "same-origin", "failed"}, |
| {"crossorigin=\"anonymous\"", "*", false, "same-origin", "sxg"}, |
| {"crossorigin=\"anonymous\"", test_server_origin.c_str(), false, |
| "same-origin", "sxg"}, |
| |
| // - When crossorigin="use-credentials" is set in the preload header, the |
| // mode of the preload request is "cors", and the credentials mode is |
| // "include". |
| // - When the credentials mode of the fetch request doesn't match, the |
| // fetch request goes to the server. |
| {"crossorigin=\"use-credentials\"", nullptr, false, "omit", "server"}, |
| {"crossorigin=\"use-credentials\"", nullptr, false, "same-origin", |
| "server"}, |
| // - When the credentials mode of the fetch request match with the |
| // credentials mode of the preload request, the prefetched signed |
| // exchange is used. |
| // - When the inner response doesn't have ACAO header, fails to load. |
| {"crossorigin=\"use-credentials\"", nullptr, false, "include", "failed"}, |
| // - Even if the inner response has "ACAO: *" header, fails to load |
| // the "include" credentials mode request. |
| {"crossorigin=\"use-credentials\"", "*", false, "include", "failed"}, |
| // - Even if the inner response has "ACAO: *" header and "ACAC: true" |
| // header, fails to load the "include" credentials mode request. |
| {"crossorigin=\"use-credentials\"", "*", true, "include", "failed"}, |
| // - Even if the inner response has "ACAO: <origin>" header, fails to |
| // load the "include" credentials mode request. |
| {"crossorigin=\"use-credentials\"", test_server_origin.c_str(), false, |
| "include", "failed"}, |
| // - If the inner response has "ACAO: <origin>" header, and |
| // "ACAC: true" header, succeeds to load. |
| {"crossorigin=\"use-credentials\"", test_server_origin.c_str(), true, |
| "include", "sxg"}, |
| }; |
| |
| const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path); |
| const GURL target_url = embedded_test_server()->GetURL(target_path); |
| |
| const net::SHA256HashValue target_header_integrity = {{0x01}}; |
| |
| std::string target_sxg_outer_link_header; |
| std::string target_sxg_inner_link_header("Link: "); |
| std::string requests_list_string; |
| |
| std::vector<MockSignedExchangeHandlerParams> mock_params; |
| for (size_t i = 0; i < base::size(kTestCases); ++i) { |
| if (i) { |
| target_sxg_outer_link_header += ","; |
| target_sxg_inner_link_header += ","; |
| requests_list_string += ","; |
| } |
| const std::string data_sxg_path = base::StringPrintf("/%zu_data.sxg", i); |
| const std::string data_path = base::StringPrintf("/%zu.data", i); |
| const GURL data_sxg_url = embedded_test_server()->GetURL(data_sxg_path); |
| const GURL data_url = data_server->GetURL(data_path); |
| requests_list_string += base::StringPrintf( |
| "new Request('%s', {credentials: '%s'})", data_url.spec().c_str(), |
| kTestCases[i].request_credentials); |
| const net::SHA256HashValue data_header_integrity = {{0x02 + i}}; |
| |
| target_sxg_outer_link_header += |
| CreateAlternateLinkHeader(data_sxg_url, data_url); |
| target_sxg_inner_link_header += base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(data_url, data_header_integrity), |
| CreatePreloadLinkHeader(data_url, "fetch")}, |
| ","); |
| if (kTestCases[i].crossorigin_preload_header) { |
| target_sxg_inner_link_header += |
| base::StringPrintf(";%s", kTestCases[i].crossorigin_preload_header); |
| } |
| RegisterResponse(data_sxg_path, CreateSignedExchangeResponseEntry("sxg")); |
| RegisterResponse( |
| data_path, |
| ResponseEntry( |
| "server", "text/plain", |
| {{"Access-Control-Allow-Origin", test_server_origin.c_str()}, |
| {"Access-Control-Allow-Credentials", "true"}})); |
| std::vector<std::string> data_sxg_inner_headers; |
| if (kTestCases[i].access_control_allow_origin_header) { |
| data_sxg_inner_headers.emplace_back( |
| base::StringPrintf("Access-Control-Allow-Origin: %s", |
| kTestCases[i].access_control_allow_origin_header)); |
| } |
| if (kTestCases[i].has_access_control_allow_credentials_true_header) { |
| data_sxg_inner_headers.emplace_back( |
| "Access-Control-Allow-Credentials: true"); |
| } |
| mock_params.emplace_back( |
| data_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, data_url, |
| "text/plain", std::move(data_sxg_inner_headers), data_header_integrity); |
| } |
| |
| std::vector<std::string> target_sxg_inner_headers = { |
| std::move(target_sxg_inner_link_header)}; |
| mock_params.emplace_back(target_sxg_url, SignedExchangeLoadResult::kSuccess, |
| net::OK, target_url, "text/html", |
| std::move(target_sxg_inner_headers), |
| target_header_integrity); |
| MockSignedExchangeHandlerFactory factory(std::move(mock_params)); |
| |
| RegisterResponse( |
| prefetch_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", target_sxg_path))); |
| RegisterResponse(target_sxg_path, |
| CreateSignedExchangeResponseEntry( |
| base::StringPrintf(R"( |
| <head><title>Prefetch Target (SXG)</title><script> |
| let results = []; |
| (async function(requests) { |
| for (let i = 0; i < requests.length; ++i) { |
| try { |
| const res = await fetch(requests[i]); |
| results.push(await res.text()); |
| } catch (err) { |
| results.push('failed'); |
| } |
| } |
| document.title = 'done'; |
| })([%s]); |
| </script></head>)", |
| requests_list_string.c_str()), |
| {{"link", target_sxg_outer_link_header}})); |
| |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)); |
| |
| // Wait until all (main- and sub-resource) SXGs are prefetched. |
| while (GetCachedExchanges(shell()).size() < base::size(kTestCases) + 1) { |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); |
| run_loop.Run(); |
| } |
| |
| NavigateToURLAndWaitTitle(target_sxg_url, "done"); |
| |
| for (size_t i = 0; i < base::size(kTestCases); ++i) { |
| SCOPED_TRACE(base::StringPrintf("TestCase %zu", i)); |
| EXPECT_EQ( |
| EvalJs(shell(), base::StringPrintf("results[%zu]", i)).ExtractString(), |
| kTestCases[i].expected_result); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| CrossOriginReadBlocking_AllowedAfterSniffing) { |
| RunCrossOriginReadBlockingTest( |
| true /* cross_origin */, "document.title=\"done\"", "text/javascript", |
| false /* has_nosniff */, "done", |
| network::CrossOriginReadBlocking::Action::kAllowedAfterSniffing); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| CrossOriginReadBlocking_BlockedAfterSniffing) { |
| RunCrossOriginReadBlockingTest( |
| true /* cross_origin */, "<!DOCTYPE html>hello;", "text/html", |
| false /* has_nosniff */, "original title", |
| network::CrossOriginReadBlocking::Action::kBlockedAfterSniffing); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| CrossOriginReadBlocking_AllowedWithoutSniffing) { |
| RunCrossOriginReadBlockingTest( |
| false /* cross_origin */, "document.title=\"done\"", "text/javascript", |
| true /* has_nosniff */, "done", |
| network::CrossOriginReadBlocking::Action::kAllowedWithoutSniffing); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| CrossOriginReadBlocking_BlockedWithoutSniffing) { |
| RunCrossOriginReadBlockingTest( |
| true /* cross_origin */, "<!DOCTYPE html>hello;", "text/html", |
| true /* has_nosniff */, "original title", |
| network::CrossOriginReadBlocking::Action::kBlockedWithoutSniffing); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| ScriptSXGNotGCed) { |
| const char* prefetch_page_path = "/prefetch.html"; |
| const char* page_sxg_path = "/target.sxg"; |
| const char* page_inner_url_path = "/target.html"; |
| const char* script_sxg_path = "/script.sxg"; |
| const char* script_inner_url_path = "/script.js"; |
| |
| auto script_sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script_sxg_path); |
| auto script_request_counter = RequestCounter::CreateAndMonitor( |
| embedded_test_server(), script_inner_url_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL prefetch_page_url = |
| embedded_test_server()->GetURL(prefetch_page_path); |
| const GURL sxg_page_url = embedded_test_server()->GetURL(page_sxg_path); |
| const GURL inner_url_page_url = |
| embedded_test_server()->GetURL(page_inner_url_path); |
| const GURL sxg_script_url = embedded_test_server()->GetURL(script_sxg_path); |
| const GURL inner_url_script_url = |
| embedded_test_server()->GetURL(script_inner_url_path); |
| |
| const net::SHA256HashValue page_header_integrity = {{0x01}}; |
| const net::SHA256HashValue script_header_integrity = {{0x02}}; |
| |
| const std::string outer_link_header = |
| CreateAlternateLinkHeader(sxg_script_url, inner_url_script_url); |
| const std::string inner_link_headers = |
| std::string("Link: ") + |
| base::JoinString( |
| {CreateAllowedAltSxgLinkHeader(inner_url_script_url, |
| script_header_integrity), |
| CreatePreloadLinkHeader(inner_url_script_url, "script")}, |
| ","); |
| |
| RegisterResponse(prefetch_page_path, |
| ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'></body>", |
| sxg_page_url.spec().c_str()))); |
| RegisterResponse( |
| page_sxg_path, |
| CreateSignedExchangeResponseEntry( |
| "<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\" async defer></script></head>", |
| {{"link", outer_link_header}})); |
| RegisterResponse(script_sxg_path, CreateSignedExchangeResponseEntry( |
| "document.title=\"done\";", {})); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| sxg_page_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| inner_url_page_url, "text/html", {inner_link_headers}, |
| page_header_integrity), |
| MockSignedExchangeHandlerParams( |
| sxg_script_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| inner_url_script_url, "text/javascript", |
| // Set "cache-control: public" to keep the script in the memory |
| // cache. |
| {"cache-control: public, max-age=600"}, script_header_integrity)}); |
| ScopedSignedExchangeHandlerFactory scoped_factory(&factory); |
| |
| EXPECT_EQ(0, GetPrefetchURLLoaderCallCount()); |
| NavigateToURL(shell(), prefetch_page_url); |
| |
| WaitUntilLoaded(sxg_page_url); |
| WaitUntilLoaded(sxg_script_url); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| EXPECT_EQ(2, GetPrefetchURLLoaderCallCount()); |
| |
| const auto cached_exchanges = GetCachedExchanges(shell()); |
| EXPECT_EQ(2u, cached_exchanges.size()); |
| |
| NavigateToURLAndWaitTitle(sxg_page_url, "done"); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| |
| // Clears the title. |
| EXPECT_TRUE(ExecuteScript(shell()->web_contents(), "document.title = '';")); |
| |
| const char* next_page_path = "/next_page.html"; |
| const GURL next_page_url = embedded_test_server()->GetURL(next_page_path); |
| RegisterResponse( |
| next_page_path, |
| ResponseEntry( |
| "<head><title>Next page</title>" |
| "<script src=\"./script.js\" async defer></script></head>")); |
| // Triggers GC. |
| EXPECT_TRUE( |
| ExecuteScript(shell()->web_contents(), "internals.scheduleBlinkGC();")); |
| // The script which was served via SXG must be kept in memory cache and must |
| // be reused. |
| NavigateToURLAndWaitTitle(next_page_url, "done"); |
| |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| DoNotSendUnrelatedSXG) { |
| 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"; |
| |
| auto script_sxg_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script_sxg_path); |
| auto script_request_counter = |
| RequestCounter::CreateAndMonitor(embedded_test_server(), script_path); |
| RegisterRequestHandler(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(0, GetPrefetchURLLoaderCallCount()); |
| |
| 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}}; |
| |
| RegisterResponse(prefetch_path, ResponseEntry(base::StringPrintf( |
| "<body><link rel='prefetch' href='%s'>" |
| "<link rel='prefetch' href='%s'></body>", |
| target_sxg_path, script_sxg_path))); |
| RegisterResponse(script_path, ResponseEntry("document.title=\"from server\";", |
| "text/javascript")); |
| RegisterResponse(target_sxg_path, |
| CreateSignedExchangeResponseEntry( |
| "<head><title>Prefetch Target (SXG)</title>" |
| "<script src=\"./script.js\"></script></head>")); |
| RegisterResponse(script_sxg_path, CreateSignedExchangeResponseEntry( |
| "document.title=\"from sxg\";")); |
| MockSignedExchangeHandlerFactory factory( |
| {MockSignedExchangeHandlerParams( |
| target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, |
| target_url, "text/html", {}, 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)); |
| |
| WaitUntilLoaded(target_sxg_url); |
| WaitUntilLoaded(script_sxg_url); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(0, script_request_counter->GetRequestCount()); |
| |
| NavigationHandleSXGAttributeObserver observer(shell()->web_contents()); |
| NavigateToURLAndWaitTitle(target_sxg_url, "from server"); |
| |
| EXPECT_EQ(1, script_sxg_request_counter->GetRequestCount()); |
| EXPECT_EQ(1, script_request_counter->GetRequestCount()); |
| ASSERT_TRUE(observer.had_prefetched_alt_sxg().has_value()); |
| EXPECT_FALSE(observer.had_prefetched_alt_sxg().value()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(SignedExchangeSubresourcePrefetchBrowserTest, |
| SignedExchangeSubresourcePrefetchBrowserTest, |
| ::testing::Bool()); |
| } // namespace content |