Avi Drissman | 4e1b7bc3 | 2022-09-15 14:03:50 | [diff] [blame] | 1 | // Copyright 2018 The Chromium Authors |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "content/browser/client_hints/client_hints.h" |
| 6 | |
Peter Kasting | 1557e5f | 2025-01-28 01:14:08 | [diff] [blame] | 7 | #include <algorithm> |
| 8 | |
Keishi Hattori | c1b0023 | 2022-11-22 09:04:26 | [diff] [blame] | 9 | #include "base/memory/raw_ptr.h" |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 10 | #include "base/strings/strcat.h" |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 11 | #include "base/strings/string_number_conversions.h" |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 12 | #include "base/strings/string_util.h" |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 13 | #include "base/time/time.h" |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 14 | #include "content/public/test/mock_client_hints_controller_delegate.h" |
| 15 | #include "content/public/test/test_browser_context.h" |
| 16 | #include "content/test/test_render_frame_host.h" |
| 17 | #include "content/test/test_render_view_host.h" |
| 18 | #include "content/test/test_web_contents.h" |
| 19 | #include "net/http/http_response_headers.h" |
Antonio Sartori | 55369fff | 2022-11-30 09:17:56 | [diff] [blame] | 20 | #include "services/metrics/public/cpp/ukm_source_id.h" |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 21 | #include "services/network/public/cpp/client_hints.h" |
| 22 | #include "services/network/public/cpp/is_potentially_trustworthy.h" |
Victor Tan | ce1f6fb | 2025-08-22 01:11:02 | [diff] [blame] | 23 | #include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h" |
Yoshisato Yanagisawa | 35b1e363 | 2025-06-18 23:37:51 | [diff] [blame] | 24 | #include "testing/gmock/include/gmock/gmock-matchers.h" |
| 25 | #include "third_party/blink/public/common/client_hints/client_hints.h" |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 26 | |
| 27 | namespace content { |
| 28 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 29 | namespace { |
| 30 | |
| 31 | using ClientHintsVector = std::vector<network::mojom::WebClientHintsType>; |
| 32 | using network::mojom::WebClientHintsType; |
| 33 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 34 | } // namespace |
| 35 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 36 | class ClientHintsTest : public RenderViewHostImplTestHarness { |
| 37 | public: |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 38 | ClientHintsTest() = default; |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 39 | ClientHintsTest(const ClientHintsTest&) = delete; |
| 40 | ClientHintsTest& operator=(const ClientHintsTest&) = delete; |
| 41 | ~ClientHintsTest() override { |
| 42 | blink::TrialTokenValidator::ResetOriginTrialPolicyGetter(); |
| 43 | } |
| 44 | |
| 45 | static constexpr char kOriginUrl[] = "https://example.com"; |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 46 | |
| 47 | void AddOneChildNode() { |
| 48 | main_test_rfh()->OnCreateChildFrame( |
| 49 | /*new_routing_id=*/14, TestRenderFrameHost::CreateStubFrameRemote(), |
| 50 | TestRenderFrameHost::CreateStubBrowserInterfaceBrokerReceiver(), |
| 51 | TestRenderFrameHost::CreateStubPolicyContainerBindParams(), |
| 52 | TestRenderFrameHost::CreateStubAssociatedInterfaceProviderReceiver(), |
| 53 | /*scope=*/blink::mojom::TreeScopeType::kDocument, /*frame_name=*/"", |
| 54 | /*frame_unique_name=*/"uniqueName0", /*is_created_by_script=*/false, |
| 55 | /*frame_token=*/blink::LocalFrameToken(), |
| 56 | /*devtools_frame_token=*/base::UnguessableToken::Create(), |
| 57 | /*document_token=*/blink::DocumentToken(), |
| 58 | /*frame_policy=*/blink::FramePolicy(), |
| 59 | /*frame_owner_properties=*/blink::mojom::FrameOwnerProperties(), |
Antonio Sartori | 55369fff | 2022-11-30 09:17:56 | [diff] [blame] | 60 | /*owner_type=*/blink::FrameOwnerElementType::kIframe, |
| 61 | /*document_ukm_source_id=*/ukm::kInvalidSourceId); |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 62 | } |
| 63 | |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 64 | std::optional<ClientHintsVector> ParseAndPersist( |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 65 | const GURL& url, |
| 66 | const net::HttpResponseHeaders* response_header, |
| 67 | const std::string& accept_ch_str, |
| 68 | FrameTreeNode* frame_tree_node, |
| 69 | MockClientHintsControllerDelegate* delegate) { |
| 70 | auto parsed_headers = network::mojom::ParsedHeaders::New(); |
| 71 | parsed_headers->accept_ch = network::ParseClientHintsHeader(accept_ch_str); |
| 72 | |
| 73 | return ParseAndPersistAcceptCHForNavigation( |
| 74 | url::Origin::Create(url), parsed_headers, response_header, |
| 75 | browser_context(), delegate, frame_tree_node); |
| 76 | } |
| 77 | |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 78 | std::string HintsToString(std::optional<ClientHintsVector> hints) { |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 79 | if (!hints) |
| 80 | return ""; |
| 81 | |
| 82 | std::vector<std::string> hints_list; |
Peter Kasting | 1d48f35 | 2023-03-07 18:29:57 | [diff] [blame] | 83 | const auto& map = network::GetClientHintToNameMap(); |
Peter Kasting | 1557e5f | 2025-01-28 01:14:08 | [diff] [blame] | 84 | std::ranges::transform(hints.value(), std::back_inserter(hints_list), |
| 85 | [&map](network::mojom::WebClientHintsType hint) { |
| 86 | return map.at(hint); |
| 87 | }); |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 88 | |
| 89 | return base::JoinString(hints_list, ","); |
| 90 | } |
| 91 | |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 92 | std::pair<std::string, ClientHintsVector> GetAllClientHints() { |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 93 | std::vector<std::string> accept_ch_tokens; |
| 94 | ClientHintsVector hints_list; |
| 95 | for (const auto& pair : network::GetClientHintToNameMap()) { |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 96 | hints_list.push_back(pair.first); |
| 97 | accept_ch_tokens.push_back(pair.second); |
| 98 | } |
| 99 | return {base::JoinString(accept_ch_tokens, ","), hints_list}; |
| 100 | } |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 101 | }; |
| 102 | |
| 103 | TEST_F(ClientHintsTest, RttRoundedOff) { |
Peter Kasting | e5a38ed | 2021-10-02 03:06:35 | [diff] [blame] | 104 | EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(1023)) % 50); |
| 105 | EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(6787)) % 50); |
| 106 | EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(12)) % 50); |
| 107 | EXPECT_EQ(0u, RoundRttForTesting("foo.com", base::Milliseconds(1023)) % 50); |
| 108 | EXPECT_EQ(0u, RoundRttForTesting("foo.com", base::Milliseconds(1193)) % 50); |
| 109 | EXPECT_EQ(0u, RoundRttForTesting("foo.com", base::Milliseconds(12)) % 50); |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 110 | } |
| 111 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 112 | TEST_F(ClientHintsTest, DownlinkRoundedOff) { |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 113 | EXPECT_GE(1, |
| 114 | static_cast<int>(RoundKbpsToMbpsForTesting("", 102) * 1000) % 50); |
| 115 | EXPECT_GE(1, static_cast<int>(RoundKbpsToMbpsForTesting("", 12) * 1000) % 50); |
| 116 | EXPECT_GE(1, |
| 117 | static_cast<int>(RoundKbpsToMbpsForTesting("", 2102) * 1000) % 50); |
| 118 | |
| 119 | EXPECT_GE( |
| 120 | 1, |
| 121 | static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 102) * 1000) % 50); |
| 122 | EXPECT_GE( |
| 123 | 1, |
| 124 | static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 12) * 1000) % 50); |
| 125 | EXPECT_GE( |
| 126 | 1, |
| 127 | static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 2102) * 1000) % 50); |
| 128 | EXPECT_GE( |
| 129 | 1, static_cast<int>(RoundKbpsToMbpsForTesting("foo.com", 12102) * 1000) % |
| 130 | 50); |
| 131 | } |
| 132 | |
| 133 | // Verify that the value of RTT after adding noise is within approximately 10% |
| 134 | // of the original value. Note that the difference between the final value of |
| 135 | // RTT and the original value may be slightly more than 10% due to rounding off. |
| 136 | // To handle that, the maximum absolute difference allowed is set to a value |
| 137 | // slightly larger than 10% of the original metric value. |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 138 | TEST_F(ClientHintsTest, FinalRttWithin10PercentValue) { |
Peter Kasting | e5a38ed | 2021-10-02 03:06:35 | [diff] [blame] | 139 | EXPECT_NEAR(98, RoundRttForTesting("", base::Milliseconds(98)), 100); |
| 140 | EXPECT_NEAR(1023, RoundRttForTesting("", base::Milliseconds(1023)), 200); |
| 141 | EXPECT_NEAR(1193, RoundRttForTesting("", base::Milliseconds(1193)), 200); |
| 142 | EXPECT_NEAR(2750, RoundRttForTesting("", base::Milliseconds(2750)), 400); |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 143 | } |
| 144 | |
| 145 | // Verify that the value of downlink after adding noise is within approximately |
| 146 | // 10% of the original value. Note that the difference between the final value |
| 147 | // of downlink and the original value may be slightly more than 10% due to |
| 148 | // rounding off. To handle that, the maximum absolute difference allowed is set |
| 149 | // to a value slightly larger than 10% of the original metric value. |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 150 | TEST_F(ClientHintsTest, FinalDownlinkWithin10PercentValue) { |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 151 | EXPECT_NEAR(0.098, RoundKbpsToMbpsForTesting("", 98), 0.1); |
| 152 | EXPECT_NEAR(1.023, RoundKbpsToMbpsForTesting("", 1023), 0.2); |
| 153 | EXPECT_NEAR(1.193, RoundKbpsToMbpsForTesting("", 1193), 0.2); |
| 154 | EXPECT_NEAR(7.523, RoundKbpsToMbpsForTesting("", 7523), 0.9); |
| 155 | EXPECT_NEAR(9.999, RoundKbpsToMbpsForTesting("", 9999), 1.2); |
| 156 | } |
| 157 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 158 | TEST_F(ClientHintsTest, RttMaxValue) { |
Peter Kasting | e5a38ed | 2021-10-02 03:06:35 | [diff] [blame] | 159 | EXPECT_GE(3000u, RoundRttForTesting("", base::Milliseconds(1023))); |
| 160 | EXPECT_GE(3000u, RoundRttForTesting("", base::Milliseconds(2789))); |
| 161 | EXPECT_GE(3000u, RoundRttForTesting("", base::Milliseconds(6023))); |
| 162 | EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(1023)) % 50); |
| 163 | EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(2789)) % 50); |
| 164 | EXPECT_EQ(0u, RoundRttForTesting("", base::Milliseconds(6023)) % 50); |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 165 | } |
| 166 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 167 | TEST_F(ClientHintsTest, DownlinkMaxValue) { |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 168 | EXPECT_GE(10.0, RoundKbpsToMbpsForTesting("", 102)); |
| 169 | EXPECT_GE(10.0, RoundKbpsToMbpsForTesting("", 2102)); |
| 170 | EXPECT_GE(10.0, RoundKbpsToMbpsForTesting("", 100102)); |
| 171 | EXPECT_GE(1, |
| 172 | static_cast<int>(RoundKbpsToMbpsForTesting("", 102) * 1000) % 50); |
| 173 | EXPECT_GE(1, |
| 174 | static_cast<int>(RoundKbpsToMbpsForTesting("", 2102) * 1000) % 50); |
| 175 | EXPECT_GE( |
| 176 | 1, static_cast<int>(RoundKbpsToMbpsForTesting("", 100102) * 1000) % 50); |
| 177 | } |
| 178 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 179 | TEST_F(ClientHintsTest, RttRandomized) { |
Peter Kasting | e5a38ed | 2021-10-02 03:06:35 | [diff] [blame] | 180 | const int initial_value = |
| 181 | RoundRttForTesting("example.com", base::Milliseconds(1023)); |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 182 | bool network_quality_randomized_by_host = false; |
| 183 | // There is a 1/20 chance that the same random noise is selected for two |
| 184 | // different hosts. Run this test across 20 hosts to reduce the chances of |
| 185 | // test failing to (1/20)^20. |
| 186 | for (size_t i = 0; i < 20; ++i) { |
Peter Kasting | e5a38ed | 2021-10-02 03:06:35 | [diff] [blame] | 187 | int value = |
| 188 | RoundRttForTesting(base::NumberToString(i), base::Milliseconds(1023)); |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 189 | // If |value| is different than |initial_value|, it implies that RTT is |
| 190 | // randomized by host. This verifies the behavior, and test can be ended. |
| 191 | if (value != initial_value) |
| 192 | network_quality_randomized_by_host = true; |
| 193 | } |
| 194 | EXPECT_TRUE(network_quality_randomized_by_host); |
| 195 | |
| 196 | // Calling RoundRttForTesting for same host should return the same result. |
| 197 | for (size_t i = 0; i < 20; ++i) { |
Peter Kasting | e5a38ed | 2021-10-02 03:06:35 | [diff] [blame] | 198 | int value = RoundRttForTesting("example.com", base::Milliseconds(1023)); |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 199 | EXPECT_EQ(initial_value, value); |
| 200 | } |
| 201 | } |
| 202 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 203 | TEST_F(ClientHintsTest, DownlinkRandomized) { |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 204 | const int initial_value = RoundKbpsToMbpsForTesting("example.com", 1023); |
| 205 | bool network_quality_randomized_by_host = false; |
| 206 | // There is a 1/20 chance that the same random noise is selected for two |
| 207 | // different hosts. Run this test across 20 hosts to reduce the chances of |
| 208 | // test failing to (1/20)^20. |
| 209 | for (size_t i = 0; i < 20; ++i) { |
| 210 | int value = RoundKbpsToMbpsForTesting(base::NumberToString(i), 1023); |
| 211 | // If |value| is different than |initial_value|, it implies that downlink is |
| 212 | // randomized by host. This verifies the behavior, and test can be ended. |
| 213 | if (value != initial_value) |
| 214 | network_quality_randomized_by_host = true; |
| 215 | } |
| 216 | EXPECT_TRUE(network_quality_randomized_by_host); |
| 217 | |
| 218 | // Calling RoundMbps for same host should return the same result. |
| 219 | for (size_t i = 0; i < 20; ++i) { |
| 220 | int value = RoundKbpsToMbpsForTesting("example.com", 1023); |
| 221 | EXPECT_EQ(initial_value, value); |
| 222 | } |
| 223 | } |
| 224 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 225 | TEST_F(ClientHintsTest, IntegrationTestsOnParseLookUp) { |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 226 | GURL url = GURL(ClientHintsTest::kOriginUrl); |
| 227 | contents()->NavigateAndCommit(url); |
| 228 | FrameTree& frame_tree = contents()->GetPrimaryFrameTree(); |
| 229 | FrameTreeNode* main_frame_node = frame_tree.root(); |
| 230 | AddOneChildNode(); |
| 231 | FrameTreeNode* sub_frame_node = main_frame_node->child_at(0); |
| 232 | |
| 233 | blink::UserAgentMetadata ua_metadata; |
| 234 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 235 | |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 236 | const auto& all_non_origin_trial_hints_pair = GetAllClientHints(); |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 237 | const struct { |
| 238 | std::string description; |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 239 | std::string accept_ch_str; |
Keishi Hattori | c1b0023 | 2022-11-22 09:04:26 | [diff] [blame] | 240 | raw_ptr<FrameTreeNode> frame_tree_node; |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 241 | std::optional<ClientHintsVector> expect_hints; |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 242 | ClientHintsVector expect_commit_hints; |
| 243 | } tests[] = { |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 244 | {"Persist hints for main frame", "sec-ch-ua-platform, sec-ch-ua-bitness", |
| 245 | main_frame_node, |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 246 | std::make_optional(ClientHintsVector{WebClientHintsType::kUAPlatform, |
| 247 | WebClientHintsType::kUABitness}), |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 248 | ClientHintsVector{WebClientHintsType::kUAPlatform, |
| 249 | WebClientHintsType::kUABitness}}, |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 250 | {"No persist hints for sub frame", |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 251 | "sec-ch-ua-platform, sec-ch-ua-bitness", sub_frame_node, std::nullopt, |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 252 | ClientHintsVector{WebClientHintsType::kUAPlatform, |
| 253 | WebClientHintsType::kUABitness}}, |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 254 | {"All client hints for main frame", all_non_origin_trial_hints_pair.first, |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 255 | main_frame_node, |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 256 | std::make_optional(all_non_origin_trial_hints_pair.second), |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 257 | all_non_origin_trial_hints_pair.second}, |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 258 | {"All client hints for sub frame", all_non_origin_trial_hints_pair.first, |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 259 | sub_frame_node, std::nullopt, all_non_origin_trial_hints_pair.second}, |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 260 | }; |
| 261 | |
| 262 | for (const auto& test : tests) { |
| 263 | auto response_headers = |
| 264 | base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\n"); |
| 265 | |
Victor Tan | 3a676368e | 2022-11-04 20:52:23 | [diff] [blame] | 266 | auto actual_hints = |
| 267 | ParseAndPersist(url, response_headers.get(), test.accept_ch_str, |
| 268 | test.frame_tree_node, &delegate); |
| 269 | EXPECT_EQ(test.expect_hints, actual_hints) |
| 270 | << "Test case [" << test.description << "]: expected hints " |
| 271 | << HintsToString(test.expect_hints) << " but got " |
| 272 | << HintsToString(actual_hints) << "."; |
| 273 | |
| 274 | // Verify commit hints. |
| 275 | ClientHintsVector actual_commit_hints = LookupAcceptCHForCommit( |
| 276 | url::Origin::Create(url), &delegate, test.frame_tree_node); |
| 277 | EXPECT_EQ(test.expect_commit_hints, actual_commit_hints) |
| 278 | << "Test case [" << test.description << "]: expected commit hints " |
| 279 | << HintsToString(test.expect_commit_hints) << " but got " |
| 280 | << HintsToString(actual_commit_hints) << "."; |
| 281 | } |
| 282 | } |
| 283 | |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 284 | TEST_F(ClientHintsTest, SubFrame) { |
Victor Tan | 969e157 | 2023-05-19 17:49:28 | [diff] [blame] | 285 | GURL url = GURL(ClientHintsTest::kOriginUrl); |
| 286 | contents()->NavigateAndCommit(url); |
| 287 | FrameTree& frame_tree = contents()->GetPrimaryFrameTree(); |
| 288 | FrameTreeNode* main_frame_node = frame_tree.root(); |
| 289 | AddOneChildNode(); |
| 290 | FrameTreeNode* sub_frame_node = main_frame_node->child_at(0); |
| 291 | |
| 292 | blink::UserAgentMetadata ua_metadata; |
| 293 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 294 | |
| 295 | // Persist existing hint to accept-ch cache. |
| 296 | ClientHintsVector existing_hints = ClientHintsVector{ |
| 297 | WebClientHintsType::kUAPlatform, WebClientHintsType::kUABitness}; |
| 298 | delegate.PersistClientHints(url::Origin::Create(url), |
| 299 | main_frame_node->GetParentOrOuterDocument(), |
| 300 | existing_hints); |
| 301 | auto response_headers = |
| 302 | base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\n"); |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 303 | std::string accept_ch_str = "sec-ch-ua-platform-version"; |
Victor Tan | 969e157 | 2023-05-19 17:49:28 | [diff] [blame] | 304 | |
Victor Tan | b37a4c8 | 2023-06-08 16:18:28 | [diff] [blame] | 305 | // We shouldn't parse accept-ch in subframe, it should not overwrite existing |
| 306 | // hints. |
Victor Tan | 969e157 | 2023-05-19 17:49:28 | [diff] [blame] | 307 | auto actual_updated_hints = ParseAndPersist( |
| 308 | url, response_headers.get(), accept_ch_str, sub_frame_node, &delegate); |
| 309 | |
Arthur Sonzogni | c686e8f | 2024-01-11 08:36:37 | [diff] [blame] | 310 | EXPECT_EQ(std::nullopt, actual_updated_hints); |
Victor Tan | 969e157 | 2023-05-19 17:49:28 | [diff] [blame] | 311 | blink::EnabledClientHints current_hints; |
| 312 | delegate.GetAllowedClientHintsFromSource(url::Origin::Create(url), |
| 313 | ¤t_hints); |
| 314 | EXPECT_EQ(existing_hints, current_hints.GetEnabledHints()); |
Victor Tan | 969e157 | 2023-05-19 17:49:28 | [diff] [blame] | 315 | } |
| 316 | |
Yoshisato Yanagisawa | b108f25 | 2025-07-24 23:49:04 | [diff] [blame] | 317 | TEST_F(ClientHintsTest, GetEnabledClientHintsMainFrame) { |
| 318 | GURL main_url(kOriginUrl); |
| 319 | contents()->NavigateAndCommit(main_url); |
| 320 | url::Origin origin = url::Origin::Create(main_url); |
Yoshisato Yanagisawa | 35b1e363 | 2025-06-18 23:37:51 | [diff] [blame] | 321 | |
| 322 | FrameTree& frame_tree = contents()->GetPrimaryFrameTree(); |
| 323 | FrameTreeNode* main_frame_node = frame_tree.root(); |
| 324 | |
| 325 | blink::UserAgentMetadata ua_metadata; |
| 326 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 327 | |
| 328 | std::vector<network::mojom::WebClientHintsType> expected_types = { |
| 329 | network::mojom::WebClientHintsType::kUAArch, |
| 330 | network::mojom::WebClientHintsType::kUAWoW64, |
| 331 | }; |
| 332 | delegate.SetAdditionalClientHints(expected_types); |
| 333 | // Add default ClientHints. |
| 334 | for (const auto& [hint, _] : network::GetClientHintToNameMap()) { |
| 335 | if (blink::IsClientHintSentByDefault(hint)) { |
| 336 | expected_types.push_back(hint); |
| 337 | } |
| 338 | } |
Yoshisato Yanagisawa | b108f25 | 2025-07-24 23:49:04 | [diff] [blame] | 339 | |
| 340 | const auto& actual_hints = |
Yoshisato Yanagisawa | 2b6d25a | 2025-07-08 04:23:39 | [diff] [blame] | 341 | GetEnabledClientHints(origin, main_frame_node, &delegate); |
Yoshisato Yanagisawa | 35b1e363 | 2025-06-18 23:37:51 | [diff] [blame] | 342 | |
Yoshisato Yanagisawa | b108f25 | 2025-07-24 23:49:04 | [diff] [blame] | 343 | EXPECT_EQ(origin, actual_hints.origin); |
| 344 | EXPECT_TRUE(actual_hints.is_outermost_main_frame); |
Yoshisato Yanagisawa | 35b1e363 | 2025-06-18 23:37:51 | [diff] [blame] | 345 | // We do not care the order of contents. |
Yoshisato Yanagisawa | b108f25 | 2025-07-24 23:49:04 | [diff] [blame] | 346 | EXPECT_THAT(actual_hints.hints, |
| 347 | testing::UnorderedElementsAreArray(expected_types)); |
| 348 | } |
| 349 | |
| 350 | TEST_F(ClientHintsTest, GetEnabledClientHintsSubframe) { |
| 351 | GURL main_url(kOriginUrl); |
| 352 | contents()->NavigateAndCommit(main_url); |
| 353 | url::Origin origin = url::Origin::Create(main_url); |
| 354 | |
| 355 | FrameTree& frame_tree = contents()->GetPrimaryFrameTree(); |
| 356 | FrameTreeNode* main_frame_node = frame_tree.root(); |
| 357 | AddOneChildNode(); |
| 358 | FrameTreeNode* sub_frame_node = main_frame_node->child_at(0); |
| 359 | |
| 360 | blink::UserAgentMetadata ua_metadata; |
| 361 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 362 | |
| 363 | // Add default ClientHints. |
| 364 | std::vector<network::mojom::WebClientHintsType> expected_types; |
| 365 | for (const auto& [hint, _] : network::GetClientHintToNameMap()) { |
| 366 | if (blink::IsClientHintSentByDefault(hint)) { |
| 367 | expected_types.push_back(hint); |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | // The origin passed to GetEnabledClientHints is the resource origin. |
| 372 | // The origin in the result is the main frame origin. |
| 373 | GURL sub_url("https://sub.example.com"); |
| 374 | const auto& actual_hints = GetEnabledClientHints(url::Origin::Create(sub_url), |
| 375 | sub_frame_node, &delegate); |
| 376 | |
| 377 | EXPECT_EQ(origin, actual_hints.origin); |
| 378 | EXPECT_FALSE(actual_hints.is_outermost_main_frame); |
| 379 | // Just check if the hints has the default low entropy client hints. |
| 380 | // We do not care the order of contents. |
| 381 | EXPECT_THAT(actual_hints.hints, |
| 382 | testing::UnorderedElementsAreArray(expected_types)); |
Yoshisato Yanagisawa | 35b1e363 | 2025-06-18 23:37:51 | [diff] [blame] | 383 | } |
| 384 | |
Victor Tan | ce1f6fb | 2025-08-22 01:11:02 | [diff] [blame] | 385 | TEST_F(ClientHintsTest, AddPrefetchNavigationRequestClientHintsHeaders) { |
| 386 | GURL url = GURL(ClientHintsTest::kOriginUrl); |
| 387 | url::Origin origin = url::Origin::Create(url); |
| 388 | |
| 389 | blink::UserAgentMetadata ua_metadata; |
| 390 | ua_metadata.architecture = "x86"; |
| 391 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 392 | |
| 393 | // Enable a variety of hints. |
| 394 | delegate.SetAdditionalClientHints( |
| 395 | {WebClientHintsType::kDeviceMemory, WebClientHintsType::kDpr, |
| 396 | WebClientHintsType::kUAArch, WebClientHintsType::kPrefersColorScheme}); |
| 397 | |
| 398 | net::HttpRequestHeaders headers; |
| 399 | AddPrefetchNavigationRequestClientHintsHeaders(origin, &headers, |
| 400 | browser_context(), &delegate, |
| 401 | /*is_ua_override_on=*/false); |
| 402 | |
| 403 | // Low-entropy UA hints are sent by default. |
| 404 | EXPECT_TRUE(headers.HasHeader("sec-ch-ua")); |
| 405 | EXPECT_TRUE(headers.HasHeader("sec-ch-ua-mobile")); |
| 406 | |
| 407 | // High-entropy hints that were enabled. |
| 408 | std::optional<std::string> header_value = headers.GetHeader("sec-ch-ua-arch"); |
| 409 | EXPECT_TRUE(header_value.has_value()); |
| 410 | EXPECT_EQ("\"x86\"", header_value.value()); |
| 411 | |
| 412 | EXPECT_TRUE(headers.HasHeader("sec-ch-device-memory")); |
| 413 | EXPECT_TRUE(headers.HasHeader("sec-ch-dpr")); |
| 414 | |
| 415 | // Prefers-color-scheme depends on a frame, which is null for prefetch. |
| 416 | EXPECT_FALSE(headers.HasHeader("sec-ch-prefers-color-scheme")); |
| 417 | |
| 418 | // Verify hints not in the requested list. |
| 419 | EXPECT_FALSE(headers.HasHeader("sec-ch-ua-bitness")); |
| 420 | } |
| 421 | |
| 422 | TEST_F(ClientHintsTest, AddNavigationRequestClientHintsHeaders_MainFrame) { |
| 423 | GURL main_url(kOriginUrl); |
| 424 | contents()->NavigateAndCommit(main_url); |
| 425 | url::Origin origin = url::Origin::Create(main_url); |
| 426 | FrameTreeNode* main_frame_node = contents()->GetPrimaryFrameTree().root(); |
| 427 | |
| 428 | blink::UserAgentMetadata ua_metadata; |
| 429 | ua_metadata.architecture = "x86"; |
| 430 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 431 | |
| 432 | // Enable a variety of hints. |
| 433 | delegate.SetAdditionalClientHints( |
| 434 | {WebClientHintsType::kDeviceMemory, WebClientHintsType::kDpr, |
| 435 | WebClientHintsType::kUAArch, WebClientHintsType::kPrefersColorScheme}); |
| 436 | |
| 437 | net::HttpRequestHeaders headers; |
| 438 | AddNavigationRequestClientHintsHeaders( |
| 439 | origin, &headers, browser_context(), &delegate, |
| 440 | /*is_ua_override_on=*/false, main_frame_node, |
| 441 | /*container_policy=*/{}); |
| 442 | |
| 443 | // Low-entropy UA hints are sent by default. |
| 444 | EXPECT_TRUE(headers.HasHeader("sec-ch-ua")); |
| 445 | EXPECT_TRUE(headers.HasHeader("sec-ch-ua-mobile")); |
| 446 | |
| 447 | // High-entropy hints that were enabled. |
| 448 | std::optional<std::string> header_value = headers.GetHeader("sec-ch-ua-arch"); |
| 449 | EXPECT_TRUE(header_value.has_value()); |
| 450 | EXPECT_EQ("\"x86\"", header_value.value()); |
| 451 | |
| 452 | EXPECT_TRUE(headers.HasHeader("sec-ch-device-memory")); |
| 453 | EXPECT_TRUE(headers.HasHeader("sec-ch-dpr")); |
| 454 | |
| 455 | // Prefers-color-scheme should have value for navigation request. |
| 456 | EXPECT_TRUE(headers.HasHeader("sec-ch-prefers-color-scheme")); |
| 457 | |
| 458 | // Verify hints not in the requested list. |
| 459 | EXPECT_FALSE(headers.HasHeader("sec-ch-ua-bitness")); |
| 460 | } |
| 461 | |
| 462 | TEST_F(ClientHintsTest, AddNavigationRequestClientHintsHeaders_Subframe) { |
| 463 | GURL url = GURL(ClientHintsTest::kOriginUrl); |
| 464 | contents()->NavigateAndCommit(url); |
| 465 | FrameTree& frame_tree = contents()->GetPrimaryFrameTree(); |
| 466 | FrameTreeNode* main_frame_node = frame_tree.root(); |
| 467 | AddOneChildNode(); |
| 468 | FrameTreeNode* sub_frame_node = main_frame_node->child_at(0); |
| 469 | |
| 470 | GURL subframe_url("https://sub.example.com"); |
| 471 | url::Origin subframe_origin = url::Origin::Create(subframe_url); |
| 472 | |
| 473 | blink::UserAgentMetadata ua_metadata; |
| 474 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 475 | |
| 476 | // Enable a hint that requires delegation. |
| 477 | delegate.SetAdditionalClientHints({WebClientHintsType::kDeviceMemory}); |
| 478 | |
| 479 | net::HttpRequestHeaders headers; |
| 480 | // Case 1: No delegation policy. Hint should not be added. |
| 481 | AddNavigationRequestClientHintsHeaders( |
| 482 | subframe_origin, &headers, browser_context(), &delegate, |
| 483 | /*is_ua_override_on=*/false, sub_frame_node, |
| 484 | /*container_policy=*/{}); |
| 485 | EXPECT_FALSE(headers.HasHeader("sec-ch-device-memory")); |
| 486 | |
| 487 | // Case 2: With delegation policy. Hint should be added. |
| 488 | headers.Clear(); |
| 489 | network::ParsedPermissionsPolicy container_policy; |
| 490 | network::ParsedPermissionsPolicyDeclaration decl( |
| 491 | network::mojom::PermissionsPolicyFeature::kClientHintDeviceMemory, |
| 492 | {*network::OriginWithPossibleWildcards::FromOrigin(subframe_origin)}, |
| 493 | /*self_if_matches=*/std::nullopt, |
| 494 | /*matches_all_origins=*/false, /*matches_opaque_src=*/false); |
| 495 | container_policy.push_back(std::move(decl)); |
| 496 | |
| 497 | AddNavigationRequestClientHintsHeaders( |
| 498 | subframe_origin, &headers, browser_context(), &delegate, |
| 499 | /*is_ua_override_on=*/false, sub_frame_node, container_policy); |
| 500 | |
| 501 | EXPECT_TRUE(headers.HasHeader("sec-ch-device-memory")); |
| 502 | } |
| 503 | |
Yoshisato Yanagisawa | 56a1d2f | 2025-08-22 09:56:55 | [diff] [blame] | 504 | TEST_F(ClientHintsTest, GetCriticalHintsMissingStatus) { |
| 505 | const GURL main_url(kOriginUrl); |
| 506 | contents()->NavigateAndCommit(main_url); |
| 507 | const url::Origin origin = url::Origin::Create(main_url); |
| 508 | |
| 509 | FrameTree& frame_tree = contents()->GetPrimaryFrameTree(); |
| 510 | FrameTreeNode* main_frame_node = frame_tree.root(); |
| 511 | |
| 512 | blink::UserAgentMetadata ua_metadata; |
| 513 | MockClientHintsControllerDelegate delegate(ua_metadata); |
| 514 | |
| 515 | const ClientHintsVector critical_hints = {WebClientHintsType::kDeviceMemory, |
| 516 | WebClientHintsType::kDpr}; |
| 517 | |
| 518 | // No hints persisted. |
| 519 | EXPECT_EQ(GetCriticalHintsMissingStatus(origin, main_frame_node, &delegate, |
| 520 | critical_hints), |
| 521 | CriticalHintsMissingStatus::kMissing); |
| 522 | |
| 523 | // One hint persisted. |
| 524 | delegate.PersistClientHints(origin, |
| 525 | main_frame_node->GetParentOrOuterDocument(), |
| 526 | {WebClientHintsType::kDeviceMemory}); |
| 527 | EXPECT_EQ(GetCriticalHintsMissingStatus(origin, main_frame_node, &delegate, |
| 528 | critical_hints), |
| 529 | CriticalHintsMissingStatus::kMissing); |
| 530 | |
| 531 | // All hints persisted. |
| 532 | delegate.PersistClientHints( |
| 533 | origin, main_frame_node->GetParentOrOuterDocument(), critical_hints); |
| 534 | EXPECT_EQ(GetCriticalHintsMissingStatus(origin, main_frame_node, &delegate, |
| 535 | critical_hints), |
| 536 | CriticalHintsMissingStatus::kPresent); |
| 537 | } |
| 538 | |
Yoav Weiss | 603be26 | 2019-03-04 13:14:57 | [diff] [blame] | 539 | } // namespace content |