| // Copyright 2018 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 <tuple> |
| |
| #include "base/bind.h" |
| #include "base/files/file_path.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/lock.h" |
| #include "base/task/post_task.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/time.h" |
| #include "content/browser/loader/prefetch_url_loader_service.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/storage_partition_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/browser/web_package/prefetched_signed_exchange_cache.h" |
| #include "content/browser/web_package/signed_exchange_handler.h" |
| #include "content/browser/web_package/signed_exchange_utils.h" |
| #include "content/common/content_constants_internal.h" |
| #include "content/public/browser/back_forward_cache.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/download_manager.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/network_service_instance.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/ssl_status.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_paths.h" |
| #include "content/public/common/page_type.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/content_cert_verifier_browser_test.h" |
| #include "content/public/test/navigation_handle_observer.h" |
| #include "content/public/test/signed_exchange_browser_test_helper.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_navigation_throttle.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/shell/browser/shell_content_browser_client.h" |
| #include "content/shell/browser/shell_download_manager_delegate.h" |
| #include "media/media_buildflags.h" |
| #include "mojo/public/cpp/bindings/sync_call_restrictions.h" |
| #include "net/base/features.h" |
| #include "net/cert/cert_verify_result.h" |
| #include "net/cert/ct_policy_status.h" |
| #include "net/cert/mock_cert_verifier.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/transport_security_state.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/embedded_test_server/controllable_http_response.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "net/test/test_data_directory.h" |
| #include "net/test/url_request/url_request_mock_http_job.h" |
| #include "services/network/public/cpp/constants.h" |
| #include "services/network/public/cpp/features.h" |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr char kExpectedSXGEnabledAcceptHeaderForPrefetch[] = |
| "application/signed-exchange;v=b3;q=0.9,*/*;q=0.8"; |
| |
| constexpr char kLoadResultHistogram[] = "SignedExchange.LoadResult2"; |
| constexpr char kPrefetchResultHistogram[] = |
| "SignedExchange.Prefetch.LoadResult2"; |
| constexpr char kRedirectLoopHistogram[] = "SignedExchange.FallbackRedirectLoop"; |
| |
| class RedirectObserver : public WebContentsObserver { |
| public: |
| explicit RedirectObserver(WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| ~RedirectObserver() override = default; |
| |
| void DidRedirectNavigation(NavigationHandle* handle) override { |
| const net::HttpResponseHeaders* response = handle->GetResponseHeaders(); |
| if (response) |
| response_code_ = response->response_code(); |
| } |
| |
| const base::Optional<int>& response_code() const { return response_code_; } |
| |
| private: |
| base::Optional<int> response_code_; |
| |
| DISALLOW_COPY_AND_ASSIGN(RedirectObserver); |
| }; |
| |
| class AssertNavigationHandleFlagObserver : public WebContentsObserver { |
| public: |
| explicit AssertNavigationHandleFlagObserver(WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| ~AssertNavigationHandleFlagObserver() override = default; |
| |
| void DidFinishNavigation(NavigationHandle* handle) override { |
| EXPECT_TRUE(handle->IsSignedExchangeInnerResponse()); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(AssertNavigationHandleFlagObserver); |
| }; |
| |
| class FinishNavigationObserver : public WebContentsObserver { |
| public: |
| FinishNavigationObserver(WebContents* contents, |
| base::OnceClosure done_closure) |
| : WebContentsObserver(contents), done_closure_(std::move(done_closure)) {} |
| |
| void DidFinishNavigation(NavigationHandle* navigation_handle) override { |
| error_code_ = navigation_handle->GetNetErrorCode(); |
| std::move(done_closure_).Run(); |
| } |
| |
| const base::Optional<net::Error>& error_code() const { return error_code_; } |
| |
| private: |
| base::OnceClosure done_closure_; |
| base::Optional<net::Error> error_code_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FinishNavigationObserver); |
| }; |
| |
| class MockContentBrowserClient final : public ContentBrowserClient { |
| public: |
| std::string GetAcceptLangs(BrowserContext* context) override { |
| return accept_langs_; |
| } |
| |
| void SetAcceptLangs(const std::string langs) { accept_langs_ = langs; } |
| |
| private: |
| std::string accept_langs_ = "en"; |
| }; |
| |
| // Histograms: PrefetchedSignedExchangeCache.* are recorded when the current |
| // document is replaced by a new one: |
| // (a) If the new document is loaded in a new RenderFrameHost. The histogram is |
| // recorded in the old RenderFrameHost destructor. |
| // (b) If the new document is loaded inside the same RenderFrameHost, it is |
| // recorded in RenderFrameHost::CommitNavigation(). |
| // |
| // Note: The RenderDocument project will remove code path (b). |
| // |
| // In (a), since the deletion of a RenderFrameHost is asynchronous, it caused |
| // several tests to be flaky. The tests weren't waiting for the RenderFrameHost |
| // deletion before checking the histograms. See https://crbug.com/1016865 |
| // |
| // Use this class for waiting any RenderFrameHost pending deletion or inside the |
| // BackForwardCache to be deleted |
| class InactiveRenderFrameHostDeletionObserver : public WebContentsObserver { |
| public: |
| InactiveRenderFrameHostDeletionObserver(WebContents* content) |
| : WebContentsObserver(content) { |
| // |rfh_count_| starts at zero, because we expect to start counting when |
| // there are zero active RenderFrameHost. |
| EXPECT_EQ(1u, content->GetAllFrames().size()); |
| EXPECT_FALSE(content->GetAllFrames()[0]->IsRenderFrameLive()); |
| } |
| ~InactiveRenderFrameHostDeletionObserver() override = default; |
| |
| void Wait() { |
| // Some RenderFrameHost may remain in the BackForwardCache. Request |
| // releasing them. This is asynchronous. |
| static_cast<WebContentsImpl*>(web_contents()) |
| ->GetController() |
| .GetBackForwardCache() |
| .Flush(); |
| |
| loop_ = std::make_unique<base::RunLoop>(); |
| target_rfh_count_ = web_contents()->GetAllFrames().size(); |
| CheckCondition(); |
| loop_->Run(); |
| } |
| |
| private: |
| void RenderFrameCreated(RenderFrameHost*) override { rfh_count_++; } |
| |
| void RenderFrameDeleted(RenderFrameHost*) override { |
| rfh_count_--; |
| CheckCondition(); |
| } |
| |
| void CheckCondition() { |
| if (loop_ && rfh_count_ == target_rfh_count_) |
| loop_->Quit(); |
| } |
| |
| std::unique_ptr<base::RunLoop> loop_; |
| int rfh_count_ = 0; |
| int target_rfh_count_; |
| |
| DISALLOW_COPY_AND_ASSIGN(InactiveRenderFrameHostDeletionObserver); |
| }; |
| |
| } // namespace |
| |
| class SignedExchangeRequestHandlerBrowserTestBase |
| : public CertVerifierBrowserTest { |
| public: |
| SignedExchangeRequestHandlerBrowserTestBase() { |
| // This installs "root_ca_cert.pem" from which our test certificates are |
| // created. (Needed for the tests that use real certificate, i.e. |
| // RealCertVerifier) |
| net::EmbeddedTestServer::RegisterTestCerts(); |
| feature_list_.InitWithFeatures({features::kSignedHTTPExchange}, {}); |
| } |
| |
| void SetUp() override { |
| sxg_test_helper_.SetUp(); |
| CertVerifierBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| CertVerifierBrowserTest::SetUpOnMainThread(); |
| |
| inactive_rfh_deletion_observer_ = |
| std::make_unique<InactiveRenderFrameHostDeletionObserver>( |
| shell()->web_contents()); |
| original_client_ = SetBrowserClientForTesting(&client_); |
| } |
| |
| void TearDownOnMainThread() override { |
| sxg_test_helper_.TearDownOnMainThread(); |
| SetBrowserClientForTesting(original_client_); |
| } |
| |
| protected: |
| void InstallUrlInterceptor(const GURL& url, const std::string& data_path) { |
| sxg_test_helper_.InstallUrlInterceptor(url, data_path); |
| } |
| |
| void InstallMockCert() { |
| sxg_test_helper_.InstallMockCert(mock_cert_verifier()); |
| } |
| |
| void InstallMockCertChainInterceptor() { |
| sxg_test_helper_.InstallMockCertChainInterceptor(); |
| } |
| |
| void SetAcceptLangs(const std::string langs) { |
| client_.SetAcceptLangs(langs); |
| StoragePartitionImpl* partition = |
| static_cast<StoragePartitionImpl*>(shell() |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition()); |
| partition->GetPrefetchURLLoaderService()->SetAcceptLanguages(langs); |
| } |
| |
| std::unique_ptr<InactiveRenderFrameHostDeletionObserver> |
| inactive_rfh_deletion_observer_; |
| |
| const base::HistogramTester histogram_tester_; |
| |
| MockContentBrowserClient client_; |
| |
| private: |
| ContentBrowserClient* original_client_ = nullptr; |
| |
| base::test::ScopedFeatureList feature_list_; |
| SignedExchangeBrowserTestHelper sxg_test_helper_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SignedExchangeRequestHandlerBrowserTestBase); |
| }; |
| |
| class SignedExchangeRequestHandlerBrowserTest |
| : public testing::WithParamInterface< |
| std::tuple<bool /* use_prefetch */, |
| bool /* sxg_subresource_prefetch_enabled */>>, |
| public SignedExchangeRequestHandlerBrowserTestBase { |
| public: |
| SignedExchangeRequestHandlerBrowserTest() { |
| std::tie(use_prefetch_, sxg_subresource_prefetch_enabled_) = GetParam(); |
| std::vector<base::Feature> enable_features; |
| std::vector<base::Feature> disabled_features; |
| 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); |
| } |
| ~SignedExchangeRequestHandlerBrowserTest() = default; |
| |
| protected: |
| bool UsePrefetch() const { return use_prefetch_; } |
| bool SXGPrefetchCacheIsEnabled() const { |
| return sxg_subresource_prefetch_enabled_; |
| } |
| |
| void MaybeTriggerPrefetchSXG(const GURL& url, bool expect_success) { |
| if (!UsePrefetch()) |
| return; |
| const GURL prefetch_html_url = embedded_test_server()->GetURL( |
| std::string("/sxg/prefetch.html#") + url.spec()); |
| std::u16string expected_title = |
| base::ASCIIToUTF16(expect_success ? "OK" : "FAIL"); |
| TitleWatcher title_watcher(shell()->web_contents(), expected_title); |
| EXPECT_TRUE(NavigateToURL(shell(), prefetch_html_url)); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| |
| if (SXGPrefetchCacheIsEnabled() && expect_success) |
| WaitUntilSXGIsCached(url); |
| } |
| |
| private: |
| class CacheObserver : public PrefetchedSignedExchangeCache::TestObserver { |
| public: |
| CacheObserver(const GURL& outer_url, base::OnceClosure quit_closure) |
| : outer_url_(outer_url), quit_closure_(std::move(quit_closure)) {} |
| ~CacheObserver() override = default; |
| |
| void OnStored(PrefetchedSignedExchangeCache* cache, |
| const GURL& outer_url) override { |
| if (quit_closure_ && (outer_url_ == outer_url)) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| private: |
| const GURL outer_url_; |
| base::OnceClosure quit_closure_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CacheObserver); |
| }; |
| |
| void WaitUntilSXGIsCached(const GURL& url) { |
| scoped_refptr<PrefetchedSignedExchangeCache> cache = |
| static_cast<RenderFrameHostImpl*>(shell() |
| ->web_contents() |
| ->GetMainFrame() |
| ->GetRenderViewHost() |
| ->GetMainFrame()) |
| ->EnsurePrefetchedSignedExchangeCache(); |
| |
| if (cache->GetExchanges().find(url) != cache->GetExchanges().end()) |
| return; |
| base::RunLoop run_loop; |
| auto observer = |
| std::make_unique<CacheObserver>(url, run_loop.QuitClosure()); |
| cache->AddObserverForTesting(observer.get()); |
| run_loop.Run(); |
| cache->RemoveObserverForTesting(observer.get()); |
| } |
| |
| bool use_prefetch_ = false; |
| bool sxg_subresource_prefetch_enabled_ = false; |
| base::test::ScopedFeatureList feature_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SignedExchangeRequestHandlerBrowserTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, Simple) { |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"); |
| |
| MaybeTriggerPrefetchSXG(url, true); |
| |
| if (!UsePrefetch()) { |
| // Need to be in a page to execute JavaScript to trigger renderer initiated |
| // navigation. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"))); |
| } |
| |
| std::u16string title = u"https://test.example.org/test/"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| RedirectObserver redirect_observer(shell()->web_contents()); |
| AssertNavigationHandleFlagObserver assert_navigation_handle_flag_observer( |
| shell()->web_contents()); |
| |
| // PrefetchedSignedExchangeCache is used only for renderer initiated |
| // navigation. So use JavaScript to trigger renderer initiated navigation. |
| EXPECT_TRUE( |
| ExecJs(shell()->web_contents(), |
| base::StringPrintf("location.href = '%s';", url.spec().c_str()))); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| EXPECT_EQ(303, redirect_observer.response_code()); |
| |
| NavigationEntry* entry = |
| shell()->web_contents()->GetController().GetVisibleEntry(); |
| EXPECT_TRUE(entry->GetSSL().initialized); |
| EXPECT_FALSE(!!(entry->GetSSL().content_status & |
| SSLStatus::DISPLAYED_INSECURE_CONTENT)); |
| ASSERT_TRUE(entry->GetSSL().certificate); |
| |
| // "test.example.org.public.pem.cbor" is generated from |
| // "prime256v1-sha256.public.pem". So the SHA256 of the certificates must |
| // match. |
| const net::SHA256HashValue fingerprint = |
| net::X509Certificate::CalculateFingerprint256( |
| entry->GetSSL().certificate->cert_buffer()); |
| scoped_refptr<net::X509Certificate> original_cert = |
| SignedExchangeBrowserTestHelper::LoadCertificate(); |
| const net::SHA256HashValue original_fingerprint = |
| net::X509Certificate::CalculateFingerprint256( |
| original_cert->cert_buffer()); |
| EXPECT_EQ(original_fingerprint, fingerprint); |
| |
| inactive_rfh_deletion_observer_->Wait(); |
| histogram_tester_.ExpectUniqueSample( |
| kLoadResultHistogram, SignedExchangeLoadResult::kSuccess, |
| (UsePrefetch() && !SXGPrefetchCacheIsEnabled()) ? 2 : 1); |
| histogram_tester_.ExpectTotalCount( |
| "SignedExchange.Time.CertificateFetch.Success", |
| (UsePrefetch() && !SXGPrefetchCacheIsEnabled()) ? 2 : 1); |
| if (UsePrefetch()) { |
| histogram_tester_.ExpectUniqueSample(kPrefetchResultHistogram, |
| SignedExchangeLoadResult::kSuccess, 1); |
| if (SXGPrefetchCacheIsEnabled()) { |
| histogram_tester_.ExpectTotalCount("PrefetchedSignedExchangeCache.Count", |
| 1); |
| } else { |
| histogram_tester_.ExpectUniqueSample( |
| "SignedExchange.Prefetch.Recall.30Seconds", true, 1); |
| histogram_tester_.ExpectUniqueSample( |
| "SignedExchange.Prefetch.Precision.30Seconds", true, 1); |
| } |
| } else { |
| histogram_tester_.ExpectUniqueSample( |
| "SignedExchange.Prefetch.Recall.30Seconds", false, 1); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, VariantMatch) { |
| SetAcceptLangs("en-US,fr"); |
| InstallUrlInterceptor( |
| GURL("https://cert.example.org/cert.msg"), |
| "content/test/data/sxg/test.example.org.public.pem.cbor"); |
| InstallMockCert(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url = |
| embedded_test_server()->GetURL("/sxg/test.example.org_fr_variant.sxg"); |
| MaybeTriggerPrefetchSXG(url, true); |
| |
| if (!UsePrefetch()) { |
| // Need to be in a page to execute JavaScript to trigger renderer initiated |
| // navigation. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"))); |
| } |
| |
| std::u16string title = u"https://test.example.org/test/"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| // PrefetchedSignedExchangeCache is used only for renderer initiated |
| // navigation. So use JavaScript to trigger renderer initiated navigation. |
| EXPECT_TRUE( |
| ExecJs(shell()->web_contents(), |
| base::StringPrintf("location.href = '%s';", url.spec().c_str()))); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| |
| inactive_rfh_deletion_observer_->Wait(); |
| histogram_tester_.ExpectUniqueSample( |
| kLoadResultHistogram, SignedExchangeLoadResult::kSuccess, |
| (UsePrefetch() && !SXGPrefetchCacheIsEnabled()) ? 2 : 1); |
| if ((UsePrefetch() && SXGPrefetchCacheIsEnabled())) { |
| histogram_tester_.ExpectTotalCount("PrefetchedSignedExchangeCache.Count", |
| 1); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| VariantMismatch) { |
| SetAcceptLangs("en-US,ja"); |
| InstallUrlInterceptor( |
| GURL("https://cert.example.org/cert.msg"), |
| "content/test/data/sxg/test.example.org.public.pem.cbor"); |
| InstallUrlInterceptor(GURL("https://test.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| InstallMockCert(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url = |
| embedded_test_server()->GetURL("/sxg/test.example.org_fr_variant.sxg"); |
| MaybeTriggerPrefetchSXG(url, false); |
| |
| std::u16string title = u"Fallback URL response"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| histogram_tester_.ExpectUniqueSample( |
| kLoadResultHistogram, SignedExchangeLoadResult::kVariantMismatch, |
| UsePrefetch() ? 2 : 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| MissingNosniff) { |
| InstallUrlInterceptor(GURL("https://test.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| InstallMockCert(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url = embedded_test_server()->GetURL( |
| "/sxg/test.example.org_test_missing_nosniff.sxg"); |
| MaybeTriggerPrefetchSXG(url, false); |
| |
| std::u16string title = u"Fallback URL response"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| RedirectObserver redirect_observer(shell()->web_contents()); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| EXPECT_EQ(303, redirect_observer.response_code()); |
| histogram_tester_.ExpectUniqueSample( |
| kLoadResultHistogram, SignedExchangeLoadResult::kSXGServedWithoutNosniff, |
| UsePrefetch() ? 2 : 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| InvalidContentType) { |
| InstallUrlInterceptor(GURL("https://test.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url = embedded_test_server()->GetURL( |
| "/sxg/test.example.org_test_invalid_content_type.sxg"); |
| MaybeTriggerPrefetchSXG(url, false); |
| |
| std::u16string title = u"Fallback URL response"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| RedirectObserver redirect_observer(shell()->web_contents()); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| EXPECT_EQ(303, redirect_observer.response_code()); |
| histogram_tester_.ExpectUniqueSample( |
| kLoadResultHistogram, SignedExchangeLoadResult::kVersionMismatch, |
| UsePrefetch() ? 2 : 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, Expired) { |
| signed_exchange_utils::SetVerificationTimeForTesting( |
| base::Time::UnixEpoch() + |
| base::TimeDelta::FromSeconds( |
| SignedExchangeBrowserTestHelper::kSignatureHeaderExpires + 1)); |
| |
| InstallMockCertChainInterceptor(); |
| InstallUrlInterceptor(GURL("https://test.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| InstallMockCert(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"); |
| |
| std::u16string title = u"Fallback URL response"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| RedirectObserver redirect_observer(shell()->web_contents()); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| EXPECT_EQ(303, redirect_observer.response_code()); |
| histogram_tester_.ExpectUniqueSample( |
| kLoadResultHistogram, |
| SignedExchangeLoadResult::kSignatureVerificationError, 1); |
| histogram_tester_.ExpectUniqueSample( |
| "SignedExchange.SignatureVerificationResult", |
| SignedExchangeSignatureVerifier::Result::kErrExpired, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| RedirectBrokenSignedExchanges) { |
| InstallUrlInterceptor(GURL("https://test.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| constexpr const char* kBrokenExchanges[] = { |
| "/sxg/test.example.org_test_invalid_magic_string.sxg", |
| "/sxg/test.example.org_test_invalid_cbor_header.sxg", |
| }; |
| |
| for (const auto* broken_exchange : kBrokenExchanges) { |
| SCOPED_TRACE(testing::Message() |
| << "testing broken exchange: " << broken_exchange); |
| |
| GURL url = embedded_test_server()->GetURL(broken_exchange); |
| |
| MaybeTriggerPrefetchSXG(url, false); |
| |
| std::u16string title = u"Fallback URL response"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| } |
| histogram_tester_.ExpectTotalCount(kLoadResultHistogram, |
| UsePrefetch() ? 4 : 2); |
| histogram_tester_.ExpectBucketCount( |
| kLoadResultHistogram, SignedExchangeLoadResult::kVersionMismatch, |
| UsePrefetch() ? 2 : 1); |
| histogram_tester_.ExpectBucketCount( |
| kLoadResultHistogram, SignedExchangeLoadResult::kHeaderParseError, |
| UsePrefetch() ? 2 : 1); |
| } |
| |
| #if defined(OS_ANDROID) |
| // https://crbug.com/966820. Fails pretty often on Android. |
| #define MAYBE_BadMICE DISABLED_BadMICE |
| #else |
| #define MAYBE_BadMICE BadMICE |
| #endif |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, MAYBE_BadMICE) { |
| InstallMockCertChainInterceptor(); |
| InstallMockCert(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url = |
| embedded_test_server()->GetURL("/sxg/test.example.org_test_bad_mice.sxg"); |
| |
| MaybeTriggerPrefetchSXG(url, false); |
| |
| const std::u16string title_good = u"Reached End: false"; |
| const std::u16string title_bad = u"Reached End: true"; |
| TitleWatcher title_watcher(shell()->web_contents(), title_good); |
| title_watcher.AlsoWaitForTitle(title_bad); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title_good, title_watcher.WaitAndGetTitle()); |
| |
| histogram_tester_.ExpectTotalCount(kLoadResultHistogram, |
| UsePrefetch() ? 2 : 1); |
| { |
| SCOPED_TRACE(testing::Message() |
| << "testing SignedExchangeLoadResult::kMerkleIntegrityError"); |
| histogram_tester_.ExpectBucketCount( |
| kLoadResultHistogram, SignedExchangeLoadResult::kMerkleIntegrityError, |
| UsePrefetch() ? 2 : 1); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, BadMICESmall) { |
| InstallMockCertChainInterceptor(); |
| InstallMockCert(); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url = embedded_test_server()->GetURL( |
| "/sxg/test.example.org_test_bad_mice_small.sxg"); |
| |
| MaybeTriggerPrefetchSXG(url, false); |
| |
| // Note: TitleWatcher is not needed. NavigateToURL waits until navigation |
| // complete. |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| |
| histogram_tester_.ExpectTotalCount(kLoadResultHistogram, |
| UsePrefetch() ? 2 : 1); |
| { |
| SCOPED_TRACE(testing::Message() |
| << "testing SignedExchangeLoadResult::kMerkleIntegrityError"); |
| histogram_tester_.ExpectBucketCount( |
| kLoadResultHistogram, SignedExchangeLoadResult::kMerkleIntegrityError, |
| UsePrefetch() ? 2 : 1); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, CertNotFound) { |
| InstallUrlInterceptor(GURL("https://cert.example.org/cert.msg"), |
| "content/test/data/sxg/404.msg"); |
| InstallUrlInterceptor(GURL("https://test.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"); |
| |
| MaybeTriggerPrefetchSXG(url, false); |
| |
| std::u16string title = u"Fallback URL response"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| histogram_tester_.ExpectUniqueSample( |
| kLoadResultHistogram, SignedExchangeLoadResult::kCertFetchError, |
| UsePrefetch() ? 2 : 1); |
| histogram_tester_.ExpectTotalCount( |
| "SignedExchange.Time.CertificateFetch.Failure", UsePrefetch() ? 2 : 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| SignedExchangeRequestHandlerBrowserTest, |
| ::testing::Combine(::testing::Bool(), |
| ::testing::Bool())); |
| |
| class SignedExchangeRequestHandlerDownloadBrowserTest |
| : public SignedExchangeRequestHandlerBrowserTestBase { |
| public: |
| SignedExchangeRequestHandlerDownloadBrowserTest() = default; |
| ~SignedExchangeRequestHandlerDownloadBrowserTest() override = default; |
| |
| protected: |
| class DownloadObserver : public DownloadManager::Observer { |
| public: |
| DownloadObserver(DownloadManager* manager) : manager_(manager) { |
| manager_->AddObserver(this); |
| } |
| ~DownloadObserver() override { manager_->RemoveObserver(this); } |
| |
| void WaitUntilDownloadCreated() { run_loop_.Run(); } |
| |
| const GURL& observed_url() const { return url_; } |
| const std::string& observed_content_disposition() const { |
| return content_disposition_; |
| } |
| |
| // content::DownloadManager::Observer implementation. |
| void OnDownloadCreated(content::DownloadManager* manager, |
| download::DownloadItem* item) override { |
| url_ = item->GetURL(); |
| content_disposition_ = item->GetContentDisposition(); |
| run_loop_.Quit(); |
| } |
| |
| private: |
| DownloadManager* manager_; |
| base::RunLoop run_loop_; |
| GURL url_; |
| std::string content_disposition_; |
| }; |
| |
| void SetUpOnMainThread() override { |
| SignedExchangeRequestHandlerBrowserTestBase::SetUpOnMainThread(); |
| ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir()); |
| ShellDownloadManagerDelegate* delegate = |
| static_cast<ShellDownloadManagerDelegate*>( |
| shell() |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDownloadManagerDelegate()); |
| delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath()); |
| } |
| |
| private: |
| base::ScopedTempDir downloads_directory_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest, |
| Download) { |
| std::unique_ptr<DownloadObserver> observer = |
| std::make_unique<DownloadObserver>( |
| shell()->web_contents()->GetBrowserContext()->GetDownloadManager()); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"))); |
| |
| const std::string load_sxg = |
| "const iframe = document.createElement('iframe');" |
| "iframe.src = './sxg/test.example.org_test_download.sxg';" |
| "document.body.appendChild(iframe);"; |
| EXPECT_TRUE(ExecJs(shell()->web_contents(), load_sxg)); |
| observer->WaitUntilDownloadCreated(); |
| EXPECT_EQ( |
| embedded_test_server()->GetURL("/sxg/test.example.org_test_download.sxg"), |
| observer->observed_url()); |
| EXPECT_EQ("attachment; filename=test.sxg", |
| observer->observed_content_disposition()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest, |
| DownloadInnerResponse) { |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| std::unique_ptr<DownloadObserver> observer = |
| std::make_unique<DownloadObserver>( |
| shell()->web_contents()->GetBrowserContext()->GetDownloadManager()); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"))); |
| |
| const std::string load_sxg = |
| "const iframe = document.createElement('iframe');" |
| "iframe.src = './sxg/test.example.org_bad_content_type.sxg';" |
| "document.body.appendChild(iframe);"; |
| // Since the inner response has an invalid Content-Type and MIME sniffing |
| // is disabled for Signed Exchange inner response, this should download the |
| // inner response of the exchange. |
| EXPECT_TRUE(ExecJs(shell()->web_contents(), load_sxg)); |
| observer->WaitUntilDownloadCreated(); |
| EXPECT_EQ(GURL("https://test.example.org/test/"), observer->observed_url()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest, |
| DataURLDownload) { |
| const GURL sxg_url = GURL("data:application/signed-exchange,"); |
| std::unique_ptr<DownloadObserver> observer = |
| std::make_unique<DownloadObserver>( |
| shell()->web_contents()->GetBrowserContext()->GetDownloadManager()); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"))); |
| |
| const std::string load_sxg = base::StringPrintf( |
| "const iframe = document.createElement('iframe');" |
| "iframe.src = '%s';" |
| "document.body.appendChild(iframe);", |
| sxg_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(shell()->web_contents(), load_sxg)); |
| observer->WaitUntilDownloadCreated(); |
| EXPECT_EQ(sxg_url, observer->observed_url()); |
| } |
| |
| class SignedExchangeRequestHandlerRealCertVerifierBrowserTest |
| : public SignedExchangeRequestHandlerBrowserTestBase { |
| public: |
| SignedExchangeRequestHandlerRealCertVerifierBrowserTest() { |
| // Use "real" CertVerifier. |
| disable_mock_cert_verifier(); |
| } |
| void SetUp() override { |
| SignedExchangeHandler::SetShouldIgnoreCertValidityPeriodErrorForTesting( |
| true); |
| SignedExchangeRequestHandlerBrowserTestBase::SetUp(); |
| } |
| void TearDown() override { |
| SignedExchangeRequestHandlerBrowserTestBase::TearDown(); |
| SignedExchangeHandler::SetShouldIgnoreCertValidityPeriodErrorForTesting( |
| false); |
| } |
| }; |
| |
| // If this fails with ERR_CERT_DATE_INVALID, try to regenerate test data |
| // by running generate-test-certs.sh and generate-test-sxgs.sh in |
| // src/content/test/data/sxg. |
| IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerRealCertVerifierBrowserTest, |
| Basic) { |
| InstallUrlInterceptor( |
| GURL("https://cert.example.org/cert.msg"), |
| "content/test/data/sxg/test.example.org-long-validity.public.pem.cbor"); |
| InstallUrlInterceptor(GURL("https://test.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url = embedded_test_server()->GetURL( |
| "/sxg/test.example.org_long_cert_validity.sxg"); |
| |
| // This signed exchange should pass CertVerifier::Verify() and then fail at |
| // SignedExchangeHandler::CheckOCSPStatus() because of the dummy OCSP |
| // response. |
| // TODO(https://crbug.com/815024): Make this test pass the OCSP check. We'll |
| // need to either generate an OCSP response on the fly, or override the OCSP |
| // verification time. |
| std::u16string title = u"Fallback URL response"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| // Verify that it failed at the OCSP check step. |
| histogram_tester_.ExpectUniqueSample(kLoadResultHistogram, |
| SignedExchangeLoadResult::kOCSPError, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| LogicalUrlInServiceWorkerScope) { |
| // SW-scope: https://test.example.org/test/ |
| // SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg |
| // SXG logical URL: https://test.example.org/test/ |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| const GURL install_sw_url = |
| GURL("https://test.example.org/test/publisher-service-worker.html"); |
| |
| InstallUrlInterceptor(install_sw_url, |
| "content/test/data/sxg/publisher-service-worker.html"); |
| InstallUrlInterceptor( |
| GURL("https://test.example.org/test/publisher-service-worker.js"), |
| "content/test/data/sxg/publisher-service-worker.js"); |
| { |
| std::u16string title = u"Done"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| EXPECT_TRUE(NavigateToURL(shell(), install_sw_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| } |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"); |
| |
| std::u16string title = u"https://test.example.org/test/"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| title_watcher.AlsoWaitForTitle(u"Generated"); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| // The page content shoud be served from the signed exchange. |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| NotControlledByDistributorsSW) { |
| // SW-scope: http://127.0.0.1:PORT/sxg/ |
| // SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg |
| // SXG logical URL: https://test.example.org/test/ |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL install_sw_url = embedded_test_server()->GetURL( |
| "/sxg/no-respond-with-service-worker.html"); |
| |
| { |
| std::u16string title = u"Done"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| EXPECT_TRUE(NavigateToURL(shell(), install_sw_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| std::u16string title = u"https://test.example.org/test/"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"), |
| GURL("https://test.example.org/test/") /* expected_commit_url */)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| |
| // The page must not be controlled by the service worker of the physical URL. |
| EXPECT_EQ(false, EvalJs(shell(), "!!navigator.serviceWorker.controller")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| NotControlledBySameOriginDistributorsSW) { |
| // SW-scope: https://test.example.org/scope/ |
| // SXG physical URL: https://test.example.org/scope/test.example.org_test.sxg |
| // SXG logical URL: https://test.example.org/test/ |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| InstallUrlInterceptor(GURL("https://test.example.org/scope/test.sxg"), |
| "content/test/data/sxg/test.example.org_test.sxg"); |
| |
| const GURL install_sw_url = GURL( |
| "https://test.example.org/scope/no-respond-with-service-worker.html"); |
| |
| InstallUrlInterceptor( |
| install_sw_url, |
| "content/test/data/sxg/no-respond-with-service-worker.html"); |
| InstallUrlInterceptor( |
| GURL("https://test.example.org/scope/no-respond-with-service-worker.js"), |
| "content/test/data/sxg/no-respond-with-service-worker.js"); |
| |
| { |
| std::u16string title = u"Done"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| EXPECT_TRUE(NavigateToURL(shell(), install_sw_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| std::u16string title = u"https://test.example.org/test/"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), GURL("https://test.example.org/scope/test.sxg"), |
| GURL("https://test.example.org/test/") /* expected_commit_url */)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| |
| // The page must not be controlled by the service worker of the physical URL. |
| EXPECT_EQ(false, EvalJs(shell(), "!!navigator.serviceWorker.controller")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, |
| RegisterServiceWorkerFromSignedExchange) { |
| // SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg |
| // SXG logical URL: https://test.example.org/test/ |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| InstallUrlInterceptor( |
| GURL("https://test.example.org/test/publisher-service-worker.js"), |
| "content/test/data/sxg/publisher-service-worker.js"); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"); |
| GURL expected_commit_url = GURL("https://test.example.org/test/"); |
| |
| { |
| std::u16string title = u"https://test.example.org/test/"; |
| TitleWatcher title_watcher(shell()->web_contents(), title); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| const std::string register_sw_script = |
| "(async function() {" |
| " try {" |
| " const registration = await navigator.serviceWorker.register(" |
| " 'publisher-service-worker.js', {scope: './'});" |
| " window.domAutomationController.send(true);" |
| " } catch (e) {" |
| " window.domAutomationController.send(false);" |
| " }" |
| "})();"; |
| // serviceWorker.register() fails because the document URL of |
| // ServiceWorkerHost is empty. |
| EXPECT_EQ(false, EvalJs(shell()->web_contents(), register_sw_script, |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY)); |
| } |
| |
| class SignedExchangeAcceptHeaderBrowserTest |
| : public ContentBrowserTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| using self = SignedExchangeAcceptHeaderBrowserTest; |
| SignedExchangeAcceptHeaderBrowserTest() |
| : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {} |
| ~SignedExchangeAcceptHeaderBrowserTest() override = default; |
| |
| protected: |
| void SetUp() override { |
| if (GetParam()) { |
| feature_list_.InitAndEnableFeature(features::kSignedHTTPExchange); |
| } else { |
| feature_list_.InitAndDisableFeature(features::kSignedHTTPExchange); |
| } |
| |
| https_server_.ServeFilesFromSourceDirectory("content/test/data"); |
| https_server_.RegisterRequestHandler( |
| base::BindRepeating(&self::RedirectResponseHandler)); |
| https_server_.RegisterRequestHandler(base::BindRepeating( |
| &self::FallbackSxgResponseHandler, base::Unretained(this))); |
| https_server_.RegisterRequestMonitor( |
| base::BindRepeating(&self::MonitorRequest, base::Unretained(this))); |
| ASSERT_TRUE(https_server_.Start()); |
| |
| ContentBrowserTest::SetUp(); |
| } |
| |
| void NavigateAndWaitForTitle(const GURL& url, const std::string title) { |
| std::u16string expected_title = base::ASCIIToUTF16(title); |
| TitleWatcher title_watcher(shell()->web_contents(), expected_title); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| void NavigateWithRedirectAndWaitForTitle(const GURL& url, |
| const GURL& expected_commit_url, |
| const std::string& title) { |
| std::u16string expected_title = base::ASCIIToUTF16(title); |
| TitleWatcher title_watcher(shell()->web_contents(), expected_title); |
| EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url)); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| bool IsSignedExchangeEnabled() const { return GetParam(); } |
| |
| void CheckAcceptHeader(const GURL& url, |
| bool is_navigation, |
| bool is_fallback) { |
| const auto accept_header = GetInterceptedAcceptHeader(url); |
| ASSERT_TRUE(accept_header); |
| EXPECT_EQ( |
| *accept_header, |
| IsSignedExchangeEnabled() && !is_fallback |
| ? (is_navigation |
| ? std::string(kFrameAcceptHeaderValue) + |
| std::string(kAcceptHeaderSignedExchangeSuffix) |
| : std::string(kExpectedSXGEnabledAcceptHeaderForPrefetch)) |
| : (is_navigation |
| ? std::string(kFrameAcceptHeaderValue) |
| : std::string(network::kDefaultAcceptHeaderValue))); |
| } |
| |
| void CheckNavigationAcceptHeader(const std::vector<GURL>& urls) { |
| for (const auto& url : urls) { |
| SCOPED_TRACE(url); |
| CheckAcceptHeader(url, true /* is_navigation */, false /* is_fallback */); |
| } |
| } |
| |
| void CheckPrefetchAcceptHeader(const std::vector<GURL>& urls) { |
| for (const auto& url : urls) { |
| SCOPED_TRACE(url); |
| CheckAcceptHeader(url, false /* is_navigation */, |
| false /* is_fallback */); |
| } |
| } |
| |
| void CheckFallbackAcceptHeader(const std::vector<GURL>& urls) { |
| for (const auto& url : urls) { |
| SCOPED_TRACE(url); |
| CheckAcceptHeader(url, true /* is_navigation */, true /* is_fallback */); |
| } |
| } |
| |
| base::Optional<std::string> GetInterceptedAcceptHeader( |
| const GURL& url) const { |
| base::AutoLock lock(url_accept_header_map_lock_); |
| const auto it = url_accept_header_map_.find(url); |
| if (it == url_accept_header_map_.end()) |
| return base::nullopt; |
| return it->second; |
| } |
| |
| void ClearInterceptedAcceptHeaders() { |
| base::AutoLock lock(url_accept_header_map_lock_); |
| url_accept_header_map_.clear(); |
| } |
| |
| net::EmbeddedTestServer https_server_; |
| |
| private: |
| static std::unique_ptr<net::test_server::HttpResponse> |
| RedirectResponseHandler(const net::test_server::HttpRequest& request) { |
| if (!base::StartsWith(request.relative_url, "/r?", |
| base::CompareCase::SENSITIVE)) { |
| return nullptr; |
| } |
| std::unique_ptr<net::test_server::BasicHttpResponse> http_response( |
| new net::test_server::BasicHttpResponse); |
| http_response->set_code(net::HTTP_MOVED_PERMANENTLY); |
| http_response->AddCustomHeader("Location", request.relative_url.substr(3)); |
| http_response->AddCustomHeader("Cache-Control", "no-cache"); |
| return std::move(http_response); |
| } |
| |
| // Responds with a prologue-only signed exchange that triggers a fallback |
| // redirect. |
| std::unique_ptr<net::test_server::HttpResponse> FallbackSxgResponseHandler( |
| const net::test_server::HttpRequest& request) { |
| const std::string prefix = "/fallback_sxg?"; |
| if (!base::StartsWith(request.relative_url, prefix, |
| base::CompareCase::SENSITIVE)) { |
| return nullptr; |
| } |
| std::string fallback_url(request.relative_url.substr(prefix.length())); |
| if (fallback_url.empty()) { |
| // If fallback URL is not specified, fallback to itself. |
| fallback_url = https_server_.GetURL(prefix).spec(); |
| } |
| |
| std::unique_ptr<net::test_server::BasicHttpResponse> http_response( |
| new net::test_server::BasicHttpResponse); |
| http_response->set_code(net::HTTP_OK); |
| http_response->set_content_type("application/signed-exchange;v=b3"); |
| |
| std::string sxg("sxg1-b3", 8); |
| sxg.push_back(fallback_url.length() >> 8); |
| sxg.push_back(fallback_url.length() & 0xff); |
| sxg += fallback_url; |
| // FallbackUrlAndAfter() requires 6 more bytes for sizes of next fields. |
| sxg.resize(sxg.length() + 6); |
| |
| http_response->set_content(sxg); |
| return std::move(http_response); |
| } |
| |
| void MonitorRequest(const net::test_server::HttpRequest& request) { |
| const auto it = request.headers.find(net::HttpRequestHeaders::kAccept); |
| if (it == request.headers.end()) |
| return; |
| // Note this method is called on the EmbeddedTestServer's background thread. |
| base::AutoLock lock(url_accept_header_map_lock_); |
| url_accept_header_map_[request.base_url.Resolve(request.relative_url)] = |
| it->second; |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| base::test::ScopedFeatureList feature_list_for_accept_header_; |
| |
| // url_accept_header_map_ is accessed both on the main thread and on the |
| // EmbeddedTestServer's background thread via MonitorRequest(), so it must be |
| // locked. |
| mutable base::Lock url_accept_header_map_lock_; |
| std::map<GURL, std::string> url_accept_header_map_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, Simple) { |
| const GURL test_url = https_server_.GetURL("/sxg/test.html"); |
| NavigateAndWaitForTitle(test_url, test_url.spec()); |
| CheckNavigationAcceptHeader({test_url}); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, Redirect) { |
| const GURL test_url = https_server_.GetURL("/sxg/test.html"); |
| const GURL redirect_url = https_server_.GetURL("/r?" + test_url.spec()); |
| const GURL redirect_redirect_url = |
| https_server_.GetURL("/r?" + redirect_url.spec()); |
| NavigateWithRedirectAndWaitForTitle(redirect_redirect_url, test_url, |
| test_url.spec()); |
| |
| CheckNavigationAcceptHeader({redirect_redirect_url, redirect_url, test_url}); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, |
| FallbackRedirect) { |
| if (!IsSignedExchangeEnabled()) |
| return; |
| |
| const GURL fallback_url = https_server_.GetURL("/sxg/test.html"); |
| const GURL test_url = |
| https_server_.GetURL("/fallback_sxg?" + fallback_url.spec()); |
| NavigateWithRedirectAndWaitForTitle(test_url, fallback_url, |
| fallback_url.spec()); |
| |
| CheckNavigationAcceptHeader({test_url}); |
| CheckFallbackAcceptHeader({fallback_url}); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, |
| FallbackRedirectLoop) { |
| if (!IsSignedExchangeEnabled()) |
| return; |
| |
| const base::HistogramTester histogram_tester; |
| base::RunLoop run_loop; |
| FinishNavigationObserver finish_navigation_observer(shell()->web_contents(), |
| run_loop.QuitClosure()); |
| const GURL test_url = https_server_.GetURL("/fallback_sxg?"); |
| EXPECT_FALSE(NavigateToURL(shell()->web_contents(), test_url)); |
| run_loop.Run(); |
| ASSERT_TRUE(finish_navigation_observer.error_code()) |
| << "Unexpected navigation success: " << test_url; |
| EXPECT_EQ(net::ERR_TOO_MANY_REDIRECTS, |
| *finish_navigation_observer.error_code()); |
| histogram_tester.ExpectUniqueSample(kRedirectLoopHistogram, true, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, |
| PrefetchEnabledPageEnabledTarget) { |
| const GURL target = https_server_.GetURL("/sxg/hello.txt"); |
| const GURL page_url = |
| https_server_.GetURL(std::string("/sxg/prefetch.html#") + target.spec()); |
| NavigateAndWaitForTitle(page_url, "OK"); |
| CheckPrefetchAcceptHeader({target}); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, |
| PrefetchRedirect) { |
| const GURL target = https_server_.GetURL("/sxg/hello.txt"); |
| const GURL redirect_url = https_server_.GetURL("/r?" + target.spec()); |
| const GURL redirect_redirect_url = |
| https_server_.GetURL("/r?" + redirect_url.spec()); |
| |
| const GURL page_url = https_server_.GetURL( |
| std::string("/sxg/prefetch.html#") + redirect_redirect_url.spec()); |
| |
| NavigateAndWaitForTitle(page_url, "OK"); |
| |
| CheckPrefetchAcceptHeader({redirect_redirect_url, redirect_url, target}); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, ServiceWorker) { |
| NavigateAndWaitForTitle(https_server_.GetURL("/sxg/service-worker.html"), |
| "Done"); |
| |
| const std::string frame_accept = kFrameAcceptHeaderValue; |
| const std::string frame_accept_with_sxg = |
| frame_accept + std::string(kAcceptHeaderSignedExchangeSuffix); |
| const std::vector<std::string> scopes = {"/sxg/sw-scope-generated/", |
| "/sxg/sw-scope-navigation-preload/", |
| "/sxg/sw-scope-no-respond-with/"}; |
| for (const auto& scope : scopes) { |
| SCOPED_TRACE(scope); |
| const bool is_generated_scope = |
| scope == std::string("/sxg/sw-scope-generated/"); |
| const GURL target_url = https_server_.GetURL(scope + "test.html"); |
| const GURL redirect_target_url = |
| https_server_.GetURL("/r?" + target_url.spec()); |
| const GURL redirect_redirect_target_url = |
| https_server_.GetURL("/r?" + redirect_target_url.spec()); |
| |
| const std::string expected_title = |
| is_generated_scope |
| ? (IsSignedExchangeEnabled() ? frame_accept_with_sxg : frame_accept) |
| : "Done"; |
| const base::Optional<std::string> expected_target_accept_header = |
| is_generated_scope |
| ? base::nullopt |
| : base::Optional<std::string>(IsSignedExchangeEnabled() |
| ? frame_accept_with_sxg |
| : frame_accept); |
| |
| NavigateAndWaitForTitle(target_url, expected_title); |
| EXPECT_EQ(expected_target_accept_header, |
| GetInterceptedAcceptHeader(target_url)); |
| ClearInterceptedAcceptHeaders(); |
| |
| NavigateWithRedirectAndWaitForTitle(redirect_target_url, target_url, |
| expected_title); |
| CheckNavigationAcceptHeader({redirect_target_url}); |
| EXPECT_EQ(expected_target_accept_header, |
| GetInterceptedAcceptHeader(target_url)); |
| ClearInterceptedAcceptHeaders(); |
| |
| NavigateWithRedirectAndWaitForTitle(redirect_redirect_target_url, |
| target_url, expected_title); |
| CheckNavigationAcceptHeader( |
| {redirect_redirect_target_url, redirect_target_url}); |
| EXPECT_EQ(expected_target_accept_header, |
| GetInterceptedAcceptHeader(target_url)); |
| ClearInterceptedAcceptHeaders(); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, |
| ServiceWorkerPrefetch) { |
| NavigateAndWaitForTitle( |
| https_server_.GetURL("/sxg/service-worker-prefetch.html"), "Done"); |
| const std::string scope = "/sxg/sw-prefetch-scope/"; |
| const GURL target_url = https_server_.GetURL(scope + "test.html"); |
| |
| const GURL prefetch_target = |
| https_server_.GetURL(std::string("/sxg/hello.txt")); |
| const std::string load_prefetch_script = base::StringPrintf( |
| "(function loadPrefetch(urls) {" |
| " for (let url of urls) {" |
| " let link = document.createElement('link');" |
| " link.rel = 'prefetch';" |
| " link.href = url;" |
| " document.body.appendChild(link);" |
| " }" |
| " function check() {" |
| " const entries = performance.getEntriesByType('resource');" |
| " const url_set = new Set(urls);" |
| " for (let entry of entries) {" |
| " url_set.delete(entry.name);" |
| " }" |
| " if (!url_set.size) {" |
| " window.domAutomationController.send(true);" |
| " } else {" |
| " setTimeout(check, 100);" |
| " }" |
| " }" |
| " check();" |
| "})(['%s'])", |
| prefetch_target.spec().c_str()); |
| |
| NavigateAndWaitForTitle(target_url, "Done"); |
| EXPECT_EQ(true, EvalJs(shell()->web_contents(), load_prefetch_script, |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY)); |
| CheckPrefetchAcceptHeader({prefetch_target}); |
| ClearInterceptedAcceptHeaders(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(SignedExchangeAcceptHeaderBrowserTest, |
| SignedExchangeAcceptHeaderBrowserTest, |
| testing::Bool()); |
| |
| class SignedExchangeExpectCTReportBrowserTest |
| : public SignedExchangeRequestHandlerBrowserTest { |
| public: |
| SignedExchangeExpectCTReportBrowserTest() { |
| feature_list_.InitWithFeatures( |
| // enabled_features |
| {net::TransportSecurityState::kDynamicExpectCTFeature, |
| net::features::kPartitionExpectCTStateByNetworkIsolationKey, |
| // These last two are not strictly necessary, but make this test more |
| // robust against enabling NetworkIsolationKeys everywhere. |
| net::features::kPartitionConnectionsByNetworkIsolationKey, |
| net::features::kPartitionSSLSessionsByNetworkIsolationKey}, |
| // disabled_features |
| {}); |
| ShellContentBrowserClient::set_enable_expect_ct_for_testing(true); |
| } |
| |
| ~SignedExchangeExpectCTReportBrowserTest() override { |
| ShellContentBrowserClient::set_enable_expect_ct_for_testing(false); |
| } |
| |
| void SetUpOnMainThread() override { |
| SignedExchangeRequestHandlerBrowserTestBase::SetUpOnMainThread(); |
| |
| // Make all attempts to connect to the domain the SXG is for fail with a DNS |
| // error. Without this, they're fail with ERR_NOT_IMPLEMENTED. Making |
| // requests fail with ERR_NAME_NOT_RESOLVED instead better matches what |
| // happens in production. |
| host_resolver()->AddSimulatedFailure("test.example.org"); |
| |
| host_resolver()->AddRule("prefetch-origin.test", "127.0.0.1"); |
| |
| // Set up callbacks for two requests for reports - first for the preflight, |
| // second for the actual request. Both use the same path. |
| preflight_response_ = |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| &report_server_, kReportPathPrefix, |
| true /* relative_url_is_prefix */); |
| report_response_ = |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| &report_server_, kReportPathPrefix, |
| true /* relative_url_is_prefix */); |
| ASSERT_TRUE(report_server_.Start()); |
| |
| // Set up certificate for the report server. |
| net::CertVerifyResult ssl_server_result; |
| ssl_server_result.verified_cert = report_server_.GetCertificate(); |
| ssl_server_result.is_issued_by_known_root = false; |
| ssl_server_result.policy_compliance = |
| net::ct::CTPolicyCompliance::CT_POLICY_COMPLIES_VIA_SCTS; |
| mock_cert_verifier()->AddResultForCert(report_server_.GetCertificate(), |
| ssl_server_result, net::OK); |
| |
| // Make the MockCertVerifier treat the signed exchange's certificate |
| // "prime256v1-sha256.public.pem" as valid for "test.example.org", but |
| // issued by a known root, which should cause a CT failure. |
| scoped_refptr<net::X509Certificate> original_cert = |
| SignedExchangeBrowserTestHelper::LoadCertificate(); |
| net::CertVerifyResult dummy_result; |
| dummy_result.verified_cert = original_cert; |
| dummy_result.cert_status = net::OK; |
| dummy_result.ocsp_result.response_status = net::OCSPVerifyResult::PROVIDED; |
| dummy_result.ocsp_result.revocation_status = |
| net::OCSPRevocationStatus::GOOD; |
| dummy_result.is_issued_by_known_root = true; |
| mock_cert_verifier()->AddResultForCertAndHost( |
| original_cert, "test.example.org", dummy_result, net::OK); |
| InstallMockCertChainInterceptor(); |
| |
| // Set up server used to serve the signed exchange. |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // All tests fetch or prefetch the signed exchange for use in a main frame |
| // load. This being the case, The NetworkIsolationKey used to load top-level |
| // signed exchanges (and thus used to check for Expect-CT information) is |
| // the NetworkIsolationKey of the site that serves the signed exchange, not |
| // the origin of the resource the signed exchange contains. Set up reports |
| // for both NetworkIsolationKeys, so can catch the wrong one being used for |
| // the report or the case NIKs are being ignored. |
| |
| url::Origin correct_report_origin = |
| url::Origin::Create(embedded_test_server()->base_url()); |
| net::NetworkIsolationKey correct_network_isolation_key( |
| correct_report_origin, correct_report_origin); |
| SetExpectCtUrl("test.example.org", correct_report_uri(), |
| correct_network_isolation_key); |
| |
| url::Origin incorrect_report_origin = |
| url::Origin::Create(sxg_validity_url()); |
| net::NetworkIsolationKey incorrect_network_isolation_key( |
| incorrect_report_origin, incorrect_report_origin); |
| SetExpectCtUrl("test.example.org", incorrect_sxg_validity_report_uri(), |
| incorrect_network_isolation_key); |
| } |
| |
| void SetExpectCtUrl(const std::string& domain, |
| const GURL& report_uri, |
| const net::NetworkIsolationKey& network_isolation_key) { |
| base::RunLoop run_loop; |
| network::mojom::NetworkContext* network_context = |
| shell() |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetNetworkContext(); |
| network_context->AddExpectCT( |
| domain, base::Time::Now() + base::TimeDelta::FromDays(1) /* expiry */, |
| true /* enforce */, report_uri, network_isolation_key, |
| base::BindLambdaForTesting([&](bool success) { |
| EXPECT_TRUE(success); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| void ValidateCtReport() { |
| // The CT failure should have generated a report. Wait for the preflight |
| // request, and respond to it. |
| preflight_response_->WaitForRequest(); |
| EXPECT_EQ(correct_report_uri(), |
| preflight_response_->http_request()->GetURL()); |
| EXPECT_EQ(net::test_server::METHOD_OPTIONS, |
| preflight_response_->http_request()->method); |
| preflight_response_->Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Access-Control-Allow-Origin: null\r\n" |
| "Access-Control-Allow-Methods: post\r\n" |
| "Access-Control-Allow-Headers: content-type\r\n\r\n"); |
| preflight_response_->Done(); |
| |
| // Responding to the preflight allows the report itself to be sent. Check |
| // that request as well. No need to respond to it. |
| report_response_->WaitForRequest(); |
| EXPECT_EQ(correct_report_uri(), report_response_->http_request()->GetURL()); |
| EXPECT_EQ(net::test_server::METHOD_POST, |
| report_response_->http_request()->method); |
| } |
| |
| net::test_server::EmbeddedTestServer* report_server() { |
| return &report_server_; |
| } |
| |
| const GURL sxg_url() const { |
| return embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"); |
| } |
| |
| // The URL the SXG resource claims to be for. |
| static GURL sxg_validity_url() { |
| return GURL("https://test.example.org/test/"); |
| } |
| |
| GURL other_incorrect_report_uri() const { |
| return report_server_.GetURL(kOtherIncorrectReportPath); |
| } |
| |
| private: |
| // Prefix used for all reports. |
| const char* kReportPathPrefix = "/report/"; |
| // Prefix used for reports made using correct NetworkIsolationKey. |
| const char* kCorrectReportPath = "/report/correct-nik"; |
| // Prefix used for reports made using sxg_url_validity_url()'s |
| // NetworkIsolationKey, which is not correct. |
| const char* kIncorrectSxgValidityReportPath = |
| "/report/incorrect-sxg-validity-nik"; |
| // Prefix used for reports made using another incorrect NetworkIsolationKey, |
| // set by the test. |
| const char* kOtherIncorrectReportPath = "/report/other-incorrect-nik"; |
| |
| // URI used for reports that use the correct NetworkIsolationKey for the |
| // report. |
| GURL correct_report_uri() const { |
| return report_server_.GetURL(kCorrectReportPath); |
| } |
| |
| // URI used for reports that incorrectly use the the SXG |
| GURL incorrect_sxg_validity_report_uri() const { |
| return report_server_.GetURL(kIncorrectSxgValidityReportPath); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| |
| // Server to send reports to. |
| net::test_server::EmbeddedTestServer report_server_{ |
| net::test_server::EmbeddedTestServer::TYPE_HTTPS}; |
| |
| // Interceptors used for CT violation report preflight and report HTTP |
| // requests. |
| std::unique_ptr<net::test_server::ControllableHttpResponse> |
| preflight_response_; |
| std::unique_ptr<net::test_server::ControllableHttpResponse> report_response_; |
| }; |
| |
| // Test that a report is send when a signed exchange fails a CT check and a |
| // matching Expect-CT header was previously received. |
| IN_PROC_BROWSER_TEST_P(SignedExchangeExpectCTReportBrowserTest, |
| CTFailureSendsExpectCTReport) { |
| if (UsePrefetch()) { |
| MaybeTriggerPrefetchSXG(sxg_url(), false /* expect_success */); |
| } else { |
| // Try to navigate to the signed exchange. The signed exchange fails the |
| // certificate transparency check. That results in trying to load the |
| // resource the SXG refers to directly, which should fail with |
| // ERR_NAME_NOT_RESOLVED. |
| NavigationHandleObserver observer(shell()->web_contents(), sxg_url()); |
| EXPECT_FALSE(NavigateToURL(shell(), sxg_url())); |
| EXPECT_EQ(sxg_validity_url(), shell()->web_contents()->GetURL()); |
| EXPECT_EQ(net::ERR_NAME_NOT_RESOLVED, observer.net_error_code()); |
| } |
| |
| ValidateCtReport(); |
| } |
| |
| // Test that a report is send when a signed exchange fails a CT check and a |
| // matching Expect-CT header was previously received. |
| IN_PROC_BROWSER_TEST_P(SignedExchangeExpectCTReportBrowserTest, |
| CrossOriginPrefetch) { |
| if (!UsePrefetch()) |
| return; |
| |
| // Hostname used to prefetch the signed exchange. The signed exchange is |
| // fetched from 127.0.0.1. |
| const char kPrefetcherHost[] = "prefetch-origin.test"; |
| |
| // Set up reports for the kPrefetcherHost's NetworkIsolationKey. This NIK |
| // should not be used by the request for the signed exchange, so use an |
| // incorrect reporting URL. This will make the test give a more useful error |
| // on failure, instead of just hanging. |
| url::Origin other_incorrect_report_origin = |
| url::Origin::Create(embedded_test_server()->GetURL(kPrefetcherHost, "/")); |
| net::NetworkIsolationKey other_incorrect_network_isolation_key( |
| other_incorrect_report_origin, other_incorrect_report_origin); |
| SetExpectCtUrl("test.example.org", other_incorrect_report_uri(), |
| other_incorrect_network_isolation_key); |
| |
| const GURL prefetch_html_url = embedded_test_server()->GetURL( |
| kPrefetcherHost, |
| std::string("/sxg/prefetch-document.html#") + sxg_url().spec()); |
| std::u16string expected_title = u"FAIL"; |
| TitleWatcher title_watcher(shell()->web_contents(), expected_title); |
| EXPECT_TRUE(NavigateToURL(shell(), prefetch_html_url)); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| |
| ValidateCtReport(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| SignedExchangeExpectCTReportBrowserTest, |
| ::testing::Combine(::testing::Bool(), |
| ::testing::Bool())); |
| |
| #if BUILDFLAG(ENABLE_REPORTING) |
| |
| class SignedExchangeReportingBrowserTest |
| : public SignedExchangeRequestHandlerBrowserTest { |
| public: |
| SignedExchangeReportingBrowserTest() { |
| feature_list_.InitWithFeatures( |
| // enabled_features |
| {net::features::kPartitionNelAndReportingByNetworkIsolationKey, |
| // These last two are not strictly necessary, but make this test more |
| // robust against enabling NetworkIsolationKeys everywhere. |
| net::features::kPartitionConnectionsByNetworkIsolationKey, |
| net::features::kPartitionSSLSessionsByNetworkIsolationKey}, |
| // disabled_features |
| {}); |
| } |
| |
| ~SignedExchangeReportingBrowserTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| SignedExchangeRequestHandlerBrowserTestBase::SetUpOnMainThread(); |
| |
| // Make all attempts to connect to the domain the SXG is for fail with a DNS |
| // error. Without this, they're fail with ERR_NOT_IMPLEMENTED. Making |
| // requests fail with ERR_NAME_NOT_RESOLVED instead better matches what |
| // happens in production. |
| host_resolver()->AddSimulatedFailure("test.example.org"); |
| |
| host_resolver()->AddRule("prefetch-origin.test", "127.0.0.1"); |
| |
| // Set up certificate for the report server. |
| net::CertVerifyResult ssl_server_result; |
| ssl_server_result.verified_cert = ssl_server_.GetCertificate(); |
| ssl_server_result.is_issued_by_known_root = false; |
| ssl_server_result.policy_compliance = |
| net::ct::CTPolicyCompliance::CT_POLICY_COMPLIES_VIA_SCTS; |
| mock_cert_verifier()->AddResultForCert(ssl_server_.GetCertificate(), |
| ssl_server_result, net::OK); |
| |
| // Make the MockCertVerifier treat the signed exchange's certificate |
| // "prime256v1-sha256.public.pem" as invalid for "test.example.org", which |
| // should generate a report. |
| scoped_refptr<net::X509Certificate> original_cert = |
| SignedExchangeBrowserTestHelper::LoadCertificate(); |
| net::CertVerifyResult dummy_result; |
| dummy_result.cert_status = net::CERT_STATUS_AUTHORITY_INVALID; |
| dummy_result.verified_cert = original_cert; |
| mock_cert_verifier()->AddResultForCertAndHost( |
| original_cert, "test.example.org", dummy_result, net::OK); |
| InstallMockCertChainInterceptor(); |
| |
| learn_report_to_response_ = |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| &ssl_server_, kLearnReportToPath); |
| report_response_ = |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| &ssl_server_, kReportPath); |
| |
| // Set up server used to serve the signed exchange. |
| ssl_server_.ServeFilesFromSourceDirectory("content/test/data"); |
| EXPECT_TRUE(ssl_server_.Start()); |
| } |
| |
| const GURL sxg_url() const { |
| return ssl_server_.GetURL("/sxg/test.example.org_test.sxg"); |
| } |
| |
| // The URL the SXG resource claims to be for. |
| static GURL sxg_validity_url() { |
| return GURL("https://test.example.org/test/"); |
| } |
| |
| protected: |
| const char* kLearnReportToPath = "/report-to"; |
| const char* kReportPath = "/report"; |
| |
| base::test::ScopedFeatureList feature_list_; |
| |
| // This server both serves the signed exchange and receives the report for the |
| // certificate error. |
| net::test_server::EmbeddedTestServer ssl_server_{ |
| net::test_server::EmbeddedTestServer::TYPE_HTTPS}; |
| |
| std::unique_ptr<net::test_server::ControllableHttpResponse> |
| learn_report_to_response_; |
| std::unique_ptr<net::test_server::ControllableHttpResponse> report_response_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SignedExchangeReportingBrowserTest, |
| CertErrorSendsReport) { |
| GURL report_url = ssl_server_.GetURL(kReportPath); |
| |
| // Get Report-To and NEL information for the server that serves the signed |
| // exchange. The same site is also used for the NetworkIsolationKey. This |
| // should result in sending a report to that server a request for that signed |
| // exchange fails with a certificate error. |
| { |
| GURL learn_report_to_url = ssl_server_.GetURL(kLearnReportToPath); |
| TestNavigationObserver same_tab_observer(shell()->web_contents(), |
| 1 /* number_of_navigations */); |
| NavigationController::LoadURLParams params(learn_report_to_url); |
| params.transition_type = ui::PageTransitionFromInt( |
| ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| shell()->web_contents()->GetController().LoadURLWithParams(params); |
| learn_report_to_response_->WaitForRequest(); |
| learn_report_to_response_->Send("HTTP/1.1 200 OK\r\n"); |
| learn_report_to_response_->Send("Report-To: {\"endpoints\":[{\"url\":\"" + |
| report_url.spec() + |
| "\"}],\"max_age\":86400}\r\n"); |
| learn_report_to_response_->Send( |
| "NEL: {\"report_to\":\"default\",\"max_age\":86400}\r\n"); |
| learn_report_to_response_->Send("\r\n"); |
| learn_report_to_response_->Done(); |
| same_tab_observer.Wait(); |
| } |
| |
| if (UsePrefetch()) { |
| // Matches MaybeTriggerPrefetchSXG(), but uses |ssl_server_| instead of |
| // embedded_test_server(). |
| const GURL prefetch_html_url = ssl_server_.GetURL( |
| std::string("/sxg/prefetch.html#") + sxg_url().spec()); |
| std::u16string expected_title = u"FAIL"; |
| TitleWatcher title_watcher(shell()->web_contents(), expected_title); |
| EXPECT_TRUE(NavigateToURL(shell(), prefetch_html_url)); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| } else { |
| // Try to navigate to the signed exchange. The signed exchange fails due |
| // to the bad certificate. That results in trying to load the |
| // resource the SXG refers to directly, which should fail with |
| // ERR_NAME_NOT_RESOLVED. |
| NavigationHandleObserver observer(shell()->web_contents(), sxg_url()); |
| EXPECT_FALSE(NavigateToURL(shell(), sxg_url())); |
| EXPECT_EQ(sxg_validity_url(), shell()->web_contents()->GetURL()); |
| EXPECT_EQ(net::ERR_NAME_NOT_RESOLVED, observer.net_error_code()); |
| } |
| |
| // Check that a report was received. |
| report_response_->WaitForRequest(); |
| EXPECT_EQ(report_url, report_response_->http_request()->GetURL()); |
| EXPECT_EQ(net::test_server::METHOD_POST, |
| report_response_->http_request()->method); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| SignedExchangeReportingBrowserTest, |
| ::testing::Combine(::testing::Bool(), |
| ::testing::Bool())); |
| |
| #endif // BUILDFLAG(ENABLE_REPORTING) |
| |
| } // namespace content |