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