| // Copyright 2018 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/client_hints/client_hints.h" |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/time/time.h" |
| #include "content/public/test/mock_client_hints_controller_delegate.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/test/test_render_frame_host.h" |
| #include "content/test/test_render_view_host.h" |
| #include "content/test/test_web_contents.h" |
| #include "net/http/http_response_headers.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "services/network/public/cpp/client_hints.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "third_party/blink/public/common/origin_trials/origin_trial_policy.h" |
| #include "third_party/blink/public/common/origin_trials/origin_trial_public_key.h" |
| #include "third_party/blink/public/common/origin_trials/trial_token_validator.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| using ClientHintsVector = std::vector<network::mojom::WebClientHintsType>; |
| using network::mojom::WebClientHintsType; |
| |
| static const blink::OriginTrialPublicKey kTestPublicKey = { |
| 0x75, 0x10, 0xac, 0xf9, 0x3a, 0x1c, 0xb8, 0xa9, 0x28, 0x70, 0xd2, |
| 0x9a, 0xd0, 0x0b, 0x59, 0xe1, 0xac, 0x2b, 0xb7, 0xd5, 0xca, 0x1f, |
| 0x64, 0x90, 0x08, 0x8e, 0xa8, 0xe0, 0x56, 0x3a, 0x04, 0xd0, |
| }; |
| |
| } // namespace |
| |
| class TestOriginTrialPolicy : public blink::OriginTrialPolicy { |
| public: |
| bool IsOriginTrialsSupported() const override { return true; } |
| bool IsOriginSecure(const GURL& url) const override { |
| return network::IsUrlPotentiallyTrustworthy(url); |
| } |
| const std::vector<blink::OriginTrialPublicKey>& GetPublicKeys() |
| const override { |
| return keys_; |
| } |
| void SetPublicKeys(const std::vector<blink::OriginTrialPublicKey>& keys) { |
| keys_ = keys; |
| } |
| |
| private: |
| std::vector<blink::OriginTrialPublicKey> keys_; |
| }; |
| |
| class ClientHintsTest : public RenderViewHostImplTestHarness { |
| public: |
| ClientHintsTest() { |
| blink::TrialTokenValidator::SetOriginTrialPolicyGetter(base::BindRepeating( |
| [](blink::OriginTrialPolicy* policy) { return policy; }, |
| base::Unretained(&policy_))); |
| policy_.SetPublicKeys({kTestPublicKey}); |
| } |
| ClientHintsTest(const ClientHintsTest&) = delete; |
| ClientHintsTest& operator=(const ClientHintsTest&) = delete; |
| ~ClientHintsTest() override { |
| blink::TrialTokenValidator::ResetOriginTrialPolicyGetter(); |
| } |
| |
| static constexpr char kOriginUrl[] = "https://example.com"; |
| // generate_token.py https://example.com SendFullUserAgentAfterReduction |
| // --expire-timestamp=2000000000 |
| static constexpr char kValidOriginTrialToken[] = |
| "AzA4UoK24p1LV/y6B032+L/M50GZfI4zx0aTri3ZGJpiq/" |
| "o4eLMdErZ9p4YzbsCY9nrjmccZe12bs80EON8/" |
| "eAYAAABpeyJvcmlnaW4iOiAiaHR0cHM6Ly9leGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6" |
| "ICJTZW5kRnVsbFVzZXJBZ2VudEFmdGVyUmVkdWN0aW9uIiwgImV4cGlyeSI6IDIwMDAwMDAw" |
| "MDB9"; |
| |
| void AddOneChildNode() { |
| main_test_rfh()->OnCreateChildFrame( |
| /*new_routing_id=*/14, TestRenderFrameHost::CreateStubFrameRemote(), |
| TestRenderFrameHost::CreateStubBrowserInterfaceBrokerReceiver(), |
| TestRenderFrameHost::CreateStubPolicyContainerBindParams(), |
| TestRenderFrameHost::CreateStubAssociatedInterfaceProviderReceiver(), |
| /*scope=*/blink::mojom::TreeScopeType::kDocument, /*frame_name=*/"", |
| /*frame_unique_name=*/"uniqueName0", /*is_created_by_script=*/false, |
| /*frame_token=*/blink::LocalFrameToken(), |
| /*devtools_frame_token=*/base::UnguessableToken::Create(), |
| /*document_token=*/blink::DocumentToken(), |
| /*frame_policy=*/blink::FramePolicy(), |
| /*frame_owner_properties=*/blink::mojom::FrameOwnerProperties(), |
| /*owner_type=*/blink::FrameOwnerElementType::kIframe, |
| /*document_ukm_source_id=*/ukm::kInvalidSourceId); |
| } |
| |
| absl::optional<ClientHintsVector> ParseAndPersist( |
| const GURL& url, |
| const net::HttpResponseHeaders* response_header, |
| const std::string& accept_ch_str, |
| FrameTreeNode* frame_tree_node, |
| MockClientHintsControllerDelegate* delegate) { |
| auto parsed_headers = network::mojom::ParsedHeaders::New(); |
| parsed_headers->accept_ch = network::ParseClientHintsHeader(accept_ch_str); |
| |
| return ParseAndPersistAcceptCHForNavigation( |
| url::Origin::Create(url), parsed_headers, response_header, |
| browser_context(), delegate, frame_tree_node); |
| } |
| |
| std::string HintsToString(absl::optional<ClientHintsVector> hints) { |
| if (!hints) |
| return ""; |
| |
| std::vector<std::string> hints_list; |
| std::transform(hints.value().begin(), hints.value().end(), |
| std::back_inserter(hints_list), |
| [](network::mojom::WebClientHintsType hint) { |
| return network::GetClientHintToNameMap().at(hint); |
| }); |
| |
| return base::JoinString(hints_list, ","); |
| } |
| |
| std::pair<std::string, ClientHintsVector> GetAllNonOriginTrialHints() { |
| std::vector<std::string> accept_ch_tokens; |
| ClientHintsVector hints_list; |
| for (const auto& pair : network::GetClientHintToNameMap()) { |
| // Skip client hints used to origin trial. |
| if (pair.first == WebClientHintsType::kFullUserAgent || |
| pair.first == WebClientHintsType::kUAReduced) |
| continue; |
| hints_list.push_back(pair.first); |
| accept_ch_tokens.push_back(pair.second); |
| } |
| return {base::JoinString(accept_ch_tokens, ","), hints_list}; |
| } |
| |
| private: |
| TestOriginTrialPolicy policy_; |
| }; |
| |
| TEST_F(ClientHintsTest, RttRoundedOff) { |
| EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(1023)) % 50); |
| EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(6787)) % 50); |
| EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(12)) % 50); |
| EXPECT_EQ(0u, RoundRttForTesting("foo.com", base::Milliseconds(1023)) % 50); |
| EXPECT_EQ(0u, RoundRttForTesting("foo.com", base::Milliseconds(1193)) % 50); |
| EXPECT_EQ(0u, RoundRttForTesting("foo.com", base::Milliseconds(12)) % 50); |
| } |
| |
| TEST_F(ClientHintsTest, DownlinkRoundedOff) { |
| EXPECT_GE(1, |
| static_cast<int>(RoundKbpsToMbpsForTesting("", 102) * 1000) % 50); |
| EXPECT_GE(1, static_cast<int>(RoundKbpsToMbpsForTesting("", 12) * 1000) % 50); |
| EXPECT_GE(1, |
| static_cast<int>(RoundKbpsToMbpsForTesting("", 2102) * 1000) % 50); |
| |
| EXPECT_GE( |
| 1, |
| static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 102) * 1000) % 50); |
| EXPECT_GE( |
| 1, |
| static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 12) * 1000) % 50); |
| EXPECT_GE( |
| 1, |
| static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 2102) * 1000) % 50); |
| EXPECT_GE( |
| 1, static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 12102) * 1000) % |
| 50); |
| } |
| |
| // Verify that the value of RTT after adding noise is within approximately 10% |
| // of the original value. Note that the difference between the final value of |
| // RTT and the original value may be slightly more than 10% due to rounding off. |
| // To handle that, the maximum absolute difference allowed is set to a value |
| // slightly larger than 10% of the original metric value. |
| TEST_F(ClientHintsTest, FinalRttWithin10PercentValue) { |
| EXPECT_NEAR(98, RoundRttForTesting("", base::Milliseconds(98)), 100); |
| EXPECT_NEAR(1023, RoundRttForTesting("", base::Milliseconds(1023)), 200); |
| EXPECT_NEAR(1193, RoundRttForTesting("", base::Milliseconds(1193)), 200); |
| EXPECT_NEAR(2750, RoundRttForTesting("", base::Milliseconds(2750)), 400); |
| } |
| |
| // Verify that the value of downlink after adding noise is within approximately |
| // 10% of the original value. Note that the difference between the final value |
| // of downlink and the original value may be slightly more than 10% due to |
| // rounding off. To handle that, the maximum absolute difference allowed is set |
| // to a value slightly larger than 10% of the original metric value. |
| TEST_F(ClientHintsTest, FinalDownlinkWithin10PercentValue) { |
| EXPECT_NEAR(0.098, RoundKbpsToMbpsForTesting("", 98), 0.1); |
| EXPECT_NEAR(1.023, RoundKbpsToMbpsForTesting("", 1023), 0.2); |
| EXPECT_NEAR(1.193, RoundKbpsToMbpsForTesting("", 1193), 0.2); |
| EXPECT_NEAR(7.523, RoundKbpsToMbpsForTesting("", 7523), 0.9); |
| EXPECT_NEAR(9.999, RoundKbpsToMbpsForTesting("", 9999), 1.2); |
| } |
| |
| TEST_F(ClientHintsTest, RttMaxValue) { |
| EXPECT_GE(3000u, RoundRttForTesting("", base::Milliseconds(1023))); |
| EXPECT_GE(3000u, RoundRttForTesting("", base::Milliseconds(2789))); |
| EXPECT_GE(3000u, RoundRttForTesting("", base::Milliseconds(6023))); |
| EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(1023)) % 50); |
| EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(2789)) % 50); |
| EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(6023)) % 50); |
| } |
| |
| TEST_F(ClientHintsTest, DownlinkMaxValue) { |
| EXPECT_GE(10.0, RoundKbpsToMbpsForTesting("", 102)); |
| EXPECT_GE(10.0, RoundKbpsToMbpsForTesting("", 2102)); |
| EXPECT_GE(10.0, RoundKbpsToMbpsForTesting("", 100102)); |
| EXPECT_GE(1, |
| static_cast<int>(RoundKbpsToMbpsForTesting("", 102) * 1000) % 50); |
| EXPECT_GE(1, |
| static_cast<int>(RoundKbpsToMbpsForTesting("", 2102) * 1000) % 50); |
| EXPECT_GE( |
| 1, static_cast<int>(RoundKbpsToMbpsForTesting("", 100102) * 1000) % 50); |
| } |
| |
| TEST_F(ClientHintsTest, RttRandomized) { |
| const int initial_value = |
| RoundRttForTesting("example.com", base::Milliseconds(1023)); |
| bool network_quality_randomized_by_host = false; |
| // There is a 1/20 chance that the same random noise is selected for two |
| // different hosts. Run this test across 20 hosts to reduce the chances of |
| // test failing to (1/20)^20. |
| for (size_t i = 0; i < 20; ++i) { |
| int value = |
| RoundRttForTesting(base::NumberToString(i), base::Milliseconds(1023)); |
| // If |value| is different than |initial_value|, it implies that RTT is |
| // randomized by host. This verifies the behavior, and test can be ended. |
| if (value != initial_value) |
| network_quality_randomized_by_host = true; |
| } |
| EXPECT_TRUE(network_quality_randomized_by_host); |
| |
| // Calling RoundRttForTesting for same host should return the same result. |
| for (size_t i = 0; i < 20; ++i) { |
| int value = RoundRttForTesting("example.com", base::Milliseconds(1023)); |
| EXPECT_EQ(initial_value, value); |
| } |
| } |
| |
| TEST_F(ClientHintsTest, DownlinkRandomized) { |
| const int initial_value = RoundKbpsToMbpsForTesting("example.com", 1023); |
| bool network_quality_randomized_by_host = false; |
| // There is a 1/20 chance that the same random noise is selected for two |
| // different hosts. Run this test across 20 hosts to reduce the chances of |
| // test failing to (1/20)^20. |
| for (size_t i = 0; i < 20; ++i) { |
| int value = RoundKbpsToMbpsForTesting(base::NumberToString(i), 1023); |
| // If |value| is different than |initial_value|, it implies that downlink is |
| // randomized by host. This verifies the behavior, and test can be ended. |
| if (value != initial_value) |
| network_quality_randomized_by_host = true; |
| } |
| EXPECT_TRUE(network_quality_randomized_by_host); |
| |
| // Calling RoundMbps for same host should return the same result. |
| for (size_t i = 0; i < 20; ++i) { |
| int value = RoundKbpsToMbpsForTesting("example.com", 1023); |
| EXPECT_EQ(initial_value, value); |
| } |
| } |
| |
| TEST_F(ClientHintsTest, IntegrationTestsOnParseLookUp) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures({blink::features::kUserAgentClientHint}, |
| {}); |
| |
| GURL url = GURL(ClientHintsTest::kOriginUrl); |
| contents()->NavigateAndCommit(url); |
| FrameTree& frame_tree = contents()->GetPrimaryFrameTree(); |
| FrameTreeNode* main_frame_node = frame_tree.root(); |
| AddOneChildNode(); |
| FrameTreeNode* sub_frame_node = main_frame_node->child_at(0); |
| |
| blink::UserAgentMetadata ua_metadata; |
| MockClientHintsControllerDelegate delegate(ua_metadata); |
| |
| const auto& all_non_origin_trial_hints_pair = GetAllNonOriginTrialHints(); |
| const struct { |
| std::string description; |
| absl::optional<std::string> origin_trial_token; |
| std::string accept_ch_str; |
| raw_ptr<FrameTreeNode> frame_tree_node; |
| absl::optional<ClientHintsVector> expect_hints; |
| ClientHintsVector expect_commit_hints; |
| } tests[] = { |
| {"Persist hints for main frame", absl::nullopt, |
| "sec-ch-ua-platform, sec-ch-ua-bitness", main_frame_node, |
| absl::make_optional(ClientHintsVector{WebClientHintsType::kUAPlatform, |
| WebClientHintsType::kUABitness}), |
| ClientHintsVector{WebClientHintsType::kUAPlatform, |
| WebClientHintsType::kUABitness}}, |
| {"No persist hints for sub frame", absl::nullopt, |
| "sec-ch-ua-platform, sec-ch-ua-bitness", sub_frame_node, absl::nullopt, |
| ClientHintsVector{WebClientHintsType::kUAPlatform, |
| WebClientHintsType::kUABitness}}, |
| {"Origin trial hint for main frame", |
| absl::nullopt, |
| "sec-ch-ua-full", |
| main_frame_node, |
| absl::make_optional(ClientHintsVector{}), |
| {}}, |
| {"Origin trial hint for sub frame", |
| absl::nullopt, |
| "sec-ch-ua-full", |
| sub_frame_node, |
| absl::make_optional(ClientHintsVector{}), |
| {}}, |
| {"Response with valid origin trial for main frame", |
| kValidOriginTrialToken, |
| "sec-ch-ua-full", |
| main_frame_node, |
| absl::make_optional( |
| ClientHintsVector{WebClientHintsType::kFullUserAgent}), |
| {WebClientHintsType::kFullUserAgent}}, |
| {"Response with valid origin trial for sub frame", |
| kValidOriginTrialToken, |
| "sec-ch-ua-full", |
| sub_frame_node, |
| absl::make_optional( |
| ClientHintsVector{WebClientHintsType::kFullUserAgent}), |
| {WebClientHintsType::kFullUserAgent}}, |
| {"All non origin trial client hints for main frame", absl::nullopt, |
| all_non_origin_trial_hints_pair.first, main_frame_node, |
| absl::make_optional(all_non_origin_trial_hints_pair.second), |
| all_non_origin_trial_hints_pair.second}, |
| {"All non origin trial client hints for sub frame", absl::nullopt, |
| all_non_origin_trial_hints_pair.first, sub_frame_node, absl::nullopt, |
| all_non_origin_trial_hints_pair.second}, |
| }; |
| |
| for (const auto& test : tests) { |
| auto response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\n"); |
| |
| if (test.origin_trial_token) |
| response_headers->SetHeader("Origin-Trial", |
| test.origin_trial_token.value()); |
| |
| auto actual_hints = |
| ParseAndPersist(url, response_headers.get(), test.accept_ch_str, |
| test.frame_tree_node, &delegate); |
| EXPECT_EQ(test.expect_hints, actual_hints) |
| << "Test case [" << test.description << "]: expected hints " |
| << HintsToString(test.expect_hints) << " but got " |
| << HintsToString(actual_hints) << "."; |
| |
| // Verify commit hints. |
| ClientHintsVector actual_commit_hints = LookupAcceptCHForCommit( |
| url::Origin::Create(url), &delegate, test.frame_tree_node); |
| EXPECT_EQ(test.expect_commit_hints, actual_commit_hints) |
| << "Test case [" << test.description << "]: expected commit hints " |
| << HintsToString(test.expect_commit_hints) << " but got " |
| << HintsToString(actual_commit_hints) << "."; |
| } |
| } |
| |
| } // namespace content |