blob: b67b22928c67ffa923357427f8c0b244338abd33 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/variations/net/variations_http_headers.h"
#include <utility>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "components/google/core/common/google_util.h"
#include "components/variations/variations_features.h"
#include "components/variations/variations_ids_provider.h"
#include "net/base/isolation_info.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_IOS)
#include "base/command_line.h"
#include "components/variations/net/variations_flags.h"
#include "net/base/url_util.h"
#endif // BUILDFLAG(IS_IOS)
namespace variations {
// The name string for the header for variations information.
// Note that prior to M33 this header was named X-Chrome-Variations.
const char kClientDataHeader[] = "X-Client-Data";
namespace {
// The result of checking whether a request to a URL should have variations
// headers appended to it.
//
// This enum is used to record UMA histogram values, and should not be
// reordered.
enum class URLValidationResult {
kNotValidInvalidUrl = 0,
// kNotValidNotHttps = 1, // Deprecated.
kNotValidNotGoogleDomain = 2,
kShouldAppend = 3,
kNotValidNeitherHttpHttps = 4,
kNotValidIsGoogleNotHttps = 5,
kMaxValue = kNotValidIsGoogleNotHttps,
};
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum RequestContextCategory {
// First-party contexts.
kBrowserInitiated = 0,
kInternalChromePageInitiated = 1,
kGooglePageInitiated = 2,
kGoogleSubFrameOnGooglePageInitiated = 3,
// Third-party contexts.
kNonGooglePageInitiated = 4,
// Deprecated because the histogram Variations.Headers.DomainOwner stores
// more finely-grained information about this case.
// kNoTrustedParams = 5,
kNoIsolationInfo = 6,
kGoogleSubFrameOnNonGooglePageInitiated = 7,
// Deprecated because this category wasn't necessary in the first place. It's
// covered by kNonGooglePageInitiated.
// kNonGooglePageInitiatedFromFrameOrigin = 8,
// The next RequestContextCategory should use 9.
kMaxValue = kGoogleSubFrameOnNonGooglePageInitiated,
};
void LogRequestContextHistogram(RequestContextCategory result) {
base::UmaHistogramEnumeration("Variations.Headers.RequestContextCategory",
result);
}
// Returns a URLValidationResult for |url|. A valid URL for headers has the
// following qualities: (i) it is well-formed, (ii) its scheme is HTTPS, and
// (iii) it has a Google-associated domain.
// On iOS, it is possible to pass the headers to localhost request if a flag
// is passed. This is needed for tests as EmbeddedTestServer is only
// accessible using 127.0.0.1. See crrev.com/c/3507791 for details.
URLValidationResult GetUrlValidationResult(const GURL& url) {
if (!url.is_valid()) {
return URLValidationResult::kNotValidInvalidUrl;
}
if (!url.SchemeIsHTTPOrHTTPS()) {
return URLValidationResult::kNotValidNeitherHttpHttps;
}
#if BUILDFLAG(IS_IOS)
if (net::IsLocalhost(url) &&
base::CommandLine::ForCurrentProcess()->HasSwitch(
kAppendVariationsHeadersToLocalhostForTesting)) {
return URLValidationResult::kShouldAppend;
}
#endif // BUILDFLAG(IS_IOS)
if (!google_util::IsGoogleAssociatedDomainUrl(url)) {
return URLValidationResult::kNotValidNotGoogleDomain;
}
// HTTPS is checked here, rather than before the IsGoogleAssociatedDomainUrl()
// check, to know how many Google domains are rejected by the change to append
// headers to only HTTPS requests.
if (!url.SchemeIs(url::kHttpsScheme)) {
return URLValidationResult::kNotValidIsGoogleNotHttps;
}
return URLValidationResult::kShouldAppend;
}
// Returns true if the request to |url| should include a variations header.
// Also, logs the result of validating |url| in histograms, one of which ends in
// |suffix|.
bool ShouldAppendVariationsHeader(const GURL& url, const std::string& suffix) {
URLValidationResult result = GetUrlValidationResult(url);
base::UmaHistogramEnumeration(
"Variations.Headers.URLValidationResult." + suffix, result);
return result == URLValidationResult::kShouldAppend;
}
// Returns true if the request is sent from a Google web property, i.e. from a
// first-party context.
//
// The context is determined using |owner| and |resource_request|. |owner| is
// used for subframe-initiated subresource requests from the renderer. Note that
// for these kinds of requests, ResourceRequest::TrustedParams is not populated.
bool IsFirstPartyContext(Owner owner,
const network::ResourceRequest& resource_request) {
if (!resource_request.request_initiator) {
// The absence of |request_initiator| means that the request was initiated
// by the browser, e.g. a request from the browser to Autofill upon form
// detection.
LogRequestContextHistogram(RequestContextCategory::kBrowserInitiated);
return true;
}
const GURL request_initiator_url =
resource_request.request_initiator->GetURL();
if (request_initiator_url.SchemeIs("chrome-search") ||
request_initiator_url.SchemeIs("chrome")) {
// A scheme matching the above patterns means that the request was
// initiated by an internal page, e.g. a request from
// chrome://newtab/ for App Launcher resources.
LogRequestContextHistogram(kInternalChromePageInitiated);
return true;
}
if (GetUrlValidationResult(request_initiator_url) !=
URLValidationResult::kShouldAppend) {
// The request was initiated by a non-Google-associated page, e.g. a request
// from https://www.bbc.com/.
LogRequestContextHistogram(kNonGooglePageInitiated);
return false;
}
if (resource_request.is_outermost_main_frame) {
// The request is from a Google-associated page--not a subframe--e.g. a
// request from https://calendar.google.com/.
LogRequestContextHistogram(kGooglePageInitiated);
return true;
}
// |is_outermost_main_frame| is false, so the request was initiated by a
// subframe (or embedded main frame like a fenced frame), and we need to
// determine whether the top-level page in which the frame is embedded is a
// Google-owned web property.
//
// If TrustedParams is populated, then we can use it to determine the request
// context. If not, e.g. for subresource requests, we use |owner|.
if (resource_request.trusted_params) {
const net::IsolationInfo* isolation_info =
&resource_request.trusted_params->isolation_info;
if (isolation_info->IsEmpty()) {
// TODO(crbug.com/40135370): If TrustedParams are present, it appears that
// IsolationInfo is too. Maybe deprecate kNoIsolationInfo if this bucket
// is never used.
LogRequestContextHistogram(kNoIsolationInfo);
// Without IsolationInfo, we cannot be certain that the request is from a
// first-party context.
return false;
}
if (GetUrlValidationResult(isolation_info->top_frame_origin()->GetURL()) ==
URLValidationResult::kShouldAppend) {
// The request is from a Google-associated subframe on a Google-associated
// page, e.g. a request from a Docs subframe on https://drive.google.com/.
LogRequestContextHistogram(kGoogleSubFrameOnGooglePageInitiated);
return true;
}
// The request is from a Google-associated subframe on a non-Google-
// associated page, e.g. a request to DoubleClick from an ad's subframe on
// https://www.lexico.com/.
LogRequestContextHistogram(kGoogleSubFrameOnNonGooglePageInitiated);
return false;
}
base::UmaHistogramEnumeration("Variations.Headers.DomainOwner", owner);
if (owner == Owner::kGoogle) {
LogRequestContextHistogram(kGoogleSubFrameOnGooglePageInitiated);
return true;
}
LogRequestContextHistogram(kGoogleSubFrameOnNonGooglePageInitiated);
return false;
}
// Returns GoogleWebVisibility::FIRST_PARTY if the request is from a first-party
// context; otherwise, returns GoogleWebVisibility::ANY.
variations::mojom::GoogleWebVisibility GetVisibilityKey(
Owner owner,
const network::ResourceRequest& resource_request) {
return IsFirstPartyContext(owner, resource_request)
? variations::mojom::GoogleWebVisibility::FIRST_PARTY
: variations::mojom::GoogleWebVisibility::ANY;
}
// Returns a variations header from |variations_headers|.
std::string SelectVariationsHeader(
variations::mojom::VariationsHeadersPtr variations_headers,
Owner owner,
const network::ResourceRequest& resource_request) {
return variations_headers->headers_map.at(
GetVisibilityKey(owner, resource_request));
}
class VariationsHeaderHelper {
public:
// Constructor for browser-initiated requests.
//
// If the signed-in status is unknown, SignedIn::kNo can be passed as it does
// not affect transmission of experiments from the variations server.
VariationsHeaderHelper(SignedIn signed_in,
network::ResourceRequest* resource_request)
: VariationsHeaderHelper(CreateVariationsHeader(signed_in,
Owner::kUnknown,
*resource_request),
resource_request) {}
// Constructor for when the appropriate header has been determined.
VariationsHeaderHelper(std::string variations_header,
network::ResourceRequest* resource_request)
: resource_request_(resource_request) {
DCHECK(resource_request_);
variations_header_ = std::move(variations_header);
}
VariationsHeaderHelper(const VariationsHeaderHelper&) = delete;
VariationsHeaderHelper& operator=(const VariationsHeaderHelper&) = delete;
bool AppendHeaderIfNeeded(const GURL& url, InIncognito incognito) {
// Note the criteria for attaching client experiment headers:
// 1. We only transmit to Google owned domains which can evaluate
// experiments.
// 1a. These include hosts which have a standard postfix such as:
// *.doubleclick.net or *.googlesyndication.com or
// exactly www.googleadservices.com or
// international TLD domains *.google.<TLD> or *.youtube.<TLD>.
// 2. Only transmit for non-Incognito profiles.
// 3. For the X-Client-Data header, only include non-empty variation IDs.
if ((incognito == InIncognito::kYes) ||
!ShouldAppendVariationsHeader(url, "Append")) {
return false;
}
if (variations_header_.empty()) {
return false;
}
// Set the variations header to cors_exempt_headers rather than headers to
// be exempted from CORS checks, and to avoid exposing the header to service
// workers.
resource_request_->cors_exempt_headers.SetHeaderIfMissing(
kClientDataHeader, variations_header_);
return true;
}
private:
// Returns a variations header containing IDs appropriate for |signed_in|.
//
// Can be used only by code running in the browser process, which is where
// the populated VariationsIdsProvider exists.
static std::string CreateVariationsHeader(
SignedIn signed_in,
Owner owner,
const network::ResourceRequest& resource_request) {
variations::mojom::VariationsHeadersPtr variations_headers =
VariationsIdsProvider::GetInstance()->GetClientDataHeaders(
signed_in == SignedIn::kYes);
if (variations_headers.is_null()) {
return "";
}
return variations_headers->headers_map.at(
GetVisibilityKey(owner, resource_request));
}
raw_ptr<network::ResourceRequest> resource_request_;
std::string variations_header_;
};
} // namespace
bool AppendVariationsHeader(const GURL& url,
InIncognito incognito,
SignedIn signed_in,
network::ResourceRequest* request) {
// TODO(crbug.com/40135370): Consider passing the Owner if we can get it.
// However, we really only care about having the owner for requests initiated
// on the renderer side.
return VariationsHeaderHelper(signed_in, request)
.AppendHeaderIfNeeded(url, incognito);
}
bool AppendVariationsHeaderWithCustomValue(
const GURL& url,
InIncognito incognito,
variations::mojom::VariationsHeadersPtr variations_headers,
Owner owner,
network::ResourceRequest* request) {
const std::string& header =
SelectVariationsHeader(std::move(variations_headers), owner, *request);
return VariationsHeaderHelper(header, request)
.AppendHeaderIfNeeded(url, incognito);
}
bool AppendVariationsHeaderUnknownSignedIn(const GURL& url,
InIncognito incognito,
network::ResourceRequest* request) {
// TODO(crbug.com/40135370): Consider passing the Owner if we can get it.
// However, we really only care about having the owner for requests initiated
// on the renderer side.
return VariationsHeaderHelper(SignedIn::kNo, request)
.AppendHeaderIfNeeded(url, incognito);
}
void RemoveVariationsHeaderIfNeeded(
const net::RedirectInfo& redirect_info,
const network::mojom::URLResponseHead& response_head,
std::vector<std::string>* to_be_removed_headers) {
if (!ShouldAppendVariationsHeader(redirect_info.new_url, "Remove")) {
to_be_removed_headers->push_back(kClientDataHeader);
}
}
std::unique_ptr<network::SimpleURLLoader>
CreateSimpleURLLoaderWithVariationsHeader(
std::unique_ptr<network::ResourceRequest> request,
InIncognito incognito,
SignedIn signed_in,
const net::NetworkTrafficAnnotationTag& annotation_tag) {
bool variations_headers_added =
AppendVariationsHeader(request->url, incognito, signed_in, request.get());
std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
network::SimpleURLLoader::Create(std::move(request), annotation_tag);
if (variations_headers_added) {
simple_url_loader->SetOnRedirectCallback(base::BindRepeating(
[](const GURL& url_before_redirect,
const net::RedirectInfo& redirect_info,
const network::mojom::URLResponseHead& response_head,
std::vector<std::string>* to_be_removed_headers) {
RemoveVariationsHeaderIfNeeded(redirect_info, response_head,
to_be_removed_headers);
}));
}
return simple_url_loader;
}
std::unique_ptr<network::SimpleURLLoader>
CreateSimpleURLLoaderWithVariationsHeaderUnknownSignedIn(
std::unique_ptr<network::ResourceRequest> request,
InIncognito incognito,
const net::NetworkTrafficAnnotationTag& annotation_tag) {
return CreateSimpleURLLoaderWithVariationsHeader(
std::move(request), incognito, SignedIn::kNo, annotation_tag);
}
bool HasVariationsHeader(const network::ResourceRequest& request) {
std::string unused_header;
return GetVariationsHeader(request, &unused_header);
}
bool GetVariationsHeader(const network::ResourceRequest& request,
std::string* out) {
std::optional<std::string> header_value =
request.cors_exempt_headers.GetHeader(kClientDataHeader);
if (header_value) {
out->swap(header_value.value());
}
return header_value.has_value();
}
bool ShouldAppendVariationsHeaderForTesting(
const GURL& url,
const std::string& histogram_suffix) {
return ShouldAppendVariationsHeader(url, histogram_suffix);
}
void UpdateCorsExemptHeaderForVariations(
network::mojom::NetworkContextParams* params) {
params->cors_exempt_header_list.push_back(kClientDataHeader);
}
} // namespace variations