1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
content / browser / browsing_topics / header_util.cc [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/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
const char kBrowsingTopicsRequestHeaderKey[] = "Sec-Browsing-Topics";
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);
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