1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/variations/net/variations_http_headers.h"
6
7 #include <utility>
8
9 #include "base/bind.h"
10 #include "base/feature_list.h"
11 #include "base/macros.h"
12 #include "base/metrics/histogram_functions.h"
13 #include "base/metrics/histogram_macros.h"
14 #include "base/stl_util.h"
15 #include "base/strings/string_util.h"
16 #include "components/google/core/common/google_util.h"
17 #include "components/variations/net/omnibox_http_headers.h"
18 #include "components/variations/variations_features.h"
19 #include "components/variations/variations_ids_provider.h"
20 #include "net/base/isolation_info.h"
21 #include "net/traffic_annotation/network_traffic_annotation.h"
22 #include "net/url_request/redirect_info.h"
23 #include "services/network/public/cpp/resource_request.h"
24 #include "services/network/public/cpp/simple_url_loader.h"
25 #include "services/network/public/mojom/url_response_head.mojom.h"
26 #include "url/gurl.h"
27
28 namespace variations {
29
30 // The name string for the header for variations information.
31 // Note that prior to M33 this header was named X-Chrome-Variations.
32 const char kClientDataHeader[] = "X-Client-Data";
33
34 namespace {
35
36 // The result of checking whether a request to a URL should have variations
37 // headers appended to it.
38 //
39 // This enum is used to record UMA histogram values, and should not be
40 // reordered.
41 enum class URLValidationResult {
42 kNotValidInvalidUrl = 0,
43 // kNotValidNotHttps = 1, // Deprecated.
44 kNotValidNotGoogleDomain = 2,
45 kShouldAppend = 3,
46 kNotValidNeitherHttpHttps = 4,
47 kNotValidIsGoogleNotHttps = 5,
48 kMaxValue = kNotValidIsGoogleNotHttps,
49 };
50
51 // These values are persisted to logs. Entries should not be renumbered and
52 // numeric values should never be reused.
53 enum RequestContextCategory {
54 // First-party contexts.
55 kBrowserInitiated = 0,
56 kInternalChromePageInitiated = 1,
57 kGooglePageInitiated = 2,
58 kGoogleSubFrameOnGooglePageInitiated = 3,
59 // Third-party contexts.
60 kNonGooglePageInitiated = 4,
61 // Deprecated because the histogram Variations.Headers.DomainOwner stores
62 // more finely-grained information about this case.
63 // kNoTrustedParams = 5,
64 kNoIsolationInfo = 6,
65 kGoogleSubFrameOnNonGooglePageInitiated = 7,
66 // Deprecated because this category wasn't necessary in the first place. It's
67 // covered by kNonGooglePageInitiated.
68 // kNonGooglePageInitiatedFromFrameOrigin = 8,
69 // The next RequestContextCategory should use 9.
70 kMaxValue = kGoogleSubFrameOnNonGooglePageInitiated,
71 };
72
LogRequestContextHistogram(RequestContextCategory result)73 void LogRequestContextHistogram(RequestContextCategory result) {
74 base::UmaHistogramEnumeration("Variations.Headers.RequestContextCategory",
75 result);
76 }
77
78 // Returns a URLValidationResult for |url|. A valid URL for headers has the
79 // following qualities: (i) it is well-formed, (ii) its scheme is HTTPS, and
80 // (iii) it has a Google-associated domain.
GetUrlValidationResult(const GURL & url)81 URLValidationResult GetUrlValidationResult(const GURL& url) {
82 if (!url.is_valid())
83 return URLValidationResult::kNotValidInvalidUrl;
84
85 if (!url.SchemeIsHTTPOrHTTPS())
86 return URLValidationResult::kNotValidNeitherHttpHttps;
87
88 if (!google_util::IsGoogleAssociatedDomainUrl(url))
89 return URLValidationResult::kNotValidNotGoogleDomain;
90
91 // HTTPS is checked here, rather than before the IsGoogleAssociatedDomainUrl()
92 // check, to know how many Google domains are rejected by the change to append
93 // headers to only HTTPS requests.
94 if (!url.SchemeIs(url::kHttpsScheme))
95 return URLValidationResult::kNotValidIsGoogleNotHttps;
96
97 return URLValidationResult::kShouldAppend;
98 }
99
100 // Returns true if the request to |url| should include a variations header.
101 // Also, logs the result of validating |url| in histograms, one of which ends in
102 // |suffix|.
ShouldAppendVariationsHeader(const GURL & url,const std::string & suffix)103 bool ShouldAppendVariationsHeader(const GURL& url, const std::string& suffix) {
104 URLValidationResult result = GetUrlValidationResult(url);
105 base::UmaHistogramEnumeration(
106 "Variations.Headers.URLValidationResult." + suffix, result);
107 return result == URLValidationResult::kShouldAppend;
108 }
109
110 // Returns true if the request is sent from a Google web property, i.e. from a
111 // first-party context.
112 //
113 // The context is determined using |owner| and |resource_request|. |owner| is
114 // used for subframe-initiated subresource requests from the renderer. Note that
115 // for these kinds of requests, ResourceRequest::TrustedParams is not populated.
IsFirstPartyContext(Owner owner,const network::ResourceRequest & resource_request)116 bool IsFirstPartyContext(Owner owner,
117 const network::ResourceRequest& resource_request) {
118 if (!resource_request.request_initiator) {
119 // The absence of |request_initiator| means that the request was initiated
120 // by the browser, e.g. a request from the browser to Autofill upon form
121 // detection.
122 LogRequestContextHistogram(RequestContextCategory::kBrowserInitiated);
123 return true;
124 }
125
126 const GURL request_initiator_url =
127 resource_request.request_initiator->GetURL();
128 if (request_initiator_url.SchemeIs("chrome-search") ||
129 request_initiator_url.SchemeIs("chrome")) {
130 // A scheme matching the above patterns means that the request was
131 // initiated by an internal page, e.g. a request from
132 // chrome-search://local-ntp/ for App Launcher resources.
133 LogRequestContextHistogram(kInternalChromePageInitiated);
134 return true;
135 }
136 if (GetUrlValidationResult(request_initiator_url) !=
137 URLValidationResult::kShouldAppend) {
138 // The request was initiated by a non-Google-associated page, e.g. a request
139 // from https://www.bbc.com/.
140 LogRequestContextHistogram(kNonGooglePageInitiated);
141 return false;
142 }
143 if (resource_request.is_main_frame) {
144 // The request is from a Google-associated page--not a subframe--e.g. a
145 // request from https://calendar.google.com/.
146 LogRequestContextHistogram(kGooglePageInitiated);
147 return true;
148 }
149 // |is_main_frame| is false, so the request was initiated by a subframe, and
150 // we need to determine whether the top-level page in which the frame is
151 // embedded is a Google-owned web property.
152 //
153 // If TrustedParams is populated, then we can use it to determine the request
154 // context. If not, e.g. for subresource requests, we use |owner|.
155 if (resource_request.trusted_params) {
156 const net::IsolationInfo* isolation_info =
157 &resource_request.trusted_params->isolation_info;
158
159 if (isolation_info->IsEmpty()) {
160 // TODO(crbug/1094303): If TrustedParams are present, it appears that
161 // IsolationInfo is too. Maybe deprecate kNoIsolationInfo if this bucket
162 // is never used.
163 LogRequestContextHistogram(kNoIsolationInfo);
164 // Without IsolationInfo, we cannot be certain that the request is from a
165 // first-party context.
166 return false;
167 }
168 if (GetUrlValidationResult(isolation_info->top_frame_origin()->GetURL()) ==
169 URLValidationResult::kShouldAppend) {
170 // The request is from a Google-associated subframe on a Google-associated
171 // page, e.g. a request from a Docs subframe on https://drive.google.com/.
172 LogRequestContextHistogram(kGoogleSubFrameOnGooglePageInitiated);
173 return true;
174 }
175 // The request is from a Google-associated subframe on a non-Google-
176 // associated page, e.g. a request to DoubleClick from an ad's subframe on
177 // https://www.lexico.com/.
178 LogRequestContextHistogram(kGoogleSubFrameOnNonGooglePageInitiated);
179 return false;
180 }
181 base::UmaHistogramEnumeration("Variations.Headers.DomainOwner", owner);
182
183 if (owner == Owner::kGoogle) {
184 LogRequestContextHistogram(kGoogleSubFrameOnGooglePageInitiated);
185 return true;
186 }
187 LogRequestContextHistogram(kGoogleSubFrameOnNonGooglePageInitiated);
188 return false;
189 }
190
191 // Returns GoogleWebVisibility::FIRST_PARTY if kRestrictGoogleWebVisibility is
192 // enabled and the request is from a first-party context; otherwise, returns
193 // GoogleWebVisibility::ANY.
GetVisibilityKey(Owner owner,const network::ResourceRequest & resource_request)194 variations::mojom::GoogleWebVisibility GetVisibilityKey(
195 Owner owner,
196 const network::ResourceRequest& resource_request) {
197 bool use_first_party_visibility =
198 IsFirstPartyContext(owner, resource_request) &&
199 base::FeatureList::IsEnabled(internal::kRestrictGoogleWebVisibility);
200
201 return use_first_party_visibility
202 ? variations::mojom::GoogleWebVisibility::FIRST_PARTY
203 : variations::mojom::GoogleWebVisibility::ANY;
204 }
205
206 // Returns a variations header from |variations_headers|. When
207 // kRestrictGoogleWebVisibility is enabled, the request context is considered
208 // and may be used to select a header with a more limited set of IDs.
SelectVariationsHeader(variations::mojom::VariationsHeadersPtr variations_headers,Owner owner,const network::ResourceRequest & resource_request)209 std::string SelectVariationsHeader(
210 variations::mojom::VariationsHeadersPtr variations_headers,
211 Owner owner,
212 const network::ResourceRequest& resource_request) {
213 return variations_headers->headers_map.at(
214 GetVisibilityKey(owner, resource_request));
215 }
216
217 class VariationsHeaderHelper {
218 public:
219 // Constructor for browser-initiated requests.
220 //
221 // If the signed-in status is unknown, SignedIn::kNo can be passed as it does
222 // not affect transmission of experiments from the variations server.
VariationsHeaderHelper(SignedIn signed_in,network::ResourceRequest * resource_request)223 VariationsHeaderHelper(SignedIn signed_in,
224 network::ResourceRequest* resource_request)
225 : VariationsHeaderHelper(CreateVariationsHeader(signed_in,
226 Owner::kUnknown,
227 *resource_request),
228 resource_request) {}
229
230 // Constructor for when the appropriate header has been determined.
VariationsHeaderHelper(std::string variations_header,network::ResourceRequest * resource_request)231 VariationsHeaderHelper(std::string variations_header,
232 network::ResourceRequest* resource_request)
233 : resource_request_(resource_request) {
234 DCHECK(resource_request_);
235 variations_header_ = std::move(variations_header);
236 }
237
AppendHeaderIfNeeded(const GURL & url,InIncognito incognito)238 bool AppendHeaderIfNeeded(const GURL& url, InIncognito incognito) {
239 AppendOmniboxOnDeviceSuggestionsHeaderIfNeeded(url, resource_request_);
240
241 // Note the criteria for attaching client experiment headers:
242 // 1. We only transmit to Google owned domains which can evaluate
243 // experiments.
244 // 1a. These include hosts which have a standard postfix such as:
245 // *.doubleclick.net or *.googlesyndication.com or
246 // exactly www.googleadservices.com or
247 // international TLD domains *.google.<TLD> or *.youtube.<TLD>.
248 // 2. Only transmit for non-Incognito profiles.
249 // 3. For the X-Client-Data header, only include non-empty variation IDs.
250 if ((incognito == InIncognito::kYes) ||
251 !ShouldAppendVariationsHeader(url, "Append"))
252 return false;
253
254 if (variations_header_.empty())
255 return false;
256
257 // Set the variations header to cors_exempt_headers rather than headers
258 // to be exempted from CORS checks.
259 resource_request_->cors_exempt_headers.SetHeaderIfMissing(
260 kClientDataHeader, variations_header_);
261 return true;
262 }
263
264 private:
265 // Returns a variations header containing IDs appropriate for |signed_in|.
266 // When kRestrictGoogleWebVisibility is enabled, the request context is
267 // considered and may be used to select a header with a more limited set of
268 // IDs.
269 //
270 // Can be used only by code running in the browser process, which is where
271 // the populated VariationsIdsProvider exists.
CreateVariationsHeader(SignedIn signed_in,Owner owner,const network::ResourceRequest & resource_request)272 static std::string CreateVariationsHeader(
273 SignedIn signed_in,
274 Owner owner,
275 const network::ResourceRequest& resource_request) {
276 variations::mojom::VariationsHeadersPtr variations_headers =
277 VariationsIdsProvider::GetInstance()->GetClientDataHeaders(
278 signed_in == SignedIn::kYes);
279
280 if (variations_headers.is_null())
281 return "";
282 return variations_headers->headers_map.at(
283 GetVisibilityKey(owner, resource_request));
284 }
285
286 network::ResourceRequest* resource_request_;
287 std::string variations_header_;
288
289 DISALLOW_COPY_AND_ASSIGN(VariationsHeaderHelper);
290 };
291
292 } // namespace
293
AppendVariationsHeader(const GURL & url,InIncognito incognito,SignedIn signed_in,network::ResourceRequest * request)294 bool AppendVariationsHeader(const GURL& url,
295 InIncognito incognito,
296 SignedIn signed_in,
297 network::ResourceRequest* request) {
298 // TODO(crbug.com/1094303): Consider passing the Owner if we can get it.
299 // However, we really only care about having the owner for requests initiated
300 // on the renderer side.
301 return VariationsHeaderHelper(signed_in, request)
302 .AppendHeaderIfNeeded(url, incognito);
303 }
304
AppendVariationsHeaderWithCustomValue(const GURL & url,InIncognito incognito,variations::mojom::VariationsHeadersPtr variations_headers,Owner owner,network::ResourceRequest * request)305 bool AppendVariationsHeaderWithCustomValue(
306 const GURL& url,
307 InIncognito incognito,
308 variations::mojom::VariationsHeadersPtr variations_headers,
309 Owner owner,
310 network::ResourceRequest* request) {
311 const std::string& header =
312 SelectVariationsHeader(std::move(variations_headers), owner, *request);
313 return VariationsHeaderHelper(header, request)
314 .AppendHeaderIfNeeded(url, incognito);
315 }
316
AppendVariationsHeaderUnknownSignedIn(const GURL & url,InIncognito incognito,network::ResourceRequest * request)317 bool AppendVariationsHeaderUnknownSignedIn(const GURL& url,
318 InIncognito incognito,
319 network::ResourceRequest* request) {
320 // TODO(crbug.com/1094303): Consider passing the Owner if we can get it.
321 // However, we really only care about having the owner for requests initiated
322 // on the renderer side.
323 return VariationsHeaderHelper(SignedIn::kNo, request)
324 .AppendHeaderIfNeeded(url, incognito);
325 }
326
RemoveVariationsHeaderIfNeeded(const net::RedirectInfo & redirect_info,const network::mojom::URLResponseHead & response_head,std::vector<std::string> * to_be_removed_headers)327 void RemoveVariationsHeaderIfNeeded(
328 const net::RedirectInfo& redirect_info,
329 const network::mojom::URLResponseHead& response_head,
330 std::vector<std::string>* to_be_removed_headers) {
331 if (!ShouldAppendVariationsHeader(redirect_info.new_url, "Remove"))
332 to_be_removed_headers->push_back(kClientDataHeader);
333 }
334
335 std::unique_ptr<network::SimpleURLLoader>
CreateSimpleURLLoaderWithVariationsHeader(std::unique_ptr<network::ResourceRequest> request,InIncognito incognito,SignedIn signed_in,const net::NetworkTrafficAnnotationTag & annotation_tag)336 CreateSimpleURLLoaderWithVariationsHeader(
337 std::unique_ptr<network::ResourceRequest> request,
338 InIncognito incognito,
339 SignedIn signed_in,
340 const net::NetworkTrafficAnnotationTag& annotation_tag) {
341 bool variations_headers_added =
342 AppendVariationsHeader(request->url, incognito, signed_in, request.get());
343 std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
344 network::SimpleURLLoader::Create(std::move(request), annotation_tag);
345 if (variations_headers_added) {
346 simple_url_loader->SetOnRedirectCallback(
347 base::BindRepeating(&RemoveVariationsHeaderIfNeeded));
348 }
349 return simple_url_loader;
350 }
351
352 std::unique_ptr<network::SimpleURLLoader>
CreateSimpleURLLoaderWithVariationsHeaderUnknownSignedIn(std::unique_ptr<network::ResourceRequest> request,InIncognito incognito,const net::NetworkTrafficAnnotationTag & annotation_tag)353 CreateSimpleURLLoaderWithVariationsHeaderUnknownSignedIn(
354 std::unique_ptr<network::ResourceRequest> request,
355 InIncognito incognito,
356 const net::NetworkTrafficAnnotationTag& annotation_tag) {
357 return CreateSimpleURLLoaderWithVariationsHeader(
358 std::move(request), incognito, SignedIn::kNo, annotation_tag);
359 }
360
IsVariationsHeader(const std::string & header_name)361 bool IsVariationsHeader(const std::string& header_name) {
362 return header_name == kClientDataHeader ||
363 header_name == kOmniboxOnDeviceSuggestionsHeader;
364 }
365
HasVariationsHeader(const network::ResourceRequest & request)366 bool HasVariationsHeader(const network::ResourceRequest& request) {
367 // Note: kOmniboxOnDeviceSuggestionsHeader is not listed because this function
368 // is only used for testing.
369 return request.cors_exempt_headers.HasHeader(kClientDataHeader);
370 }
371
ShouldAppendVariationsHeaderForTesting(const GURL & url,const std::string & histogram_suffix)372 bool ShouldAppendVariationsHeaderForTesting(
373 const GURL& url,
374 const std::string& histogram_suffix) {
375 return ShouldAppendVariationsHeader(url, histogram_suffix);
376 }
377
UpdateCorsExemptHeaderForVariations(network::mojom::NetworkContextParams * params)378 void UpdateCorsExemptHeaderForVariations(
379 network::mojom::NetworkContextParams* params) {
380 params->cors_exempt_header_list.push_back(kClientDataHeader);
381
382 if (base::FeatureList::IsEnabled(kReportOmniboxOnDeviceSuggestionsHeader)) {
383 params->cors_exempt_header_list.push_back(
384 kOmniboxOnDeviceSuggestionsHeader);
385 }
386 }
387
388 } // namespace variations
389