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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
|
// 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
|