| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/renderer_host/navigation_request.h" |
| |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/containers/flat_map.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/number_formatting.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "content/browser/renderer_host/navigation_throttle_runner.h" |
| #include "content/public/browser/navigation_throttle.h" |
| #include "content/public/browser/origin_trials_controller_delegate.h" |
| #include "content/public/browser/ssl_status.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/public/test/test_navigation_throttle.h" |
| #include "content/test/fenced_frame_test_utils.h" |
| #include "content/test/navigation_simulator_impl.h" |
| #include "content/test/test_content_browser_client.h" |
| #include "content/test/test_render_frame_host.h" |
| #include "content/test/test_web_contents.h" |
| #include "net/base/features.h" |
| #include "net/ssl/ssl_connection_status_flags.h" |
| #include "services/network/public/cpp/content_security_policy/content_security_policy.h" |
| #include "services/network/public/cpp/features.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/navigation/navigation_params.h" |
| #include "third_party/blink/public/common/navigation/navigation_params_mojom_traits.h" |
| #include "third_party/blink/public/common/origin_trials/scoped_test_origin_trial_policy.h" |
| #include "third_party/blink/public/common/runtime_feature_state/runtime_feature_state_context.h" |
| #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h" |
| |
| namespace content { |
| |
| class NavigationRequestTest : public RenderViewHostImplTestHarness { |
| public: |
| NavigationRequestTest() : callback_result_(NavigationThrottle::DEFER) {} |
| |
| void SetUp() override { |
| RenderViewHostImplTestHarness::SetUp(); |
| CreateNavigationHandle(); |
| contents()->GetPrimaryMainFrame()->InitializeRenderFrameIfNeeded(); |
| } |
| |
| void TearDown() override { RenderViewHostImplTestHarness::TearDown(); } |
| |
| void CancelDeferredNavigation( |
| NavigationThrottle::ThrottleCheckResult result) { |
| GetNavigationRequest()->CancelDeferredNavigationInternal(result); |
| } |
| |
| // Helper function to call WillStartRequest on |handle|. If this function |
| // returns DEFER, |callback_result_| will be set to the actual result of |
| // the throttle checks when they are finished. |
| void SimulateWillStartRequest() { |
| was_callback_called_ = false; |
| callback_result_ = NavigationThrottle::DEFER; |
| |
| // It's safe to use base::Unretained since the NavigationRequest is owned by |
| // the NavigationRequestTest. |
| GetNavigationRequest()->set_complete_callback_for_testing( |
| base::BindOnce(&NavigationRequestTest::UpdateThrottleCheckResult, |
| base::Unretained(this))); |
| |
| GetNavigationRequest()->WillStartRequest(); |
| } |
| |
| // Helper function to call WillRedirectRequest on |handle|. If this function |
| // returns DEFER, |callback_result_| will be set to the actual result of the |
| // throttle checks when they are finished. |
| // TODO(clamy): this should also simulate that WillStartRequest was called if |
| // it has not been called before. |
| void SimulateWillRedirectRequest() { |
| was_callback_called_ = false; |
| callback_result_ = NavigationThrottle::DEFER; |
| |
| // It's safe to use base::Unretained since the NavigationRequest is owned by |
| // the NavigationRequestTest. |
| GetNavigationRequest()->set_complete_callback_for_testing( |
| base::BindOnce(&NavigationRequestTest::UpdateThrottleCheckResult, |
| base::Unretained(this))); |
| |
| GetNavigationRequest()->WillRedirectRequest( |
| GURL(), nullptr /* post_redirect_process */); |
| } |
| |
| // Helper function to call WillFailRequest on |handle|. If this function |
| // returns DEFER, |callback_result_| will be set to the actual result of the |
| // throttle checks when they are finished. |
| void SimulateWillFailRequest( |
| net::Error net_error_code, |
| const std::optional<net::SSLInfo> ssl_info = std::nullopt) { |
| was_callback_called_ = false; |
| callback_result_ = NavigationThrottle::DEFER; |
| GetNavigationRequest()->set_net_error(net_error_code); |
| |
| // It's safe to use base::Unretained since the NavigationRequest is owned by |
| // the NavigationRequestTest. |
| GetNavigationRequest()->set_complete_callback_for_testing( |
| base::BindOnce(&NavigationRequestTest::UpdateThrottleCheckResult, |
| base::Unretained(this))); |
| |
| GetNavigationRequest()->WillFailRequest(); |
| } |
| |
| // Helper function to call WillCommitWithoutUrlLoader on |handle|. If this |
| // function returns DEFER, |callback_result_| will be set to the actual result |
| // of the throttle checks when they are finished. |
| void SimulateWillCommitWithoutUrlLoader() { |
| was_callback_called_ = false; |
| callback_result_ = NavigationThrottle::DEFER; |
| |
| // It's safe to use base::Unretained since the NavigationRequest is owned by |
| // the NavigationRequestTest. |
| GetNavigationRequest()->set_complete_callback_for_testing( |
| base::BindOnce(&NavigationRequestTest::UpdateThrottleCheckResult, |
| base::Unretained(this))); |
| |
| GetNavigationRequest()->WillCommitWithoutUrlLoader(); |
| } |
| |
| // Whether the callback was called. |
| bool was_callback_called() const { return was_callback_called_; } |
| |
| // Returns the callback_result. |
| NavigationThrottle::ThrottleCheckResult callback_result() const { |
| return callback_result_; |
| } |
| |
| NavigationRequest::NavigationState state() { |
| return GetNavigationRequest()->state(); |
| } |
| |
| bool call_counts_match(TestNavigationThrottle* throttle, |
| int start, |
| int redirect, |
| int failure, |
| int process, |
| int withoutUrlLoader) { |
| return start == throttle->GetCallCount( |
| TestNavigationThrottle::WILL_START_REQUEST) && |
| redirect == throttle->GetCallCount( |
| TestNavigationThrottle::WILL_REDIRECT_REQUEST) && |
| failure == throttle->GetCallCount( |
| TestNavigationThrottle::WILL_FAIL_REQUEST) && |
| process == throttle->GetCallCount( |
| TestNavigationThrottle::WILL_PROCESS_RESPONSE) && |
| withoutUrlLoader == |
| throttle->GetCallCount( |
| TestNavigationThrottle::WILL_COMMIT_WITHOUT_URL_LOADER); |
| } |
| |
| // Creates, register and returns a TestNavigationThrottle that will |
| // synchronously return |result| on checks by default. |
| TestNavigationThrottle* CreateTestNavigationThrottle( |
| NavigationThrottle::ThrottleCheckResult result) { |
| TestNavigationThrottle* test_throttle = new TestNavigationThrottle( |
| *GetNavigationRequest()->GetNavigationThrottleRegistryForTesting()); |
| test_throttle->SetResponseForAllMethods(TestNavigationThrottle::SYNCHRONOUS, |
| result); |
| GetNavigationRequest()->RegisterThrottleForTesting( |
| std::unique_ptr<TestNavigationThrottle>(test_throttle)); |
| return test_throttle; |
| } |
| |
| // Creates, register and returns a TestNavigationThrottle that will |
| // synchronously return |result| on check for the given |method|, and |
| // NavigationThrottle::PROCEED otherwise. |
| TestNavigationThrottle* CreateTestNavigationThrottle( |
| TestNavigationThrottle::ThrottleMethod method, |
| NavigationThrottle::ThrottleCheckResult result) { |
| TestNavigationThrottle* test_throttle = |
| CreateTestNavigationThrottle(NavigationThrottle::PROCEED); |
| test_throttle->SetResponse(method, TestNavigationThrottle::SYNCHRONOUS, |
| result); |
| return test_throttle; |
| } |
| |
| // TODO(zetamoo): Use NavigationSimulator instead of creating |
| // NavigationRequest and NavigationHandleImpl. |
| void CreateNavigationHandle() { |
| auto common_params = blink::CreateCommonNavigationParams(); |
| common_params->initiator_origin = |
| url::Origin::Create(GURL("https://initiator.example.com")); |
| auto commit_params = blink::CreateCommitNavigationParams(); |
| commit_params->frame_policy = |
| main_test_rfh()->frame_tree_node()->pending_frame_policy(); |
| auto request = NavigationRequest::CreateBrowserInitiated( |
| main_test_rfh()->frame_tree_node(), std::move(common_params), |
| std::move(commit_params), false /* was_opener_suppressed */, |
| std::string() /* extra_headers */, nullptr /* frame_entry */, |
| nullptr /* entry */, false /* is_form_submission */, |
| nullptr /* navigation_ui_data */, std::nullopt /* impression */, |
| false /* is_pdf */); |
| main_test_rfh()->frame_tree_node()->TakeNavigationRequest( |
| std::move(request)); |
| GetNavigationRequest()->StartNavigation(); |
| } |
| |
| FrameTreeNode* AddFrame(FrameTree& frame_tree, |
| RenderFrameHostImpl* parent, |
| int process_id, |
| int new_routing_id, |
| const blink::FramePolicy& frame_policy, |
| blink::FrameOwnerElementType owner_type) { |
| return frame_tree.AddFrame( |
| parent, process_id, new_routing_id, |
| TestRenderFrameHost::CreateStubFrameRemote(), |
| TestRenderFrameHost::CreateStubBrowserInterfaceBrokerReceiver(), |
| TestRenderFrameHost::CreateStubPolicyContainerBindParams(), |
| TestRenderFrameHost::CreateStubAssociatedInterfaceProviderReceiver(), |
| blink::mojom::TreeScopeType::kDocument, std::string(), "uniqueName0", |
| false, blink::LocalFrameToken(), base::UnguessableToken::Create(), |
| blink::DocumentToken(), frame_policy, |
| blink::mojom::FrameOwnerProperties(), false, owner_type, |
| /*is_dummy_frame_for_inner_tree=*/false); |
| } |
| |
| private: |
| // The callback provided to NavigationRequest::WillStartRequest, |
| // NavigationRequest::WillRedirectRequest, and |
| // NavigationRequest::WillFailRequest during the tests. |
| bool UpdateThrottleCheckResult( |
| NavigationThrottle::ThrottleCheckResult result) { |
| callback_result_ = result; |
| was_callback_called_ = true; |
| return true; |
| } |
| |
| // This must be called after CreateNavigationHandle(). |
| NavigationRequest* GetNavigationRequest() { |
| return main_test_rfh()->frame_tree_node()->navigation_request(); |
| } |
| |
| bool was_callback_called_ = false; |
| NavigationThrottle::ThrottleCheckResult callback_result_; |
| }; |
| |
| // Checks that the request_context_type is properly set. |
| // Note: can be extended to cover more internal members. |
| TEST_F(NavigationRequestTest, SimpleDataChecksRedirectAndProcess) { |
| const GURL kUrl1 = GURL("http://chromium.org"); |
| const GURL kUrl2 = GURL("http://google.com"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl1, main_rfh()); |
| navigation->Start(); |
| EXPECT_EQ(blink::mojom::RequestContextType::LOCATION, |
| NavigationRequest::From(navigation->GetNavigationHandle()) |
| ->request_context_type()); |
| EXPECT_EQ(net::HttpConnectionInfo::kUNKNOWN, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| navigation->set_http_connection_info(net::HttpConnectionInfo::kHTTP1_1); |
| navigation->Redirect(kUrl2); |
| EXPECT_EQ(blink::mojom::RequestContextType::LOCATION, |
| NavigationRequest::From(navigation->GetNavigationHandle()) |
| ->request_context_type()); |
| EXPECT_EQ(net::HttpConnectionInfo::kHTTP1_1, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| navigation->set_http_connection_info(net::HttpConnectionInfo::kQUIC_35); |
| navigation->ReadyToCommit(); |
| EXPECT_EQ(blink::mojom::RequestContextType::LOCATION, |
| NavigationRequest::From(navigation->GetNavigationHandle()) |
| ->request_context_type()); |
| EXPECT_EQ(net::HttpConnectionInfo::kQUIC_35, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| } |
| |
| TEST_F(NavigationRequestTest, SimpleDataCheckNoRedirect) { |
| const GURL kUrl = GURL("http://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh()); |
| navigation->Start(); |
| EXPECT_EQ(net::HttpConnectionInfo::kUNKNOWN, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| navigation->set_http_connection_info(net::HttpConnectionInfo::kQUIC_35); |
| navigation->ReadyToCommit(); |
| EXPECT_EQ(net::HttpConnectionInfo::kQUIC_35, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| } |
| |
| TEST_F(NavigationRequestTest, SimpleDataChecksFailure) { |
| const GURL kUrl = GURL("http://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh()); |
| navigation->Start(); |
| EXPECT_EQ(blink::mojom::RequestContextType::LOCATION, |
| NavigationRequest::From(navigation->GetNavigationHandle()) |
| ->request_context_type()); |
| EXPECT_EQ(net::HttpConnectionInfo::kUNKNOWN, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| navigation->Fail(net::ERR_CERT_DATE_INVALID); |
| EXPECT_EQ(blink::mojom::RequestContextType::LOCATION, |
| NavigationRequest::From(navigation->GetNavigationHandle()) |
| ->request_context_type()); |
| EXPECT_EQ(net::ERR_CERT_DATE_INVALID, |
| navigation->GetNavigationHandle()->GetNetErrorCode()); |
| } |
| |
| // Checks that a navigation deferred during WillStartRequest can be properly |
| // cancelled. |
| TEST_F(NavigationRequestTest, CancelDeferredWillStart) { |
| TestNavigationThrottle* test_throttle = |
| CreateTestNavigationThrottle(NavigationThrottle::DEFER); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 0)); |
| |
| // Simulate WillStartRequest. The request should be deferred. The callback |
| // should not have been called. |
| SimulateWillStartRequest(); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 0, 0, 0)); |
| |
| // Cancel the request. The callback should have been called. |
| CancelDeferredNavigation(NavigationThrottle::CANCEL_AND_IGNORE); |
| EXPECT_EQ(NavigationRequest::CANCELING, state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(NavigationThrottle::CANCEL_AND_IGNORE, callback_result()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 0, 0, 0)); |
| } |
| |
| // Checks that a navigation deferred during WillRedirectRequest can be properly |
| // cancelled. |
| TEST_F(NavigationRequestTest, CancelDeferredWillRedirect) { |
| TestNavigationThrottle* test_throttle = |
| CreateTestNavigationThrottle(NavigationThrottle::DEFER); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 0)); |
| |
| // Simulate WillRedirectRequest. The request should be deferred. The callback |
| // should not have been called. |
| SimulateWillRedirectRequest(); |
| EXPECT_EQ(NavigationRequest::WILL_REDIRECT_REQUEST, state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 1, 0, 0, 0)); |
| |
| // Cancel the request. The callback should have been called. |
| CancelDeferredNavigation(NavigationThrottle::CANCEL_AND_IGNORE); |
| EXPECT_EQ(NavigationRequest::CANCELING, state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(NavigationThrottle::CANCEL_AND_IGNORE, callback_result()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 1, 0, 0, 0)); |
| } |
| |
| // Checks that a navigation deferred during WillFailRequest can be properly |
| // cancelled. |
| TEST_F(NavigationRequestTest, CancelDeferredWillFail) { |
| TestNavigationThrottle* test_throttle = CreateTestNavigationThrottle( |
| TestNavigationThrottle::WILL_FAIL_REQUEST, NavigationThrottle::DEFER); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 0)); |
| |
| // Simulate WillStartRequest. |
| SimulateWillStartRequest(); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 0, 0, 0)); |
| |
| // Simulate WillFailRequest. The request should be deferred. The callback |
| // should not have been called. |
| SimulateWillFailRequest(net::ERR_CERT_DATE_INVALID); |
| EXPECT_EQ(NavigationRequest::WILL_FAIL_REQUEST, state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 1, 0, 0)); |
| |
| // Cancel the request. The callback should have been called. |
| CancelDeferredNavigation(NavigationThrottle::CANCEL_AND_IGNORE); |
| EXPECT_EQ(NavigationRequest::CANCELING, state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(NavigationThrottle::CANCEL_AND_IGNORE, callback_result()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 1, 0, 0)); |
| } |
| |
| // Checks that a navigation deferred can be canceled and not ignored. |
| TEST_F(NavigationRequestTest, CancelDeferredWillRedirectNoIgnore) { |
| TestNavigationThrottle* test_throttle = |
| CreateTestNavigationThrottle(NavigationThrottle::DEFER); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 0)); |
| |
| // Simulate WillStartRequest. The request should be deferred. The callback |
| // should not have been called. |
| SimulateWillStartRequest(); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 0, 0, 0)); |
| |
| // Cancel the request. The callback should have been called with CANCEL, and |
| // not CANCEL_AND_IGNORE. |
| CancelDeferredNavigation(NavigationThrottle::CANCEL); |
| EXPECT_EQ(NavigationRequest::CANCELING, state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(NavigationThrottle::CANCEL, callback_result()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 0, 0, 0)); |
| } |
| |
| // Checks that a navigation deferred by WillFailRequest can be canceled and not |
| // ignored. |
| TEST_F(NavigationRequestTest, CancelDeferredWillFailNoIgnore) { |
| TestNavigationThrottle* test_throttle = CreateTestNavigationThrottle( |
| TestNavigationThrottle::WILL_FAIL_REQUEST, NavigationThrottle::DEFER); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 0)); |
| |
| // Simulate WillStartRequest. |
| SimulateWillStartRequest(); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 0, 0, 0)); |
| |
| // Simulate WillFailRequest. The request should be deferred. The callback |
| // should not have been called. |
| SimulateWillFailRequest(net::ERR_CERT_DATE_INVALID); |
| EXPECT_EQ(NavigationRequest::WILL_FAIL_REQUEST, state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 1, 0, 0)); |
| |
| // Cancel the request. The callback should have been called with CANCEL, and |
| // not CANCEL_AND_IGNORE. |
| CancelDeferredNavigation(NavigationThrottle::CANCEL); |
| EXPECT_EQ(NavigationRequest::CANCELING, state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(NavigationThrottle::CANCEL, callback_result()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 1, 0, 1, 0, 0)); |
| } |
| |
| // Checks that a navigation deferred during WillCommitWithoutUrlLoader can be |
| // properly cancelled. |
| TEST_F(NavigationRequestTest, CancelDeferredWillCommitWithoutUrlLoader) { |
| TestNavigationThrottle* test_throttle = |
| CreateTestNavigationThrottle(NavigationThrottle::DEFER); |
| EXPECT_EQ(NavigationRequest::WILL_START_REQUEST, state()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 0)); |
| |
| // Simulate WillCommitWithoutUrlLoader. The request should be deferred. The |
| // callback should not have been called. |
| SimulateWillCommitWithoutUrlLoader(); |
| EXPECT_EQ(NavigationRequest::WILL_COMMIT_WITHOUT_URL_LOADER, state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 1)); |
| |
| // Cancel the request. The callback should have been called. |
| CancelDeferredNavigation(NavigationThrottle::CANCEL_AND_IGNORE); |
| EXPECT_EQ(NavigationRequest::CANCELING, state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(NavigationThrottle::CANCEL_AND_IGNORE, callback_result()); |
| EXPECT_TRUE(call_counts_match(test_throttle, 0, 0, 0, 0, 1)); |
| } |
| |
| // Checks that data from the SSLInfo passed into SimulateWillStartRequest() is |
| // stored on the handle. |
| TEST_F(NavigationRequestTest, WillFailRequestSetsSSLInfo) { |
| uint16_t cipher_suite = 0xc02f; // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
| int connection_status = 0; |
| net::SSLConnectionStatusSetCipherSuite(cipher_suite, &connection_status); |
| |
| // Set some test values. |
| net::SSLInfo ssl_info; |
| ssl_info.cert_status = net::CERT_STATUS_AUTHORITY_INVALID; |
| ssl_info.connection_status = connection_status; |
| |
| const GURL kUrl = GURL("https://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh()); |
| navigation->SetSSLInfo(ssl_info); |
| navigation->Fail(net::ERR_CERT_DATE_INVALID); |
| |
| EXPECT_EQ(net::CERT_STATUS_AUTHORITY_INVALID, |
| navigation->GetNavigationHandle()->GetSSLInfo()->cert_status); |
| EXPECT_EQ(connection_status, |
| navigation->GetNavigationHandle()->GetSSLInfo()->connection_status); |
| } |
| |
| TEST_F(NavigationRequestTest, SharedStorageWritable) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitWithFeatures( |
| /*enabled_features=*/{network::features::kSharedStorageAPI, |
| blink::features::kFencedFrames}, |
| /*disabled_features=*/{}); |
| |
| // Create and start a simulated `NavigationRequest` for the main frame. |
| GURL main_url = GURL("https://main.com"); |
| auto main_navigation = |
| NavigationSimulatorImpl::CreateBrowserInitiated(main_url, contents()); |
| main_navigation->Start(); |
| main_navigation->ReadyToCommit(); |
| |
| // Verify that the main frame's `NavigationRequest` will not be |
| // SharedStorageWritable. |
| ASSERT_TRUE(main_navigation->GetNavigationHandle()); |
| EXPECT_FALSE(main_navigation->GetNavigationHandle() |
| ->shared_storage_writable_eligible()); |
| |
| // Commit the navigation. |
| main_navigation->Commit(); |
| |
| // Append a child frame and set its `shared_storage_writable` attribute to |
| // true. |
| TestRenderFrameHost* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_rfh())->AppendChild("child")); |
| blink::mojom::IframeAttributesPtr child_attributes = |
| blink::mojom::IframeAttributes::New(); |
| child_attributes->shared_storage_writable_opted_in = true; |
| child_frame->frame_tree_node()->SetAttributes(std::move(child_attributes)); |
| |
| // Create and start a simulated `NavigationRequest` for the child frame. |
| GURL a_url = GURL("https://a.com"); |
| auto child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(a_url, child_frame); |
| child_navigation->Start(); |
| |
| // Verify that the `NavigationRequest` will be SharedStorageWritable. |
| ASSERT_TRUE(child_navigation->GetNavigationHandle()); |
| EXPECT_TRUE(child_navigation->GetNavigationHandle() |
| ->shared_storage_writable_eligible()); |
| |
| // Commit the navigation. |
| child_navigation->Commit(); |
| |
| // Append a fenced frame and give it permission to access Shared Storage. |
| TestRenderFrameHost* fenced_frame_root = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_rfh())->AppendFencedFrame()); |
| FrameTreeNode* fenced_frame_node = |
| static_cast<RenderFrameHostImpl*>(fenced_frame_root)->frame_tree_node(); |
| FencedFrameConfig new_config = FencedFrameConfig(GURL("about:blank")); |
| new_config.AddEffectiveEnabledPermissionForTesting( |
| network::mojom::PermissionsPolicyFeature::kSharedStorage); |
| FencedFrameProperties new_props = FencedFrameProperties(new_config); |
| fenced_frame_node->set_fenced_frame_properties(new_props); |
| fenced_frame_root->ResetPermissionsPolicy({}); |
| |
| // Append a child frame to the fenced frame root and set its |
| // `shared_storage_writable` attribute to true. |
| TestRenderFrameHost* child_of_fenced_frame = |
| static_cast<TestRenderFrameHost*>( |
| fenced_frame_root->AppendChild("child_of_fenced")); |
| blink::mojom::IframeAttributesPtr child_of_fenced_frame_attributes = |
| blink::mojom::IframeAttributes::New(); |
| child_of_fenced_frame_attributes->shared_storage_writable_opted_in = true; |
| child_of_fenced_frame->frame_tree_node()->SetAttributes( |
| std::move(child_of_fenced_frame_attributes)); |
| |
| // Create and start a simulated `NavigationRequest` for the child frame. |
| GURL b_url = GURL("https://b.com"); |
| auto child_of_fenced_frame_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(b_url, |
| child_of_fenced_frame); |
| child_of_fenced_frame_navigation->Start(); |
| |
| // Verify that the `NavigationRequest` will be SharedStorageWritable. |
| ASSERT_TRUE(child_of_fenced_frame_navigation->GetNavigationHandle()); |
| EXPECT_TRUE(child_of_fenced_frame_navigation->GetNavigationHandle() |
| ->shared_storage_writable_eligible()); |
| |
| // Commit the navigation. |
| child_of_fenced_frame_navigation->Commit(); |
| } |
| |
| namespace { |
| |
| // Helper throttle which checks that it can access NavigationHandle's |
| // RenderFrameHost in WillFailRequest() and then defers the failure. |
| class GetRenderFrameHostOnFailureNavigationThrottle |
| : public NavigationThrottle { |
| public: |
| explicit GetRenderFrameHostOnFailureNavigationThrottle( |
| NavigationThrottleRegistry& registry) |
| : NavigationThrottle(registry) {} |
| |
| GetRenderFrameHostOnFailureNavigationThrottle( |
| const GetRenderFrameHostOnFailureNavigationThrottle&) = delete; |
| GetRenderFrameHostOnFailureNavigationThrottle& operator=( |
| const GetRenderFrameHostOnFailureNavigationThrottle&) = delete; |
| |
| ~GetRenderFrameHostOnFailureNavigationThrottle() override = default; |
| |
| NavigationThrottle::ThrottleCheckResult WillFailRequest() override { |
| EXPECT_TRUE(navigation_handle()->GetRenderFrameHost()); |
| return NavigationThrottle::DEFER; |
| } |
| |
| const char* GetNameForLogging() override { |
| return "GetRenderFrameHostOnFailureNavigationThrottle"; |
| } |
| }; |
| |
| class ThrottleTestContentBrowserClient : public ContentBrowserClient { |
| void CreateThrottlesForNavigation( |
| NavigationThrottleRegistry& registry) override { |
| registry.AddThrottle( |
| std::make_unique<GetRenderFrameHostOnFailureNavigationThrottle>( |
| registry)); |
| } |
| }; |
| |
| } // namespace |
| |
| // Verify that the NavigationHandle::GetRenderFrameHost() can be retrieved by a |
| // throttle in WillFailRequest(), as well as after deferring the failure. This |
| // is allowed, since at that point the final RenderFrameHost will have already |
| // been chosen. See https://crbug.com/817881. |
| TEST_F(NavigationRequestTest, WillFailRequestCanAccessRenderFrameHost) { |
| std::unique_ptr<ContentBrowserClient> client( |
| new ThrottleTestContentBrowserClient); |
| ContentBrowserClient* old_browser_client = |
| SetBrowserClientForTesting(client.get()); |
| |
| const GURL kUrl = GURL("http://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh()); |
| navigation->SetAutoAdvance(false); |
| navigation->Start(); |
| navigation->Fail(net::ERR_CERT_DATE_INVALID); |
| EXPECT_EQ( |
| NavigationRequest::WILL_FAIL_REQUEST, |
| NavigationRequest::From(navigation->GetNavigationHandle())->state()); |
| EXPECT_TRUE(navigation->GetNavigationHandle()->GetRenderFrameHost()); |
| NavigationRequest::From(navigation->GetNavigationHandle()) |
| ->GetNavigationThrottleRegistryForTesting() |
| ->GetNavigationThrottleRunnerForTesting() |
| .CallResumeForTesting(); |
| EXPECT_TRUE(navigation->GetNavigationHandle()->GetRenderFrameHost()); |
| |
| SetBrowserClientForTesting(old_browser_client); |
| } |
| |
| TEST_F(NavigationRequestTest, PolicyContainerInheritance) { |
| struct TestCase { |
| const char* url; |
| bool expect_inherit; |
| } cases[]{{"about:blank", true}, |
| {"data:text/plain,hello", true}, |
| {"file://local", false}, |
| {"http://chromium.org", false}}; |
| |
| const GURL kUrl1 = GURL("http://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl1, main_rfh()); |
| navigation->Commit(); |
| |
| for (auto test : cases) { |
| // We navigate child frames because the BlockedSchemeNavigationThrottle |
| // restricts navigations in the main frame. |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_rfh())->AppendChild("child")); |
| |
| // We set the referrer policy of the frame to "always". We then create a new |
| // navigation, set as initiator the frame itself, start the navigation, and |
| // change the referrer policy of the frame to "never". After we commit the |
| // navigation: |
| // - If navigating to a local scheme, the target frame should have inherited |
| // the referrer policy of the initiator ("always"). |
| // - If navigating to a non-local scheme, the target frame should have a new |
| // policy container (hence referrer policy set to "default"). |
| const GURL kUrl = GURL(test.url); |
| navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, child_frame); |
| static_cast<blink::mojom::PolicyContainerHost*>( |
| child_frame->policy_container_host()) |
| ->SetReferrerPolicy(network::mojom::ReferrerPolicy::kAlways); |
| navigation->SetInitiatorFrame(child_frame); |
| navigation->Start(); |
| static_cast<blink::mojom::PolicyContainerHost*>( |
| child_frame->policy_container_host()) |
| ->SetReferrerPolicy(network::mojom::ReferrerPolicy::kNever); |
| navigation->Commit(); |
| EXPECT_EQ( |
| test.expect_inherit ? network::mojom::ReferrerPolicy::kAlways |
| : network::mojom::ReferrerPolicy::kDefault, |
| static_cast<RenderFrameHostImpl*>(navigation->GetFinalRenderFrameHost()) |
| ->policy_container_host() |
| ->referrer_policy()); |
| } |
| } |
| |
| TEST_F(NavigationRequestTest, DnsAliasesCanBeAccessed) { |
| // Create simulated NavigationRequest for the URL, which has aliases. |
| const GURL kUrl = GURL("http://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh()); |
| std::vector<std::string> dns_aliases({"alias1", "alias2"}); |
| navigation->SetResponseDnsAliases(std::move(dns_aliases)); |
| |
| // Start the navigation. |
| navigation->Start(); |
| EXPECT_EQ(net::HttpConnectionInfo::kUNKNOWN, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| // Commit the navigation. |
| navigation->set_http_connection_info(net::HttpConnectionInfo::kQUIC_35); |
| navigation->ReadyToCommit(); |
| EXPECT_EQ(net::HttpConnectionInfo::kQUIC_35, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| // Verify that the aliases are accessible from the NavigationRequest. |
| EXPECT_THAT(navigation->GetNavigationHandle()->GetDnsAliases(), |
| testing::ElementsAre("alias1", "alias2")); |
| } |
| |
| TEST_F(NavigationRequestTest, NoDnsAliases) { |
| // Create simulated NavigationRequest for the URL, which does not |
| // have aliases. (Note the empty alias list.) |
| const GURL kUrl = GURL("http://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh()); |
| std::vector<std::string> dns_aliases; |
| navigation->SetResponseDnsAliases(std::move(dns_aliases)); |
| |
| // Start the navigation. |
| navigation->Start(); |
| EXPECT_EQ(net::HttpConnectionInfo::kUNKNOWN, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| // Commit the navigation. |
| navigation->set_http_connection_info(net::HttpConnectionInfo::kQUIC_35); |
| navigation->ReadyToCommit(); |
| EXPECT_EQ(net::HttpConnectionInfo::kQUIC_35, |
| navigation->GetNavigationHandle()->GetConnectionInfo()); |
| |
| // Verify that there are no aliases in the NavigationRequest. |
| EXPECT_TRUE(navigation->GetNavigationHandle()->GetDnsAliases().empty()); |
| } |
| |
| TEST_F(NavigationRequestTest, StorageKeyToCommit) { |
| TestRenderFrameHost* child_document = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_rfh())->AppendChild("")); |
| auto attributes = child_document->frame_tree_node()->attributes_->Clone(); |
| attributes->credentialless = true; |
| child_document->frame_tree_node()->SetAttributes(std::move(attributes)); |
| |
| const GURL kUrl = GURL("http://chromium.org"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, child_document); |
| navigation->ReadyToCommit(); |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| EXPECT_TRUE(request->commit_params().storage_key.nonce().has_value()); |
| EXPECT_EQ(child_document->GetPage().credentialless_iframes_nonce(), |
| request->commit_params().storage_key.nonce().value()); |
| |
| navigation->Commit(); |
| child_document = |
| static_cast<TestRenderFrameHost*>(navigation->GetFinalRenderFrameHost()); |
| EXPECT_TRUE(child_document->IsCredentialless()); |
| EXPECT_EQ(blink::StorageKey::CreateWithNonce( |
| url::Origin::Create(kUrl), |
| child_document->GetPage().credentialless_iframes_nonce()), |
| child_document->GetStorageKey()); |
| } |
| |
| // Test that the StorageKey's value is correctly affected by the |
| // RuntimeFeatureStateContext. |
| TEST_F(NavigationRequestTest, RuntimeFeatureStateStorageKey) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| // Because the StorageKey's (and Storage Partitioning's) usage of |
| // RuntimeFeatureState is only meant to disable partitioning (i.e.: |
| // first-party only), we need the make sure the net::features is always |
| // enabled. |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| // This lambda performs the navigation and compares the commit_params' |
| // StorageKey against the passed in one. If `disable_sp` is true then it will |
| // also enable the user bypass feature in the RFSC. It returns |
| // the new TestRenderFrameHost* to the navigated frame. |
| auto NavigateAndCompareKeys = |
| [](NavigationSimulator* navigation, const blink::StorageKey& key, |
| bool disable_sp = false) -> TestRenderFrameHost* { |
| navigation->Start(); |
| |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| |
| if (disable_sp) { |
| request->GetMutableRuntimeFeatureStateContext() |
| .SetThirdPartyStoragePartitioningUserBypassEnabled(true); |
| } |
| |
| navigation->ReadyToCommit(); |
| |
| EXPECT_EQ(key, request->commit_params().storage_key); |
| |
| navigation->Commit(); |
| return static_cast<TestRenderFrameHost*>( |
| navigation->GetFinalRenderFrameHost()); |
| }; |
| |
| // Throughout the test we'll be creating a frame tree with a main frame, a |
| // child frame, and a grandchild frame. |
| GURL main_url("https://main.com"); |
| GURL b_url("https://b.com"); |
| GURL c_url("https://c.com"); |
| |
| url::Origin main_origin = url::Origin::Create(main_url); |
| url::Origin b_origin = url::Origin::Create(b_url); |
| url::Origin c_origin = url::Origin::Create(c_url); |
| |
| // Begin by testing with Storage Partitioning enabled. |
| |
| auto main_navigation = |
| NavigationSimulatorImpl::CreateBrowserInitiated(main_url, contents()); |
| |
| // By definition the main frame's StorageKey will always be first party |
| blink::StorageKey main_frame_key = |
| blink::StorageKey::CreateFirstParty(main_origin); |
| |
| NavigateAndCompareKeys(main_navigation.get(), main_frame_key); |
| |
| TestRenderFrameHost* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_rfh())->AppendChild("child")); |
| |
| auto child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(b_url, child_frame); |
| |
| // The child and grandchild should both be third-party keys. |
| blink::StorageKey child_frame_key = |
| blink::StorageKey::Create(b_origin, net::SchemefulSite(main_origin), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| |
| child_frame = NavigateAndCompareKeys(child_navigation.get(), child_frame_key); |
| |
| TestRenderFrameHost* grandchild_frame = |
| child_frame->AppendChild("grandchild"); |
| |
| auto grandchild_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(c_url, grandchild_frame); |
| |
| blink::StorageKey grandchild_frame_key = |
| blink::StorageKey::Create(c_origin, net::SchemefulSite(main_origin), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| grandchild_frame = |
| NavigateAndCompareKeys(grandchild_navigation.get(), grandchild_frame_key); |
| |
| // Only the RuntimeFeatureStateContext in the main frame's matters. So |
| // disabling Storage Partitioning in the child_frame shouldn't affect the |
| // child's or the grandchild's StorageKey. |
| child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(b_url, child_frame); |
| |
| child_frame = NavigateAndCompareKeys(child_navigation.get(), child_frame_key, |
| /*disable_sp=*/true); |
| |
| grandchild_frame = child_frame->AppendChild("grandchild"); |
| |
| grandchild_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(c_url, grandchild_frame); |
| |
| grandchild_frame = |
| NavigateAndCompareKeys(grandchild_navigation.get(), grandchild_frame_key); |
| |
| // Disabling Storage Partitioning on the main frame should cause the child's |
| // and grandchild's StorageKey to be first-party. |
| main_navigation = |
| NavigationSimulatorImpl::CreateBrowserInitiated(main_url, contents()); |
| |
| NavigateAndCompareKeys(main_navigation.get(), main_frame_key, |
| /*disable_sp=*/true); |
| |
| child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_rfh())->AppendChild("child")); |
| |
| child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(b_url, child_frame); |
| |
| // The child and grandchild should both be first-party keys. |
| blink::StorageKey child_frame_key_1p = |
| blink::StorageKey::CreateFirstParty(b_origin); |
| |
| child_frame = |
| NavigateAndCompareKeys(child_navigation.get(), child_frame_key_1p); |
| |
| grandchild_frame = child_frame->AppendChild("grandchild"); |
| |
| blink::StorageKey grandchild_frame_key_1p = |
| blink::StorageKey::CreateFirstParty(c_origin); |
| |
| grandchild_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(c_url, grandchild_frame); |
| |
| grandchild_frame = NavigateAndCompareKeys(grandchild_navigation.get(), |
| grandchild_frame_key_1p); |
| } |
| |
| TEST_F(NavigationRequestTest, |
| NavigationToCredentiallessDocumentNetworkIsolationInfo) { |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| auto attributes = child_frame->frame_tree_node()->attributes_->Clone(); |
| attributes->credentialless = true; |
| child_frame->frame_tree_node()->SetAttributes(std::move(attributes)); |
| |
| std::unique_ptr<NavigationSimulator> navigation = |
| NavigationSimulator::CreateRendererInitiated( |
| GURL("https://example.com/navigation.html"), child_frame); |
| navigation->ReadyToCommit(); |
| |
| EXPECT_EQ(main_test_rfh()->GetPage().credentialless_iframes_nonce(), |
| static_cast<NavigationRequest*>(navigation->GetNavigationHandle()) |
| ->isolation_info_for_subresources() |
| .network_isolation_key() |
| .GetNonce()); |
| EXPECT_EQ(main_test_rfh()->GetPage().credentialless_iframes_nonce(), |
| static_cast<NavigationRequest*>(navigation->GetNavigationHandle()) |
| ->GetIsolationInfo() |
| .network_isolation_key() |
| .GetNonce()); |
| } |
| |
| TEST_F(NavigationRequestTest, UpdatePrivateNetworkRequestPolicy) { |
| std::unique_ptr<NavigationSimulator> navigation = |
| NavigationSimulator::CreateRendererInitiated(GURL("https://example.com/"), |
| main_test_rfh()); |
| navigation->SetSocketAddress(net::IPEndPoint()); |
| |
| navigation->ReadyToCommit(); |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| EXPECT_FALSE(request->GetSocketAddress().address().IsValid()); |
| navigation->Commit(); |
| } |
| |
| // Test to ensure that the SanitizeRedirectsForCommit method correctly removes |
| // the query parameters parts of the URL that can contain sensitive information. |
| TEST_F(NavigationRequestTest, SanitizeRedirectsForCommit) { |
| const GURL start_url("https://a.com?param=1"); |
| const GURL url_2("https://b.com?param=2#foo"); |
| const GURL url_3("https://c.com?param=3"); |
| const GURL final_url("https://d.com?param=4"); |
| std::unique_ptr<NavigationSimulator> navigation = |
| NavigationSimulator::CreateRendererInitiated(start_url, main_test_rfh()); |
| navigation->Start(); |
| navigation->Redirect(url_2); |
| navigation->Redirect(url_3); |
| navigation->Redirect(final_url); |
| |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| auto commit_params = request->commit_params().Clone(); |
| request->SanitizeRedirectsForCommit(commit_params); |
| |
| // redirect_infos contains entries for B, C, and D, but not the starting URL. |
| // Ensure that the full URL for D is preserved. |
| EXPECT_EQ(3, commit_params->redirect_infos.size()); |
| EXPECT_EQ(GURL("https://b.com"), commit_params->redirect_infos[0].new_url); |
| EXPECT_EQ(GURL("https://c.com"), commit_params->redirect_infos[1].new_url); |
| EXPECT_EQ(final_url, commit_params->redirect_infos[2].new_url); |
| |
| // In contrast, redirects contains A, B, and C (i.e., the starting URL but not |
| // the final URL). |
| EXPECT_EQ(3, commit_params->redirects.size()); |
| EXPECT_EQ(GURL("https://a.com"), commit_params->redirects[0]); |
| EXPECT_EQ(GURL("https://b.com"), commit_params->redirects[1]); |
| EXPECT_EQ(GURL("https://c.com"), commit_params->redirects[2]); |
| } |
| |
| // Test that the required CSP of every frame is computed/inherited correctly and |
| // that the Sec-Required-CSP header is set. |
| class CSPEmbeddedEnforcementUnitTest : public NavigationRequestTest { |
| protected: |
| TestRenderFrameHost* main_rfh() { |
| return static_cast<TestRenderFrameHost*>(NavigationRequestTest::main_rfh()); |
| } |
| |
| // Simulate the |csp| attribute being set in |rfh|'s frame. Then navigate it. |
| // Returns the request's Sec-Required-CSP header. |
| std::string NavigateWithRequiredCSP(TestRenderFrameHost** rfh, |
| std::string required_csp) { |
| TestRenderFrameHost* document = *rfh; |
| |
| if (!required_csp.empty()) { |
| auto headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK"); |
| headers->SetHeader("Content-Security-Policy", required_csp); |
| std::vector<network::mojom::ContentSecurityPolicyPtr> policies; |
| network::AddContentSecurityPolicyFromHeaders( |
| *headers, GURL("https://example.com/"), &policies); |
| auto attributes = document->frame_tree_node()->attributes_->Clone(); |
| // Set csp value. |
| attributes->parsed_csp_attribute = std::move(policies[0]); |
| document->frame_tree_node()->SetAttributes(std::move(attributes)); |
| } |
| |
| // Chrome blocks a document navigating to a URL if more than one of its |
| // ancestors have the same URL. Use a different URL every time, to |
| // avoid blocking navigation of the grandchild frame. |
| static int nonce = 0; |
| GURL url("https://www.example.com" + base::NumberToString(nonce++)); |
| |
| auto navigation = |
| content::NavigationSimulator::CreateRendererInitiated(url, *rfh); |
| navigation->Start(); |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| std::string sec_required_csp = request->GetRequestHeaders() |
| .GetHeader("sec-required-csp") |
| .value_or(std::string()); |
| |
| // Complete the navigation so that the required csp is stored in the |
| // RenderFrameHost, so that when we will add children to this document they |
| // will be able to get the parent's required csp (and hence also test that |
| // the whole logic works). |
| auto response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK"); |
| response_headers->SetHeader("Allow-CSP-From", "*"); |
| navigation->SetResponseHeaders(response_headers); |
| navigation->Commit(); |
| |
| *rfh = static_cast<TestRenderFrameHost*>( |
| navigation->GetFinalRenderFrameHost()); |
| |
| return sec_required_csp; |
| } |
| |
| TestRenderFrameHost* AddChild(TestRenderFrameHost* parent) { |
| return static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(parent)->AppendChild("")); |
| } |
| }; |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, TopLevel) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| std::string sec_required_csp = NavigateWithRequiredCSP(&top_document, ""); |
| EXPECT_EQ("", sec_required_csp); |
| EXPECT_FALSE(top_document->required_csp()); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, ChildNoCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| std::string sec_required_csp = NavigateWithRequiredCSP(&child_document, ""); |
| EXPECT_EQ("", sec_required_csp); |
| EXPECT_FALSE(child_document->required_csp()); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, ChildWithCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| std::string sec_required_csp = |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(child_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| child_document->required_csp()->header->header_value); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, ChildSiblingNoCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* sibling_document = AddChild(top_document); |
| std::string sec_required_csp = NavigateWithRequiredCSP(&sibling_document, ""); |
| EXPECT_FALSE(sibling_document->required_csp()); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, ChildSiblingCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* sibling_document = AddChild(top_document); |
| std::string sec_required_csp = |
| NavigateWithRequiredCSP(&sibling_document, "script-src 'none'"); |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(sibling_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| sibling_document->required_csp()->header->header_value); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, GrandChildNoCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* grand_child_document = AddChild(child_document); |
| std::string sec_required_csp = |
| NavigateWithRequiredCSP(&grand_child_document, ""); |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(grand_child_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| grand_child_document->required_csp()->header->header_value); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, GrandChildSameCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* grand_child_document = AddChild(child_document); |
| std::string sec_required_csp = |
| NavigateWithRequiredCSP(&grand_child_document, "script-src 'none'"); |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(grand_child_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| grand_child_document->required_csp()->header->header_value); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, GrandChildDifferentCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* grand_child_document = AddChild(child_document); |
| std::string sec_required_csp = |
| NavigateWithRequiredCSP(&grand_child_document, "img-src 'none'"); |
| |
| // This seems weird, but it is the intended behaviour according to the spec. |
| // The problem is that "script-src 'none'" does not subsume "img-src 'none'", |
| // so "img-src 'none'" on the grandchild is an invalid csp attribute, and we |
| // just discard it in favour of the parent's csp attribute. |
| // |
| // This should probably be fixed in the specification: |
| // https://github.com/w3c/webappsec-cspee/pull/11 |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(grand_child_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| grand_child_document->required_csp()->header->header_value); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, InvalidCSP) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| std::string sec_required_csp = |
| NavigateWithRequiredCSP(&child_document, "report-to group"); |
| EXPECT_EQ("", sec_required_csp); |
| EXPECT_FALSE(child_document->required_csp()); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, InvalidCspAndInheritFromParent) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* grand_child_document = AddChild(child_document); |
| std::string sec_required_csp = |
| NavigateWithRequiredCSP(&grand_child_document, "report-to group"); |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(grand_child_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| grand_child_document->required_csp()->header->header_value); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, |
| SemiInvalidCspAndInheritSameCspFromParent) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* grand_child_document = AddChild(child_document); |
| std::string sec_required_csp = NavigateWithRequiredCSP( |
| &grand_child_document, "script-src 'none'; report-to group"); |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(grand_child_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| grand_child_document->required_csp()->header->header_value); |
| } |
| |
| TEST_F(CSPEmbeddedEnforcementUnitTest, |
| SemiInvalidCspAndInheritDifferentCspFromParent) { |
| TestRenderFrameHost* top_document = main_rfh(); |
| TestRenderFrameHost* child_document = AddChild(top_document); |
| NavigateWithRequiredCSP(&child_document, "script-src 'none'"); |
| TestRenderFrameHost* grand_child_document = AddChild(child_document); |
| std::string sec_required_csp = NavigateWithRequiredCSP( |
| &grand_child_document, "sandbox; report-to group"); |
| EXPECT_EQ("script-src 'none'", sec_required_csp); |
| EXPECT_TRUE(grand_child_document->required_csp()); |
| EXPECT_EQ("script-src 'none'", |
| grand_child_document->required_csp()->header->header_value); |
| } |
| |
| namespace { |
| |
| // Mock that allows us to avoid depending on the origin_trials component. |
| class OriginTrialsControllerDelegateMock |
| : public OriginTrialsControllerDelegate { |
| public: |
| ~OriginTrialsControllerDelegateMock() override = default; |
| |
| void PersistTrialsFromTokens( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| const base::span<const std::string> header_tokens, |
| const base::Time current_time, |
| std::optional<ukm::SourceId> source_id) override { |
| persisted_tokens_[origin] = |
| std::vector<std::string>(header_tokens.begin(), header_tokens.end()); |
| } |
| void PersistAdditionalTrialsFromTokens( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| const base::span<const url::Origin> script_origins, |
| const base::span<const std::string> header_tokens, |
| const base::Time current_time, |
| std::optional<ukm::SourceId> source_id) override { |
| NOTREACHED() << "not used by test"; |
| } |
| bool IsFeaturePersistedForOrigin(const url::Origin& origin, |
| const url::Origin& partition_origin, |
| blink::mojom::OriginTrialFeature feature, |
| const base::Time current_time) override { |
| DCHECK(false) << "Method not implemented for test."; |
| return false; |
| } |
| |
| base::flat_set<std::string> GetPersistedTrialsForOrigin( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| base::Time current_time) override { |
| DCHECK(false) << "Method not implemented for test."; |
| return base::flat_set<std::string>(); |
| } |
| |
| void ClearPersistedTokens() override { persisted_tokens_.clear(); } |
| |
| base::flat_map<url::Origin, std::vector<std::string>> persisted_tokens_; |
| }; |
| |
| } // namespace |
| |
| class PersistentOriginTrialNavigationRequestTest |
| : public NavigationRequestTest { |
| public: |
| PersistentOriginTrialNavigationRequestTest() |
| : delegate_mock_(std::make_unique<OriginTrialsControllerDelegateMock>()) { |
| } |
| ~PersistentOriginTrialNavigationRequestTest() override = default; |
| |
| std::vector<std::string> GetPersistedTokens(const url::Origin& origin) { |
| return delegate_mock_->persisted_tokens_[origin]; |
| } |
| |
| protected: |
| std::unique_ptr<BrowserContext> CreateBrowserContext() override { |
| std::unique_ptr<TestBrowserContext> context = |
| std::make_unique<TestBrowserContext>(); |
| context->SetOriginTrialsControllerDelegate(delegate_mock_.get()); |
| return context; |
| } |
| |
| private: |
| std::unique_ptr<OriginTrialsControllerDelegateMock> delegate_mock_; |
| }; |
| |
| // Ensure that navigations with a valid Origin-Trial header with a persistent |
| // origin trial token results in the trial being marked as enabled. |
| // Then check that subsequent navigations without headers trigger an update |
| // that clears out stored trials. |
| TEST_F(PersistentOriginTrialNavigationRequestTest, |
| NavigationCommitsPersistentOriginTrials) { |
| // Generated with: |
| // tools/origin_trials/generate_token.py https://example.com |
| // FrobulatePersistent |
| // --expire-timestamp=2000000000 |
| const char kPersistentOriginTrialToken[] = |
| "AzZfd1vKZ0SSGRGk/" |
| "8nIszQSlHYjbuYVE3jwaNZG3X4t11zRhzPWWJwTZ+JJDS3JJsyEZcpz+y20pAP6/" |
| "6upOQ4AAABdeyJvcmlnaW4iOiAiaHR0cHM6Ly9leGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZ" |
| "SI" |
| "6ICJGcm9idWxhdGVQZXJzaXN0ZW50IiwgImV4cGlyeSI6IDIwMDAwMDAwMDB9"; |
| |
| blink::ScopedTestOriginTrialPolicy origin_trial_policy_; |
| |
| const GURL kUrl = GURL("https://example.com"); |
| auto navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh()); |
| |
| auto response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK"); |
| response_headers->SetHeader("Origin-Trial", kPersistentOriginTrialToken); |
| navigation->SetResponseHeaders(response_headers); |
| |
| navigation->Commit(); |
| |
| url::Origin origin = url::Origin::Create(kUrl); |
| EXPECT_EQ(std::vector<std::string>{kPersistentOriginTrialToken}, |
| GetPersistedTokens(origin)); |
| |
| // Navigate again without response headers to assert the trial information is |
| // still updated and cleared. |
| NavigationSimulatorImpl::CreateRendererInitiated(kUrl, main_rfh())->Commit(); |
| EXPECT_EQ(std::vector<std::string>(), GetPersistedTokens(origin)); |
| } |
| |
| namespace { |
| |
| // Test version of a NavigationThrottle that requests the response body. |
| class ResponseBodyNavigationThrottle : public NavigationThrottle { |
| public: |
| using ResponseBodyCallback = base::OnceCallback<void(const std::string&)>; |
| |
| ResponseBodyNavigationThrottle(NavigationThrottleRegistry& registry, |
| ResponseBodyCallback callback) |
| : NavigationThrottle(registry), callback_(std::move(callback)) {} |
| ResponseBodyNavigationThrottle(const ResponseBodyNavigationThrottle&) = |
| delete; |
| ResponseBodyNavigationThrottle& operator=( |
| const ResponseBodyNavigationThrottle&) = delete; |
| ~ResponseBodyNavigationThrottle() override = default; |
| |
| NavigationThrottle::ThrottleCheckResult WillProcessResponse() override { |
| navigation_handle()->GetResponseBody( |
| base::BindOnce(&ResponseBodyNavigationThrottle::OnResponseBodyReady, |
| base::Unretained(this))); |
| return NavigationThrottle::DEFER; |
| } |
| |
| const char* GetNameForLogging() override { |
| return "ResponseBodyNavigationThrottle"; |
| } |
| |
| private: |
| void OnResponseBodyReady(const std::string& response_body) { |
| std::move(callback_).Run(response_body); |
| NavigationRequest::From(navigation_handle()) |
| ->GetNavigationThrottleRegistryForTesting() |
| ->GetNavigationThrottleRunnerForTesting() |
| .CallResumeForTesting(); |
| } |
| |
| ResponseBodyCallback callback_; |
| }; |
| |
| } // namespace |
| |
| // Tests response body. |
| class NavigationRequestResponseBodyTest : public NavigationRequestTest { |
| public: |
| std::unique_ptr<NavigationSimulator> CreateNavigationSimulator() { |
| auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( |
| GURL("http://example.test"), main_rfh()); |
| navigation->SetAutoAdvance(false); |
| navigation->Start(); |
| // It is safe to use base::Unretained as the NavigationThrottle will not be |
| // destroyed before the callback is called. |
| auto& registry = navigation->GetNavigationThrottleRegistry(); |
| auto throttle = std::make_unique<ResponseBodyNavigationThrottle>( |
| registry, |
| base::BindOnce(&NavigationRequestResponseBodyTest::UpdateResponseBody, |
| base::Unretained(this))); |
| registry.AddThrottle(std::move(throttle)); |
| return navigation; |
| } |
| |
| void UpdateResponseBody(const std::string& response_body) { |
| response_body_ = response_body; |
| was_callback_called_ = true; |
| } |
| |
| bool was_callback_called() const { return was_callback_called_; } |
| |
| const std::string& response_body() const { return response_body_; } |
| |
| protected: |
| mojo::ScopedDataPipeProducerHandle producer_handle_; |
| mojo::ScopedDataPipeConsumerHandle consumer_handle_; |
| |
| private: |
| bool was_callback_called_ = false; |
| std::string response_body_; |
| }; |
| |
| TEST_F(NavigationRequestResponseBodyTest, Received) { |
| auto navigation = CreateNavigationSimulator(); |
| std::string response = "response-body-content"; |
| ASSERT_EQ(MOJO_RESULT_OK, |
| mojo::CreateDataPipe(response.size(), producer_handle_, |
| consumer_handle_)); |
| navigation->SetResponseBody(std::move(consumer_handle_)); |
| |
| navigation->ReadyToCommit(); |
| EXPECT_EQ( |
| NavigationRequest::WILL_PROCESS_RESPONSE, |
| NavigationRequest::From(navigation->GetNavigationHandle())->state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_EQ(std::string(), response_body()); |
| |
| size_t actually_written_bytes = 0; |
| ASSERT_EQ(MOJO_RESULT_OK, |
| producer_handle_->WriteData(base::as_byte_span(response), |
| MOJO_WRITE_DATA_FLAG_NONE, |
| actually_written_bytes)); |
| EXPECT_EQ(actually_written_bytes, response.size()); |
| |
| navigation->Wait(); |
| EXPECT_EQ( |
| NavigationRequest::READY_TO_COMMIT, |
| NavigationRequest::From(navigation->GetNavigationHandle())->state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(response, response_body()); |
| } |
| |
| TEST_F(NavigationRequestResponseBodyTest, PartiallyReceived) { |
| auto navigation = CreateNavigationSimulator(); |
| |
| // The data pipe size is smaller than the response body size. |
| uint32_t pipe_size = 8u; |
| ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(pipe_size, producer_handle_, |
| consumer_handle_)); |
| navigation->SetResponseBody(std::move(consumer_handle_)); |
| |
| navigation->ReadyToCommit(); |
| EXPECT_EQ( |
| NavigationRequest::WILL_PROCESS_RESPONSE, |
| NavigationRequest::From(navigation->GetNavigationHandle())->state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_EQ(std::string(), response_body()); |
| |
| std::string response = "response-body-content"; |
| size_t actually_written_bytes = 0; |
| ASSERT_EQ(MOJO_RESULT_OK, |
| producer_handle_->WriteData(base::as_byte_span(response), |
| MOJO_WRITE_DATA_FLAG_NONE, |
| actually_written_bytes)); |
| EXPECT_EQ(actually_written_bytes, pipe_size); |
| |
| navigation->Wait(); |
| EXPECT_EQ( |
| NavigationRequest::READY_TO_COMMIT, |
| NavigationRequest::From(navigation->GetNavigationHandle())->state()); |
| EXPECT_TRUE(was_callback_called()); |
| // Only the first part of the response body that fits in the pipe is received. |
| EXPECT_EQ("response", response_body()); |
| } |
| |
| TEST_F(NavigationRequestResponseBodyTest, PipeClosed) { |
| auto navigation = CreateNavigationSimulator(); |
| ASSERT_EQ(MOJO_RESULT_OK, |
| mojo::CreateDataPipe(10u, producer_handle_, consumer_handle_)); |
| navigation->SetResponseBody(std::move(consumer_handle_)); |
| navigation->ReadyToCommit(); |
| EXPECT_EQ( |
| NavigationRequest::WILL_PROCESS_RESPONSE, |
| NavigationRequest::From(navigation->GetNavigationHandle())->state()); |
| EXPECT_FALSE(was_callback_called()); |
| EXPECT_EQ(std::string(), response_body()); |
| |
| // Close the pipe before any data is sent. |
| producer_handle_.reset(); |
| navigation->Wait(); |
| EXPECT_EQ( |
| NavigationRequest::READY_TO_COMMIT, |
| NavigationRequest::From(navigation->GetNavigationHandle())->state()); |
| EXPECT_TRUE(was_callback_called()); |
| EXPECT_EQ(std::string(), response_body()); |
| } |
| |
| } // namespace content |