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