blob: 81e41399f8ff9dd869b92d332f5214814406db70 [file] [log] [blame]
// Copyright 2019 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 <algorithm>
#include <string>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/preloading/prerender/prerender_host.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigator.h"
#include "content/browser/renderer_host/navigator_delegate.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/client_hints_controller_delegate.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/structured_headers.h"
#include "net/nqe/effective_connection_type.h"
#include "net/nqe/network_quality_estimator_params.h"
#include "services/network/public/cpp/client_hints.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/network_quality_tracker.h"
#include "services/network/public/mojom/web_client_hints_types.mojom-shared.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/client_hints/client_hints.h"
#include "third_party/blink/public/common/client_hints/enabled_client_hints.h"
#include "third_party/blink/public/common/device_memory/approximated_device_memory.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/blink/public/common/permissions_policy/origin_with_possible_wildcards.h"
#include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
#include "third_party/blink/public/common/user_agent/user_agent_metadata.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
using ::network::mojom::WebClientHintsType;
uint8_t randomization_salt = 0;
constexpr size_t kMaxRandomNumbers = 21;
// Returns the randomization salt (weak and insecure) that should be used when
// adding noise to the network quality metrics. This is known only to the
// device, and is generated only once. This makes it possible to add the same
// amount of noise for a given origin.
uint8_t RandomizationSalt() {
if (randomization_salt == 0)
randomization_salt = base::RandInt(1, kMaxRandomNumbers);
DCHECK_LE(1, randomization_salt);
DCHECK_GE(kMaxRandomNumbers, randomization_salt);
return randomization_salt;
}
double GetRandomMultiplier(const std::string& host) {
// The random number should be a function of the hostname to reduce
// cross-origin fingerprinting. The random number should also be a function
// of randomized salt which is known only to the device. This prevents
// origin from removing noise from the estimates.
unsigned hash = std::hash<std::string>{}(host) + RandomizationSalt();
double random_multiplier =
0.9 + static_cast<double>((hash % kMaxRandomNumbers)) * 0.01;
DCHECK_LE(0.90, random_multiplier);
DCHECK_GE(1.10, random_multiplier);
return random_multiplier;
}
unsigned long RoundRtt(const std::string& host,
const absl::optional<base::TimeDelta>& rtt) {
if (!rtt.has_value()) {
// RTT is unavailable. So, return the fastest value.
return 0;
}
// Limit the maximum reported value and the granularity to reduce
// fingerprinting.
constexpr base::TimeDelta kMaxRtt = base::Seconds(3);
constexpr base::TimeDelta kGranularity = base::Milliseconds(50);
const base::TimeDelta modified_rtt =
std::min(rtt.value() * GetRandomMultiplier(host), kMaxRtt);
DCHECK_GE(modified_rtt, base::TimeDelta());
return modified_rtt.RoundToMultiple(kGranularity).InMilliseconds();
}
double RoundKbpsToMbps(const std::string& host,
const absl::optional<int32_t>& downlink_kbps) {
// Limit the size of the buckets and the maximum reported value to reduce
// fingerprinting.
static const size_t kGranularityKbps = 50;
static const double kMaxDownlinkKbps = 10.0 * 1000;
// If downlink is unavailable, return the fastest value.
double randomized_downlink_kbps = downlink_kbps.value_or(kMaxDownlinkKbps);
randomized_downlink_kbps *= GetRandomMultiplier(host);
randomized_downlink_kbps =
std::min(randomized_downlink_kbps, kMaxDownlinkKbps);
DCHECK_LE(0, randomized_downlink_kbps);
DCHECK_GE(kMaxDownlinkKbps, randomized_downlink_kbps);
// Round down to the nearest kGranularityKbps kbps value.
double downlink_kbps_rounded =
std::round(randomized_downlink_kbps / kGranularityKbps) *
kGranularityKbps;
// Convert from Kbps to Mbps.
return downlink_kbps_rounded / 1000;
}
double GetDeviceScaleFactor() {
double device_scale_factor = 1.0;
if (display::Screen::GetScreen()) {
device_scale_factor =
display::Screen::GetScreen()->GetPrimaryDisplay().device_scale_factor();
}
DCHECK_LT(0.0, device_scale_factor);
return device_scale_factor;
}
// Returns the zoom factor for a given |url|.
double GetZoomFactor(BrowserContext* context, const GURL& url) {
#if BUILDFLAG(IS_ANDROID)
// On Android, use the default value when the AccessibilityPageZoom
// feature is not enabled.
if (!base::FeatureList::IsEnabled(features::kAccessibilityPageZoom))
return 1.0;
#endif
double zoom_level = HostZoomMap::GetDefaultForBrowserContext(context)
->GetZoomLevelForHostAndScheme(
url.scheme(), net::GetHostOrSpecFromURL(url));
if (zoom_level == 0.0) {
// Get default zoom level.
zoom_level = HostZoomMap::GetDefaultForBrowserContext(context)
->GetDefaultZoomLevel();
}
return blink::PageZoomLevelToZoomFactor(zoom_level);
}
// Returns a string corresponding to |value|. The returned string satisfies
// ABNF: 1*DIGIT [ "." 1*DIGIT ]
std::string DoubleToSpecCompliantString(double value) {
DCHECK_LE(0.0, value);
std::string result = base::NumberToString(value);
DCHECK(!result.empty());
if (value >= 1.0)
return result;
DCHECK_LE(0.0, value);
DCHECK_GT(1.0, value);
// Check if there is at least one character before period.
if (result.at(0) != '.')
return result;
// '.' is the first character in |result|. Prefix one digit before the
// period to make it spec compliant.
return "0" + result;
}
// Return the effective connection type value overridden for web APIs.
// If no override value has been set, a null value is returned.
absl::optional<net::EffectiveConnectionType>
GetWebHoldbackEffectiveConnectionType() {
if (!base::FeatureList::IsEnabled(
features::kNetworkQualityEstimatorWebHoldback)) {
return absl::nullopt;
}
std::string effective_connection_type_param =
base::GetFieldTrialParamValueByFeature(
features::kNetworkQualityEstimatorWebHoldback,
"web_effective_connection_type_override");
absl::optional<net::EffectiveConnectionType> effective_connection_type =
net::GetEffectiveConnectionTypeForName(effective_connection_type_param);
DCHECK(effective_connection_type_param.empty() || effective_connection_type);
if (!effective_connection_type)
return absl::nullopt;
DCHECK_NE(net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN,
effective_connection_type.value());
return effective_connection_type;
}
void SetHeaderToDouble(net::HttpRequestHeaders* headers,
WebClientHintsType client_hint_type,
double value) {
headers->SetHeader(network::GetClientHintToNameMap().at(client_hint_type),
DoubleToSpecCompliantString(value));
}
void SetHeaderToInt(net::HttpRequestHeaders* headers,
WebClientHintsType client_hint_type,
double value) {
headers->SetHeader(network::GetClientHintToNameMap().at(client_hint_type),
base::NumberToString(std::round(value)));
}
void SetHeaderToString(net::HttpRequestHeaders* headers,
WebClientHintsType client_hint_type,
const std::string& value) {
headers->SetHeader(network::GetClientHintToNameMap().at(client_hint_type),
value);
}
void RemoveClientHintHeader(WebClientHintsType client_hint_type,
net::HttpRequestHeaders* headers) {
headers->RemoveHeader(network::GetClientHintToNameMap().at(client_hint_type));
}
void AddDeviceMemoryHeader(net::HttpRequestHeaders* headers,
bool use_deprecated_version = false) {
DCHECK(headers);
blink::ApproximatedDeviceMemory::Initialize();
const float device_memory =
blink::ApproximatedDeviceMemory::GetApproximatedDeviceMemory();
DCHECK_LT(0.0, device_memory);
SetHeaderToDouble(headers,
use_deprecated_version
? WebClientHintsType::kDeviceMemory_DEPRECATED
: WebClientHintsType::kDeviceMemory,
device_memory);
}
void AddDPRHeader(net::HttpRequestHeaders* headers,
BrowserContext* context,
const GURL& url,
bool use_deprecated_version = false) {
DCHECK(headers);
DCHECK(context);
double device_scale_factor = GetDeviceScaleFactor();
double zoom_factor = GetZoomFactor(context, url);
SetHeaderToDouble(headers,
use_deprecated_version ? WebClientHintsType::kDpr_DEPRECATED
: WebClientHintsType::kDpr,
device_scale_factor * zoom_factor);
}
void AddSaveDataHeader(net::HttpRequestHeaders* headers,
BrowserContext* context) {
DCHECK(headers);
DCHECK(context);
// Unlike other client hints, this one is only sent when it has a value.
if (GetContentClient()->browser()->IsDataSaverEnabled(context))
SetHeaderToString(headers, WebClientHintsType::kSaveData, "on");
}
RenderWidgetHostView* GetRenderWidgetHostViewFromFrameTreeNode(
FrameTreeNode* frame_tree_node) {
if (!frame_tree_node || !frame_tree_node->current_frame_host())
return nullptr;
return frame_tree_node->current_frame_host()->GetView();
}
gfx::Size GetViewportSize(FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate) {
// If possible, return the current viewport size.
RenderWidgetHostView* view =
GetRenderWidgetHostViewFromFrameTreeNode(frame_tree_node);
if (view) {
return view->GetVisibleViewportSize();
}
// Otherwise, use the cached viewport size if it is valid (both dimensions are
// greater than zero).
gfx::Size cached_viewport_size =
delegate->GetMostRecentMainFrameViewportSize();
if (cached_viewport_size.width() > 0 && cached_viewport_size.height() > 0) {
return cached_viewport_size;
}
// Finally, use the display size if neither of the above methods work. Applies
// the device scale factor in this case, which is implicitly applied to other
// viewport sizes already.
return ScaleToRoundedSize(
display::Screen::GetScreen()->GetPrimaryDisplay().GetSizeInPixel(),
1.0 / GetDeviceScaleFactor());
}
gfx::Size GetScaledViewportSize(BrowserContext* context,
const GURL& url,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate) {
gfx::Size viewport_size = GetViewportSize(frame_tree_node, delegate);
#if BUILDFLAG(IS_ANDROID)
// On Android, the viewport is scaled so the width is 980. See
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/viewportAndroid.css.
// TODO(1246208): Improve the usefulness of the viewport client hints for
// navigation requests.
if (viewport_size.width() > 0) {
viewport_size =
ScaleToRoundedSize(viewport_size, 980.0 / viewport_size.width());
}
// On Android, use the default value when the AccessibilityPageZoom
// feature is not enabled.
if (!base::FeatureList::IsEnabled(features::kAccessibilityPageZoom)) {
return viewport_size;
}
#endif
base::UmaHistogramBoolean("ClientHints.Viewport.IsDeviceScaleFactorOne",
GetDeviceScaleFactor() == 1.0);
double zoom_factor = GetZoomFactor(context, url);
if (zoom_factor > 0) {
viewport_size = ScaleToRoundedSize(viewport_size, 1.0 / zoom_factor);
}
return viewport_size;
}
void AddViewportWidthHeader(net::HttpRequestHeaders* headers,
BrowserContext* context,
const GURL& url,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate,
bool use_deprecated_version = false) {
DCHECK(headers);
DCHECK(context);
gfx::Size viewport_size =
GetScaledViewportSize(context, url, frame_tree_node, delegate);
DCHECK_LT(0, viewport_size.width());
// TODO(yoav): Find out why this 0 check is needed...
if (viewport_size.width() > 0) {
SetHeaderToInt(headers,
use_deprecated_version
? WebClientHintsType::kViewportWidth_DEPRECATED
: WebClientHintsType::kViewportWidth,
viewport_size.width());
}
}
void AddViewportHeightHeader(net::HttpRequestHeaders* headers,
BrowserContext* context,
const GURL& url,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate) {
DCHECK(headers);
DCHECK(context);
gfx::Size viewport_size =
GetScaledViewportSize(context, url, frame_tree_node, delegate);
DCHECK_LT(0, viewport_size.height());
SetHeaderToInt(headers, network::mojom::WebClientHintsType::kViewportHeight,
viewport_size.height());
}
void AddRttHeader(net::HttpRequestHeaders* headers,
network::NetworkQualityTracker* network_quality_tracker,
const GURL& url) {
DCHECK(headers);
absl::optional<net::EffectiveConnectionType> web_holdback_ect =
GetWebHoldbackEffectiveConnectionType();
base::TimeDelta http_rtt;
if (web_holdback_ect.has_value()) {
http_rtt = net::NetworkQualityEstimatorParams::GetDefaultTypicalHttpRtt(
web_holdback_ect.value());
} else if (network_quality_tracker) {
http_rtt = network_quality_tracker->GetHttpRTT();
} else {
http_rtt = net::NetworkQualityEstimatorParams::GetDefaultTypicalHttpRtt(
net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
}
SetHeaderToInt(headers, WebClientHintsType::kRtt_DEPRECATED,
RoundRtt(url.host(), http_rtt));
}
void AddDownlinkHeader(net::HttpRequestHeaders* headers,
network::NetworkQualityTracker* network_quality_tracker,
const GURL& url) {
DCHECK(headers);
absl::optional<net::EffectiveConnectionType> web_holdback_ect =
GetWebHoldbackEffectiveConnectionType();
int32_t downlink_throughput_kbps;
if (web_holdback_ect.has_value()) {
downlink_throughput_kbps =
net::NetworkQualityEstimatorParams::GetDefaultTypicalDownlinkKbps(
web_holdback_ect.value());
} else if (network_quality_tracker) {
downlink_throughput_kbps =
network_quality_tracker->GetDownstreamThroughputKbps();
} else {
downlink_throughput_kbps =
net::NetworkQualityEstimatorParams::GetDefaultTypicalDownlinkKbps(
net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
}
SetHeaderToDouble(headers, WebClientHintsType::kDownlink_DEPRECATED,
RoundKbpsToMbps(url.host(), downlink_throughput_kbps));
}
void AddEctHeader(net::HttpRequestHeaders* headers,
network::NetworkQualityTracker* network_quality_tracker,
const GURL& url) {
DCHECK(headers);
DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));
absl::optional<net::EffectiveConnectionType> web_holdback_ect =
GetWebHoldbackEffectiveConnectionType();
int effective_connection_type;
if (web_holdback_ect.has_value()) {
effective_connection_type = web_holdback_ect.value();
} else if (network_quality_tracker) {
effective_connection_type =
static_cast<int>(network_quality_tracker->GetEffectiveConnectionType());
} else {
effective_connection_type =
static_cast<int>(net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
}
SetHeaderToString(
headers, WebClientHintsType::kEct_DEPRECATED,
network::kWebEffectiveConnectionTypeMapping[effective_connection_type]);
}
void AddPrefersColorSchemeHeader(net::HttpRequestHeaders* headers,
FrameTreeNode* frame_tree_node) {
if (!frame_tree_node)
return;
blink::mojom::PreferredColorScheme preferred_color_scheme =
WebContents::FromRenderFrameHost(frame_tree_node->current_frame_host())
->GetOrCreateWebPreferences()
.preferred_color_scheme;
bool is_dark_mode =
preferred_color_scheme == blink::mojom::PreferredColorScheme::kDark;
SetHeaderToString(headers, WebClientHintsType::kPrefersColorScheme,
is_dark_mode ? network::kPrefersColorSchemeDark
: network::kPrefersColorSchemeLight);
}
void AddPrefersReducedMotionHeader(net::HttpRequestHeaders* headers,
FrameTreeNode* frame_tree_node) {
if (!frame_tree_node)
return;
bool prefers_reduced_motion =
WebContents::FromRenderFrameHost(frame_tree_node->current_frame_host())
->GetOrCreateWebPreferences()
.prefers_reduced_motion;
SetHeaderToString(headers, WebClientHintsType::kPrefersReducedMotion,
prefers_reduced_motion
? network::kPrefersReducedMotionReduce
: network::kPrefersReducedMotionNoPreference);
}
bool IsValidURLForClientHints(const url::Origin& origin) {
return network::IsOriginPotentiallyTrustworthy(origin);
}
bool UserAgentClientHintEnabled() {
return base::FeatureList::IsEnabled(blink::features::kUserAgentClientHint);
}
void AddUAHeader(net::HttpRequestHeaders* headers,
WebClientHintsType type,
const std::string& value) {
SetHeaderToString(headers, type, value);
}
// Creates a serialized string header value out of the input type, using
// structured headers as described in
// https://www.rfc-editor.org/rfc/rfc8941.html.
template <typename T>
const std::string SerializeHeaderString(const T& value) {
return net::structured_headers::SerializeItem(
net::structured_headers::Item(value))
.value_or(std::string());
}
// Returns true iff the `url` is embedded inside a frame that has the
// corresponding Sec-CH-UA-Reduced or Sec-CH-UA-Full client hint and thus, is
// enrolled in the UserAgentReduction or SendFullUserAgentAfterReduction
// Origin Trial.
//
// TODO(crbug.com/1258063): Remove when the UserAgentReduction and
// SendFullUserAgentAfterReduction Origin Trial is finished.
bool IsOriginTrialHintEnabledForFrame(
const url::Origin& origin,
const url::Origin& outermost_main_frame_origin,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate,
WebClientHintsType hint_type) {
// TODO(crbug.com/1300194): Refactor away from the use of FrameTreeNode
RenderFrameHostImpl* current = frame_tree_node->current_frame_host();
while (current) {
const url::Origin& current_origin =
(current == frame_tree_node->current_frame_host())
? origin
: current->GetLastCommittedOrigin();
// Don't use Sec-CH-UA-Reduced or Sec-CH-UA-Full from third-party origins if
// third-party cookies are blocked, so that we don't reveal any more user
// data than is allowed by the cookie settings.
if (outermost_main_frame_origin.IsSameOriginWith(current_origin) ||
!delegate->AreThirdPartyCookiesBlocked(current_origin.GetURL(),
current)) {
blink::EnabledClientHints current_url_hints;
delegate->GetAllowedClientHintsFromSource(current_origin,
&current_url_hints);
if (base::Contains(current_url_hints.GetEnabledHints(), hint_type))
return true;
}
current = current->GetParent();
}
return false;
}
// TODO(crbug.com/1258063): Delete this function when the UserAgentReduction and
// SendFullUserAgentAfterReduction Origin Trial is finished.
void RemoveAllClientHintsExceptOriginTrialHints(
const url::Origin& origin,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate,
std::vector<WebClientHintsType>* accept_ch,
url::Origin* outermost_main_frame_origin,
absl::optional<url::Origin>* third_party_origin) {
RenderFrameHostImpl* outermost_main_frame =
frame_tree_node->frame_tree().GetMainFrame()->GetOutermostMainFrame();
for (auto it = accept_ch->begin(); it != accept_ch->end();) {
if (*it == WebClientHintsType::kUAReduced ||
*it == WebClientHintsType::kFullUserAgent) {
++it;
} else {
it = accept_ch->erase(it);
}
}
if (!outermost_main_frame->GetLastCommittedOrigin().IsSameOriginWith(
origin)) {
// If third-party cookeis are blocked, we will not persist the
// Sec-CH-UA-Reduced client hint in a third-party context.
if (delegate->AreThirdPartyCookiesBlocked(
origin.GetURL(), frame_tree_node->current_frame_host())) {
accept_ch->clear();
return;
}
// Third-party contexts need the correct main frame URL and third-party
// URL in order to validate the Origin Trial token correctly, if present.
*outermost_main_frame_origin =
outermost_main_frame->GetLastCommittedOrigin();
*third_party_origin = absl::make_optional(origin);
}
}
// Captures the state used in applying client hints.
struct ClientHintsExtendedData {
ClientHintsExtendedData(const url::Origin& origin,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate,
const absl::optional<GURL>& maybe_request_url)
: resource_origin(origin) {
// If the current frame is the outermost main frame, the URL wasn't
// committed yet, so in order to get the main frame URL, we should use the
// provided URL instead. Otherwise, the current frame is a subframe and the
// outermost main frame URL was committed, so we can safely get it from it.
// Similarly, an in-navigation outermost main frame doesn't yet have a
// permissions policy.
is_outermost_main_frame =
!frame_tree_node || frame_tree_node->IsOutermostMainFrame();
if (is_outermost_main_frame) {
outermost_main_frame_origin = resource_origin;
is_1p_origin = true;
} else if (frame_tree_node->IsInFencedFrameTree()) {
permissions_policy = blink::PermissionsPolicy::CreateForFencedFrame(
resource_origin, frame_tree_node->GetFencedFrameMode().value());
} else {
RenderFrameHostImpl* outermost_main_frame =
frame_tree_node->frame_tree().GetMainFrame()->GetOutermostMainFrame();
outermost_main_frame_origin =
outermost_main_frame->GetLastCommittedOrigin();
permissions_policy = blink::PermissionsPolicy::CopyStateFrom(
outermost_main_frame->permissions_policy());
is_1p_origin = resource_origin.IsSameOriginWith(
outermost_main_frame->GetLastCommittedOrigin());
}
const base::TimeTicks start_time = base::TimeTicks::Now();
delegate->GetAllowedClientHintsFromSource(outermost_main_frame_origin,
&hints);
const base::TimeTicks pref_read_time = base::TimeTicks::Now();
// If this is a prerender tree, also capture prerender local setting. The
// setting was given by navigation requests on the prerendering page, and
// has not been used as a global setting.
if (frame_tree_node && frame_tree_node->frame_tree().is_prerendering()) {
// If prerender host is nullptr, it means prerender has been canceled and
// the host will be discarded soon, so we do not need to continue.
if (auto* host = PrerenderHost::GetPrerenderHostFromFrameTreeNode(
*frame_tree_node))
host->GetAllowedClientHintsOnPage(outermost_main_frame_origin, &hints);
}
const base::TimeTicks prerender_host_time = base::TimeTicks::Now();
// If this is not a top-level frame, then check if any of the ancestors
// in the path that led to this request have Sec-CH-UA-Reduced set.
// TODO(crbug.com/1258063): Remove once the UserAgentReduction Origin Trial
// is finished.
if (frame_tree_node && !is_outermost_main_frame) {
url::Origin trial_origin =
maybe_request_url ? url::Origin::Create(maybe_request_url.value())
: origin;
is_embedder_ua_reduced = IsOriginTrialHintEnabledForFrame(
trial_origin, outermost_main_frame_origin, frame_tree_node, delegate,
WebClientHintsType::kUAReduced);
is_embedder_ua_full = IsOriginTrialHintEnabledForFrame(
trial_origin, outermost_main_frame_origin, frame_tree_node, delegate,
WebClientHintsType::kFullUserAgent);
}
// Record the time spent getting the client hints.
const base::TimeTicks end_time = base::TimeTicks::Now();
base::UmaHistogramMicrosecondsTimes("ClientHints.FetchLatency_PrefRead",
pref_read_time - start_time);
base::UmaHistogramMicrosecondsTimes(
"ClientHints.FetchLatency_PrerenderHost",
prerender_host_time - pref_read_time);
base::UmaHistogramMicrosecondsTimes(
"ClientHints.FetchLatency_OriginTrialCheck",
end_time - prerender_host_time);
base::UmaHistogramMicrosecondsTimes("ClientHints.FetchLatency_Total",
end_time - start_time);
}
blink::EnabledClientHints hints;
// If true, one of the ancestor requests in the path to this request had
// Sec-CH-UA-Reduced in their Accept-CH cache. Only applies to embedded
// requests (top-level requests will always set this to false).
//
// If an embedder of a request has Sec-CH-UA-Reduced, it means it will
// receive the reduced User-Agent header, so we want to also send the reduced
// User-Agent for the embedded request as well.
bool is_embedder_ua_reduced = false;
// If true, one of the ancestor requests in the path to this request had
// Sec-CH-UA-Full in their Accept-CH cache. Only applies to embedded
// requests (top-level requests will always set this to false).
//
// If an embedder of a request has Sec-CH-UA-Full, it means it will
// receive the full User-Agent header, so we want to also send the full
// User-Agent for the embedded request as well.
bool is_embedder_ua_full = false;
url::Origin resource_origin;
bool is_outermost_main_frame = false;
url::Origin outermost_main_frame_origin;
std::unique_ptr<blink::PermissionsPolicy> permissions_policy;
bool is_1p_origin = false;
};
bool SkipPermissionPolicyCheck(WebClientHintsType type) {
return type == WebClientHintsType::kUAReduced ||
type == WebClientHintsType::kFullUserAgent;
}
bool IsClientHintEnabled(const ClientHintsExtendedData& data,
WebClientHintsType type) {
return blink::IsClientHintSentByDefault(type) || data.hints.IsEnabled(type) ||
(type == WebClientHintsType::kUAReduced &&
data.is_embedder_ua_reduced) ||
(type == WebClientHintsType::kFullUserAgent &&
data.is_embedder_ua_full);
}
bool IsClientHintAllowed(const ClientHintsExtendedData& data,
WebClientHintsType type) {
if (data.is_outermost_main_frame) {
return data.is_1p_origin;
}
return SkipPermissionPolicyCheck(type) ||
(data.permissions_policy->IsFeatureEnabledForOrigin(
blink::GetClientHintToPolicyFeatureMap().at(type),
data.resource_origin));
}
bool ShouldAddClientHint(const ClientHintsExtendedData& data,
WebClientHintsType type) {
return IsClientHintEnabled(data, type) && IsClientHintAllowed(data, type);
}
bool IsJavascriptEnabled(FrameTreeNode* frame_tree_node) {
return WebContents::FromRenderFrameHost(frame_tree_node->current_frame_host())
->GetOrCreateWebPreferences()
.javascript_enabled;
}
// This modifies `data.permissions_policy` to reflect any changes to client hint
// permissions which may have occurred via the named accept-ch meta tag.
// The permissions policy the browser side has for the frame was set in stone
// before HTML parsing began, so any updates must be sent via
// `container_policy`.
// TODO(crbug.com/1278127): Replace w/ generic HTML policy modification.
void UpdateIFramePermissionsPolicyWithDelegationSupportForClientHints(
ClientHintsExtendedData& data,
const blink::ParsedPermissionsPolicy& container_policy) {
if (container_policy.empty() ||
!base::FeatureList::IsEnabled(
blink::features::kClientHintThirdPartyDelegation)) {
return;
}
// For client hints specifically, we need to allow the container policy
// to overwrite the parent policy so that permissions policies set in HTML
// via an accept-ch meta tag can be respected.
blink::ParsedPermissionsPolicy client_hints_container_policy;
for (const auto& container_policy_item : container_policy) {
const auto& it = blink::GetPolicyFeatureToClientHintMap().find(
container_policy_item.feature);
if (it != blink::GetPolicyFeatureToClientHintMap().end()) {
client_hints_container_policy.push_back(container_policy_item);
// We need to ensure `blink::EnabledClientHints` is updated where the
// main frame now has permission for the given client hints.
for (const auto& origin_with_possible_wildcards :
container_policy_item.allowed_origins) {
if (origin_with_possible_wildcards.DoesMatchOrigin(
data.outermost_main_frame_origin)) {
for (const auto& hint : it->second) {
data.hints.SetIsEnabled(hint, /*should_send*/ true);
}
break;
}
}
}
}
data.permissions_policy->OverwriteHeaderPolicyForClientHints(
client_hints_container_policy);
}
// Captures when UpdateNavigationRequestClientUaHeadersImpl() is being called.
enum class ClientUaHeaderCallType {
// The call is happening during creation of the NavigationRequest.
kDuringCreation,
// The call is happening after creation of the NavigationRequest.
kAfterCreated,
};
// Implementation of UpdateNavigationRequestClientUaHeaders().
void UpdateNavigationRequestClientUaHeadersImpl(
ClientHintsControllerDelegate* delegate,
bool override_ua,
FrameTreeNode* frame_tree_node,
ClientUaHeaderCallType call_type,
net::HttpRequestHeaders* headers,
const blink::ParsedPermissionsPolicy& container_policy,
const absl::optional<GURL>& request_url,
const ClientHintsExtendedData& data) {
absl::optional<blink::UserAgentMetadata> ua_metadata;
bool disable_due_to_custom_ua = false;
if (override_ua) {
NavigatorDelegate* nav_delegate =
frame_tree_node ? frame_tree_node->navigator().GetDelegate() : nullptr;
ua_metadata =
nav_delegate ? nav_delegate->GetUserAgentOverride().ua_metadata_override
: absl::nullopt;
// If a custom UA override is set, but no value is provided for UA client
// hints, disable them.
disable_due_to_custom_ua = !ua_metadata.has_value();
}
if (frame_tree_node &&
devtools_instrumentation::ApplyUserAgentMetadataOverrides(frame_tree_node,
&ua_metadata)) {
// Likewise, if devtools says to override client hints but provides no
// value, disable them. This overwrites previous decision from UI.
disable_due_to_custom_ua = !ua_metadata.has_value();
}
if (!disable_due_to_custom_ua) {
if (!ua_metadata.has_value())
ua_metadata = delegate->GetUserAgentMetadata();
// The `Sec-CH-UA` client hint is attached to all outgoing requests. This is
// (intentionally) different than other client hints.
// It's barred behind ShouldAddClientHints to make sure it's controlled by
// Permissions Policy.
//
// https://wicg.github.io/client-hints-infrastructure/#abstract-opdef-append-client-hints-to-request
if (ShouldAddClientHint(data, WebClientHintsType::kUA)) {
AddUAHeader(headers, WebClientHintsType::kUA,
ua_metadata->SerializeBrandMajorVersionList());
}
// The `Sec-CH-UA-Mobile client hint was also deemed "low entropy" and can
// safely be sent with every request. Similarly to UA, ShouldAddClientHints
// makes sure it's controlled by Permissions Policy.
if (ShouldAddClientHint(data, WebClientHintsType::kUAMobile)) {
AddUAHeader(headers, WebClientHintsType::kUAMobile,
SerializeHeaderString(ua_metadata->mobile));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAFullVersion)) {
AddUAHeader(headers, WebClientHintsType::kUAFullVersion,
SerializeHeaderString(ua_metadata->full_version));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAArch)) {
AddUAHeader(headers, WebClientHintsType::kUAArch,
SerializeHeaderString(ua_metadata->architecture));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAPlatform)) {
AddUAHeader(headers, WebClientHintsType::kUAPlatform,
SerializeHeaderString(ua_metadata->platform));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAPlatformVersion)) {
AddUAHeader(headers, WebClientHintsType::kUAPlatformVersion,
SerializeHeaderString(ua_metadata->platform_version));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAModel)) {
AddUAHeader(headers, WebClientHintsType::kUAModel,
SerializeHeaderString(ua_metadata->model));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUABitness)) {
AddUAHeader(headers, WebClientHintsType::kUABitness,
SerializeHeaderString(ua_metadata->bitness));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAWoW64)) {
AddUAHeader(headers, WebClientHintsType::kUAWoW64,
SerializeHeaderString(ua_metadata->wow64));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAReduced)) {
AddUAHeader(headers, WebClientHintsType::kUAReduced,
SerializeHeaderString(true));
}
if (ShouldAddClientHint(data, WebClientHintsType::kUAFullVersionList)) {
AddUAHeader(headers, WebClientHintsType::kUAFullVersionList,
ua_metadata->SerializeBrandFullVersionList());
}
if (ShouldAddClientHint(data, WebClientHintsType::kFullUserAgent)) {
AddUAHeader(headers, WebClientHintsType::kFullUserAgent,
SerializeHeaderString(true));
}
} else if (call_type == ClientUaHeaderCallType::kAfterCreated) {
RemoveClientHintHeader(WebClientHintsType::kUA, headers);
RemoveClientHintHeader(WebClientHintsType::kUAMobile, headers);
RemoveClientHintHeader(WebClientHintsType::kUAFullVersion, headers);
RemoveClientHintHeader(WebClientHintsType::kUAArch, headers);
RemoveClientHintHeader(WebClientHintsType::kUAPlatform, headers);
RemoveClientHintHeader(WebClientHintsType::kUAPlatformVersion, headers);
RemoveClientHintHeader(WebClientHintsType::kUAModel, headers);
RemoveClientHintHeader(WebClientHintsType::kUABitness, headers);
RemoveClientHintHeader(WebClientHintsType::kUAReduced, headers);
RemoveClientHintHeader(WebClientHintsType::kUAFullVersionList, headers);
RemoveClientHintHeader(WebClientHintsType::kUAWoW64, headers);
RemoveClientHintHeader(WebClientHintsType::kFullUserAgent, headers);
}
}
} // namespace
bool ShouldAddClientHints(const url::Origin& origin,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate,
const absl::optional<GURL> maybe_request_url) {
url::Origin origin_to_check =
maybe_request_url ? url::Origin::Create(maybe_request_url.value())
: origin;
// Client hints should only be enabled when JavaScript is enabled. Platforms
// which enable/disable JavaScript on a per-origin basis should implement
// IsJavaScriptAllowed to check a given origin. Other platforms (Android
// WebView) enable/disable JavaScript on a per-View basis, using the
// WebPreferences setting.
return IsValidURLForClientHints(origin_to_check) &&
delegate->IsJavaScriptAllowed(
origin_to_check.GetURL(),
frame_tree_node ? frame_tree_node->GetParentOrOuterDocument()
: nullptr) &&
(!frame_tree_node || IsJavascriptEnabled(frame_tree_node));
}
unsigned long RoundRttForTesting(const std::string& host,
const absl::optional<base::TimeDelta>& rtt) {
return RoundRtt(host, rtt);
}
double RoundKbpsToMbpsForTesting(const std::string& host,
const absl::optional<int32_t>& downlink_kbps) {
return RoundKbpsToMbps(host, downlink_kbps);
}
void UpdateNavigationRequestClientUaHeaders(
const url::Origin& origin,
ClientHintsControllerDelegate* delegate,
bool override_ua,
FrameTreeNode* frame_tree_node,
net::HttpRequestHeaders* headers,
const absl::optional<GURL>& request_url) {
DCHECK(frame_tree_node);
if (!UserAgentClientHintEnabled() ||
!ShouldAddClientHints(origin, frame_tree_node, delegate, request_url)) {
return;
}
ClientHintsExtendedData data(origin, frame_tree_node, delegate, request_url);
UpdateNavigationRequestClientUaHeadersImpl(
delegate, override_ua, frame_tree_node,
ClientUaHeaderCallType::kAfterCreated, headers, {}, request_url, data);
}
namespace {
void AddRequestClientHintsHeaders(
const url::Origin& origin,
net::HttpRequestHeaders* headers,
BrowserContext* context,
ClientHintsControllerDelegate* delegate,
bool is_ua_override_on,
FrameTreeNode* frame_tree_node,
const blink::ParsedPermissionsPolicy& container_policy,
const absl::optional<GURL>& request_url) {
ClientHintsExtendedData data(origin, frame_tree_node, delegate, request_url);
UpdateIFramePermissionsPolicyWithDelegationSupportForClientHints(
data, container_policy);
GURL url = origin.GetURL();
// Add Headers
if (ShouldAddClientHint(data, WebClientHintsType::kDeviceMemory_DEPRECATED)) {
AddDeviceMemoryHeader(headers, /*use_deprecated_version*/ true);
}
if (ShouldAddClientHint(data, WebClientHintsType::kDeviceMemory)) {
AddDeviceMemoryHeader(headers);
}
if (ShouldAddClientHint(data, WebClientHintsType::kDpr_DEPRECATED)) {
AddDPRHeader(headers, context, url, /*use_deprecated_version*/ true);
}
if (ShouldAddClientHint(data, WebClientHintsType::kDpr)) {
AddDPRHeader(headers, context, url);
}
if (ShouldAddClientHint(data,
WebClientHintsType::kViewportWidth_DEPRECATED)) {
AddViewportWidthHeader(headers, context, url, frame_tree_node, delegate,
/*use_deprecated_version*/ true);
}
if (ShouldAddClientHint(data, WebClientHintsType::kViewportWidth)) {
AddViewportWidthHeader(headers, context, url, frame_tree_node, delegate);
}
if (ShouldAddClientHint(
data, network::mojom::WebClientHintsType::kViewportHeight)) {
AddViewportHeightHeader(headers, context, url, frame_tree_node, delegate);
}
network::NetworkQualityTracker* network_quality_tracker =
delegate->GetNetworkQualityTracker();
if (ShouldAddClientHint(data, WebClientHintsType::kRtt_DEPRECATED)) {
AddRttHeader(headers, network_quality_tracker, url);
}
if (ShouldAddClientHint(data, WebClientHintsType::kDownlink_DEPRECATED)) {
AddDownlinkHeader(headers, network_quality_tracker, url);
}
if (ShouldAddClientHint(data, WebClientHintsType::kEct_DEPRECATED)) {
AddEctHeader(headers, network_quality_tracker, url);
}
if (UserAgentClientHintEnabled()) {
UpdateNavigationRequestClientUaHeadersImpl(
delegate, is_ua_override_on, frame_tree_node,
ClientUaHeaderCallType::kDuringCreation, headers, container_policy,
request_url, data);
}
if (ShouldAddClientHint(data, WebClientHintsType::kPrefersColorScheme)) {
AddPrefersColorSchemeHeader(headers, frame_tree_node);
}
if (ShouldAddClientHint(data, WebClientHintsType::kPrefersReducedMotion)) {
AddPrefersReducedMotionHeader(headers, frame_tree_node);
}
if (ShouldAddClientHint(data, WebClientHintsType::kSaveData))
AddSaveDataHeader(headers, context);
// Static assert that triggers if a new client hint header is added. If a
// new client hint header is added, the following assertion should be updated.
// If possible, logic should be added above so that the request headers for
// the newly added client hint can be added to the request.
static_assert(
network::mojom::WebClientHintsType::kPrefersReducedMotion ==
network::mojom::WebClientHintsType::kMaxValue,
"Consider adding client hint request headers from the browser process");
// TODO(crbug.com/735518): If the request is redirected, the client hint
// headers stay attached to the redirected request. Consider removing/adding
// the client hints headers if the request is redirected with a change in
// scheme or a change in the origin.
}
} // namespace
void AddPrefetchNavigationRequestClientHintsHeaders(
const url::Origin& origin,
net::HttpRequestHeaders* headers,
BrowserContext* context,
ClientHintsControllerDelegate* delegate,
bool is_ua_override_on,
bool is_javascript_enabled) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));
DCHECK(context);
// Since prefetch navigation doesn't have a related frame tree node,
// |is_javascript_enabled| is passed in to get whether a typical frame tree
// node would support javascript.
if (!is_javascript_enabled ||
!ShouldAddClientHints(origin, nullptr, delegate)) {
return;
}
AddRequestClientHintsHeaders(origin, headers, context, delegate,
is_ua_override_on, nullptr, {}, absl::nullopt);
}
void AddNavigationRequestClientHintsHeaders(
const url::Origin& origin,
net::HttpRequestHeaders* headers,
BrowserContext* context,
ClientHintsControllerDelegate* delegate,
bool is_ua_override_on,
FrameTreeNode* frame_tree_node,
const blink::ParsedPermissionsPolicy& container_policy,
const absl::optional<GURL>& request_url) {
DCHECK(frame_tree_node);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));
DCHECK(context);
if (!ShouldAddClientHints(origin, frame_tree_node, delegate, request_url)) {
return;
}
AddRequestClientHintsHeaders(origin, headers, context, delegate,
is_ua_override_on, frame_tree_node,
container_policy, request_url);
}
absl::optional<std::vector<WebClientHintsType>>
ParseAndPersistAcceptCHForNavigation(
const url::Origin& origin,
const network::mojom::ParsedHeadersPtr& parsed_headers,
const net::HttpResponseHeaders* response_headers,
BrowserContext* context,
ClientHintsControllerDelegate* delegate,
FrameTreeNode* frame_tree_node) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(context);
DCHECK(parsed_headers);
if (!parsed_headers->accept_ch)
return absl::nullopt;
if (!IsValidURLForClientHints(origin))
return absl::nullopt;
// Client hints should only be enabled when JavaScript is enabled. Platforms
// which enable/disable JavaScript on a per-origin basis should implement
// IsJavaScriptAllowed to check a given origin. Other platforms (Android
// WebView) enable/disable JavaScript on a per-View basis, using the
// WebPreferences setting.
if (!delegate->IsJavaScriptAllowed(
origin.GetURL(), frame_tree_node->GetParentOrOuterDocument()) ||
!IsJavascriptEnabled(frame_tree_node)) {
return absl::nullopt;
}
std::vector<WebClientHintsType> accept_ch = parsed_headers->accept_ch.value();
url::Origin main_frame_origin = origin;
absl::optional<url::Origin> third_party_origin;
// Only the main frame should parse accept-CH, except for the temporary
// Sec-CH-UA-Reduced client hint (used for the User-Agent reduction origin
// trial).
//
// Note that if Sec-CH-UA-Reduced is persisted for an embedded frame, it
// means a subsequent top-level navigation will read Sec-CH-UA-Reduced from
// the Accept-CH cache and send a reduced User-Agent string.
//
// TODO(crbug.com/1258063): Delete this call when the UserAgentReduction
// Origin Trial is finished.
if (!frame_tree_node->IsMainFrame()) {
RemoveAllClientHintsExceptOriginTrialHints(
origin, frame_tree_node, delegate, &accept_ch, &main_frame_origin,
&third_party_origin);
if (accept_ch.empty()) {
// There are is no Sec-CH-UA-Reduced in Accept-CH for the embedded frame,
// so nothing should be persisted.
return absl::nullopt;
}
}
absl::optional<GURL> third_party_url = absl::nullopt;
if (third_party_origin)
third_party_url = third_party_origin->GetURL();
blink::EnabledClientHints enabled_hints;
for (const WebClientHintsType type : accept_ch) {
enabled_hints.SetIsEnabled(main_frame_origin.GetURL(), third_party_url,
response_headers, type, true);
}
const std::vector<WebClientHintsType> persisted_hints =
enabled_hints.GetEnabledHints();
DCHECK(frame_tree_node);
PersistAcceptCH(origin, *frame_tree_node, delegate, persisted_hints);
return persisted_hints;
}
void PersistAcceptCH(const url::Origin& origin,
FrameTreeNode& frame_tree_node,
ClientHintsControllerDelegate* delegate,
const std::vector<WebClientHintsType>& hints) {
DCHECK(delegate);
// TODO(https://crbug.com/1355279): Moving the if condition from the caller
// into this function after PrerenderHost becomes a FrameTreeDelegate. A
// clearer pattern should be to check whether it is in a prerendering tree
// and return a nullptr if it isn't. However, the current prerender
// implementation returns a nullptr in two cases: not prerendered or
// prerender is canceled, and the callers cannot distinguish between the two
// reasons and have to have another if condition.
if (frame_tree_node.frame_tree().is_prerendering()) {
// For prerendering headers, it should not persist the client header until
// activation, considering user has not visited the page and allowed it to
// change content setting yet. The client hints should apply to navigations
// in the prerendering page, and propagate to the global setting upon user
// navigation.
// If host is nullptr, it means prerender has been canceled and will be
// deleted soon, so we do not need to persist anything.
if (auto* host =
PrerenderHost::GetPrerenderHostFromFrameTreeNode(frame_tree_node)) {
host->OnAcceptClientHintChanged(origin, hints);
}
return;
}
delegate->PersistClientHints(
origin, frame_tree_node.GetParentOrOuterDocument(), hints);
}
std::vector<WebClientHintsType> LookupAcceptCHForCommit(
const url::Origin& origin,
ClientHintsControllerDelegate* delegate,
FrameTreeNode* frame_tree_node,
const absl::optional<GURL>& request_url) {
std::vector<WebClientHintsType> result;
if (!ShouldAddClientHints(origin, frame_tree_node, delegate, request_url)) {
return result;
}
const ClientHintsExtendedData data(origin, frame_tree_node, delegate,
request_url);
std::vector<WebClientHintsType> hints = data.hints.GetEnabledHints();
if (data.is_embedder_ua_reduced &&
!base::Contains(hints, WebClientHintsType::kUAReduced)) {
hints.push_back(WebClientHintsType::kUAReduced);
}
if (data.is_embedder_ua_full &&
!base::Contains(hints, WebClientHintsType::kFullUserAgent)) {
hints.push_back(WebClientHintsType::kFullUserAgent);
}
return hints;
}
bool AreCriticalHintsMissing(
const url::Origin& origin,
FrameTreeNode* frame_tree_node,
ClientHintsControllerDelegate* delegate,
const std::vector<WebClientHintsType>& critical_hints) {
ClientHintsExtendedData data(origin, frame_tree_node, delegate,
absl::nullopt);
// Note: these only check for per-hint origin/permissions policy settings, not
// origin-level or "browser-level" policies like disabiling JS or other
// features.
for (auto hint : critical_hints) {
if (IsClientHintAllowed(data, hint) && !IsClientHintEnabled(data, hint)) {
return true;
}
}
return false;
}
} // namespace content