blob: eb302bc5447dc4b93ac33e5bd27d17a768797281 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cmath>
#include <functional>
#include <string>
#include "chrome/browser/client_hints/client_hints.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/field_trial_params.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/client_hints/client_hints.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_features.h"
#include "content/public/common/origin_util.h"
#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "net/nqe/effective_connection_type.h"
#include "net/nqe/network_quality_estimator_params.h"
#include "net/url_request/url_request.h"
#include "services/network/public/cpp/network_quality_tracker.h"
#include "third_party/blink/public/common/client_hints/client_hints.h"
#include "third_party/blink/public/common/device_memory/approximated_device_memory.h"
#include "third_party/blink/public/platform/web_client_hints_type.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "url/gurl.h"
#if !defined(OS_ANDROID)
#include "content/public/browser/host_zoom_map.h"
#include "content/public/common/page_zoom.h"
#endif // !OS_ANDROID
namespace {
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;
}
bool IsJavaScriptAllowed(Profile* profile, const GURL& url) {
return HostContentSettingsMapFactory::GetForProfile(profile)
->GetContentSetting(url, url, CONTENT_SETTINGS_TYPE_JAVASCRIPT,
std::string()) == CONTENT_SETTING_ALLOW;
}
// Returns the zoom factor for a given |url|.
double GetZoomFactor(content::BrowserContext* context, const GURL& url) {
// Android does not have the concept of zooming in like desktop.
#if defined(OS_ANDROID)
return 1.0;
#else
double zoom_level = content::HostZoomMap::GetDefaultForBrowserContext(context)
->GetZoomLevelForHostAndScheme(
url.scheme(), net::GetHostOrSpecFromURL(url));
if (zoom_level == 0.0) {
// Get default zoom level.
zoom_level = content::HostZoomMap::GetDefaultForBrowserContext(context)
->GetDefaultZoomLevel();
}
return content::ZoomLevelToZoomFactor(zoom_level);
#endif
}
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 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.
base::Optional<net::EffectiveConnectionType>
GetWebHoldbackEffectiveConnectionType() {
if (!base::FeatureList::IsEnabled(
features::kNetworkQualityEstimatorWebHoldback)) {
return base::nullopt;
}
std::string effective_connection_type_param =
base::GetFieldTrialParamValueByFeature(
features::kNetworkQualityEstimatorWebHoldback,
"web_effective_connection_type_override");
base::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 base::nullopt;
DCHECK_NE(net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN,
effective_connection_type.value());
return effective_connection_type;
}
} // namespace
namespace client_hints {
namespace internal {
unsigned long RoundRtt(const std::string& host,
const base::Optional<base::TimeDelta>& rtt) {
// Limit the size of the buckets and the maximum reported value to reduce
// fingerprinting.
static const size_t kGranularityMsec = 50;
static const double kMaxRttMsec = 3.0 * 1000;
if (!rtt.has_value()) {
// RTT is unavailable. So, return the fastest value.
return 0;
}
double rtt_msec = static_cast<double>(rtt.value().InMilliseconds());
rtt_msec *= GetRandomMultiplier(host);
rtt_msec = std::min(rtt_msec, kMaxRttMsec);
DCHECK_LE(0, rtt_msec);
DCHECK_GE(kMaxRttMsec, rtt_msec);
// Round down to the nearest kBucketSize msec value.
return std::round(rtt_msec / kGranularityMsec) * kGranularityMsec;
}
double RoundKbpsToMbps(const std::string& host,
const base::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;
}
} // namespace internal
ClientHints::ClientHints(content::BrowserContext* context)
: context_(context) {}
ClientHints::~ClientHints() = default;
void ClientHints::GetAdditionalNavigationRequestClientHintsHeaders(
const GURL& url,
net::HttpRequestHeaders* additional_headers) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_EQ(blink::kWebEffectiveConnectionTypeMappingCount,
net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
DCHECK_EQ(blink::kWebEffectiveConnectionTypeMappingCount,
static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));
// Get the client hint headers.
if (!url.is_valid())
return;
if (!url.SchemeIsHTTPOrHTTPS())
return;
if (url.SchemeIs(url::kHttpScheme) && !net::IsLocalhost(url))
return;
DCHECK(url.SchemeIs(url::kHttpsScheme) ||
(url.SchemeIs(url::kHttpScheme) && net::IsLocalhost(url)));
Profile* profile = Profile::FromBrowserContext(context_);
if (!profile)
return;
// Check if |url| is allowed to run JavaScript. If not, client hints are not
// attached to the requests that initiate on the browser side.
if (!IsJavaScriptAllowed(profile, url))
return;
ContentSettingsForOneType client_hints_host_settings;
HostContentSettingsMapFactory::GetForProfile(profile)->GetSettingsForOneType(
CONTENT_SETTINGS_TYPE_CLIENT_HINTS, std::string(),
&client_hints_host_settings);
blink::WebEnabledClientHints web_client_hints;
GetAllowedClientHintsFromSource(
url /* resource url */, client_hints_host_settings, &web_client_hints);
// Currently, only "device-memory" client hint request header is added from
// the browser process.
if (web_client_hints.IsEnabled(
blink::mojom::WebClientHintsType::kDeviceMemory)) {
// Initialize device memory if it's not already.
blink::ApproximatedDeviceMemory::Initialize();
const float device_memory =
blink::ApproximatedDeviceMemory::GetApproximatedDeviceMemory();
DCHECK_LT(0.0, device_memory);
additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kDeviceMemory)],
DoubleToSpecCompliantString(device_memory));
}
if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kDpr)) {
double device_scale_factor = GetDeviceScaleFactor();
double zoom_factor = GetZoomFactor(context_, url);
additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kDpr)],
DoubleToSpecCompliantString(device_scale_factor * zoom_factor));
}
if (web_client_hints.IsEnabled(
blink::mojom::WebClientHintsType::kViewportWidth)) {
// The default value on Android. See
// https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/viewportAndroid.css.
double viewport_width = 980;
#if !defined(OS_ANDROID)
double device_scale_factor = GetDeviceScaleFactor();
viewport_width = (display::Screen::GetScreen()
->GetPrimaryDisplay()
.GetSizeInPixel()
.width()) /
GetZoomFactor(context_, url) / device_scale_factor;
#endif // !OS_ANDROID
DCHECK_LT(0, viewport_width);
if (viewport_width > 0) {
additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kViewportWidth)],
base::NumberToString(std::round(viewport_width)));
}
}
network::NetworkQualityTracker* network_quality_tracker =
g_browser_process->network_quality_tracker();
if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kRtt)) {
base::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 {
http_rtt = network_quality_tracker->GetHttpRTT();
}
additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kRtt)],
base::NumberToString(internal::RoundRtt(url.host(), http_rtt)));
}
if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kDownlink)) {
base::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 {
downlink_throughput_kbps =
network_quality_tracker->GetDownstreamThroughputKbps();
}
additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kDownlink)],
DoubleToSpecCompliantString(
internal::RoundKbpsToMbps(url.host(), downlink_throughput_kbps)));
}
if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kEct)) {
DCHECK_EQ(blink::kWebEffectiveConnectionTypeMappingCount,
net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
DCHECK_EQ(blink::kWebEffectiveConnectionTypeMappingCount,
static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));
base::Optional<net::EffectiveConnectionType> web_holdback_ect =
GetWebHoldbackEffectiveConnectionType();
int effective_connection_type =
web_holdback_ect.has_value()
? web_holdback_ect.value()
: static_cast<int>(
network_quality_tracker->GetEffectiveConnectionType());
additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kEct)],
blink::kWebEffectiveConnectionTypeMapping[effective_connection_type]);
}
// 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(
blink::mojom::WebClientHintsType::kEct ==
blink::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 client_hints