| // 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/ranges/algorithm.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; | 
 |     const auto& map = network::GetClientHintToNameMap(); | 
 |     base::ranges::transform(hints.value(), std::back_inserter(hints_list), | 
 |                             [&map](network::mojom::WebClientHintsType hint) { | 
 |                               return map.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 |