blob: bc90694bf25ef1f092ac036b5515a06c89903013 [file] [log] [blame]
// Copyright 2022 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/browsing_topics/header_util.h"
#include "base/strings/strcat.h"
#include "components/browsing_topics/common/common_types.h"
#include "components/browsing_topics/common/semantic_tree.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/page.h"
#include "content/public/common/content_client.h"
#include "net/http/http_request_headers.h"
#include "net/http/structured_headers.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
namespace content {
namespace {
// The max number of digits in a topic. As new taxonomies are introduced and old
// topics are expired, the expectation is this value will gradually grow.
constexpr int kTopicMaxLength = 3;
// The number of characters in a version string, e.g., chrome.1:1:10. This will
// grow as versions require more digits.
constexpr int kVersionMaxLength = 13;
static_assert(browsing_topics::ConfigVersion::kMaxValue < 10,
"Topics config version should not exceed 1 digit, or "
"`kVersionMaxLength` should be updated accordingly.");
static_assert(blink::features::kBrowsingTopicsTaxonomyVersionDefault < 10,
"Topics taxonomy version should not exceed 1 digit, or "
"`kVersionMaxLength` should be updated accordingly.");
static_assert(browsing_topics::SemanticTree::kNumTopics < 1000,
"Total number of topics (i.e. max topic ID) should not exceed 3 "
"digits, or `kTopicMaxLength` should be updated accordingly.");
} // namespace
std::string DeriveTopicsHeaderValue(
const std::vector<blink::mojom::EpochTopicPtr>& topics,
int num_versions_in_epochs) {
net::structured_headers::List header_list;
std::optional<std::string> last_version;
std::vector<net::structured_headers::ParameterizedItem> cur_topics;
// Build up the header without the padding parameter.
for (auto& topic : topics) {
bool new_version =
(!last_version.has_value() || last_version.value() != topic->version);
if (new_version) {
if (cur_topics.size() > 0) {
CHECK(last_version.has_value());
header_list.push_back(net::structured_headers::ParameterizedMember(
cur_topics,
{{"v",
net::structured_headers::Item(
*last_version, net::structured_headers::Item::kTokenType)}}));
cur_topics.clear();
}
last_version = topic->version;
}
cur_topics.push_back(net::structured_headers::ParameterizedItem(
net::structured_headers::Item(static_cast<int64_t>(topic->topic)), {}));
}
if (cur_topics.size() > 0) {
CHECK(last_version.has_value());
header_list.push_back(net::structured_headers::ParameterizedMember(
cur_topics, {{"v", net::structured_headers::Item(
*last_version,
net::structured_headers::Item::kTokenType)}}));
}
// The header is now complete, except for padding. We want the header to be of
// fixed size for the given number of versions in the list, so we add padding
// to make that happen.
// When adding padding, we'll always have at least 1 version.
if (num_versions_in_epochs == 0) {
num_versions_in_epochs = 1;
}
// The number of topics that should be in the padded response.
int max_number_of_epochs =
blink::features::kBrowsingTopicsNumberOfEpochsToExpose.Get();
CHECK_LE(num_versions_in_epochs, max_number_of_epochs);
CHECK_GT(max_number_of_epochs, 0);
// The padded length of the header given the number of versions.
// Example header: Sec-Browsing-Topics: (100 200);v=chrome.1:1:2,
// (300);v=chrome.1:1:4, ();p=P00
int max_length =
max_number_of_epochs * kTopicMaxLength + // length of three topics
max_number_of_epochs -
num_versions_in_epochs + // spaces between topics in a list
num_versions_in_epochs * 5 + // '();v='
num_versions_in_epochs *
kVersionMaxLength + // max length of the versions
(num_versions_in_epochs - 1) * 2; // the ', ' between topic lists
// Add the bytes for the ", " between the last list and the padding list in
// the event that there are no topics.
if (header_list.size() == 0) {
max_length += 2;
}
// How many bytes of padding do we need to add?
int padding_needed =
header_list.size() > 0
? max_length -
net::structured_headers::SerializeList(header_list)->length()
: max_length;
// The padding should generally be >= 0. It can be negative in certain
// circumstances and we need to handle that here. It can be negative if a new
// version is rolled out via finch (e.g., model or taxonomy) that uses an
// extra digit in its number but the binary hasn't been updated to handle the
// extra digit yet. It could also happen if there is a race between getting
// topics and getting the number of distinct topic versions. We clamp to 0 to
// prevent breakage in these rare circumstances.
if (padding_needed < 0) {
padding_needed = 0;
}
// Add the padding list at the end.
header_list.push_back(net::structured_headers::ParameterizedMember(
std::vector<net::structured_headers::ParameterizedItem>(),
{{"p", net::structured_headers::Item(
base::StrCat({"P", std::string(padding_needed, '0')}),
net::structured_headers::Item::kTokenType)}}));
std::optional<std::string> serialized_header =
net::structured_headers::SerializeList(header_list);
CHECK(serialized_header);
return *serialized_header;
}
void HandleTopicsEligibleResponse(
const network::mojom::ParsedHeadersPtr& parsed_headers,
const url::Origin& caller_origin,
RenderFrameHost& request_initiator_frame,
browsing_topics::ApiCallerSource caller_source) {
DCHECK(caller_source == browsing_topics::ApiCallerSource::kFetch ||
caller_source == browsing_topics::ApiCallerSource::kIframeAttribute ||
caller_source == browsing_topics::ApiCallerSource::kImgAttribute);
if (!parsed_headers || !parsed_headers->observe_browsing_topics) {
return;
}
// Check the page's IsPrimary() status again in case it has changed since the
// request time.
if (!request_initiator_frame.GetPage().IsPrimary()) {
return;
}
// Store the observation.
std::vector<blink::mojom::EpochTopicPtr> topics;
GetContentClient()->browser()->HandleTopicsWebApi(
caller_origin, request_initiator_frame.GetMainFrame(), caller_source,
/*get_topics=*/false,
/*observe=*/true, topics);
}
} // namespace content