blob: 9656b44be83e1e855a42a61042ae7193da187ea6 [file] [log] [blame]
// 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