1// Copyright 2014 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/variations_http_header_provider.h"
6
7#include <vector>
8
9#include "base/base64.h"
10#include "base/memory/singleton.h"
11#include "base/metrics/histogram.h"
12#include "base/strings/string_number_conversions.h"
13#include "base/strings/string_split.h"
14#include "base/strings/string_util.h"
15#include "components/google/core/browser/google_util.h"
16#include "components/variations/proto/client_variations.pb.h"
17#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
18#include "net/http/http_request_headers.h"
19#include "url/gurl.h"
20
21namespace variations {
22
23namespace {
24
25const char* kSuffixesToSetHeadersFor[] = {
26  ".android.com",
27  ".doubleclick.com",
28  ".doubleclick.net",
29  ".ggpht.com",
30  ".googleadservices.com",
31  ".googleapis.com",
32  ".googlesyndication.com",
33  ".googleusercontent.com",
34  ".googlevideo.com",
35  ".gstatic.com",
36  ".ytimg.com",
37};
38
39}  // namespace
40
41VariationsHttpHeaderProvider* VariationsHttpHeaderProvider::GetInstance() {
42  return Singleton<VariationsHttpHeaderProvider>::get();
43}
44
45void VariationsHttpHeaderProvider::AppendHeaders(
46    const GURL& url,
47    bool incognito,
48    bool uma_enabled,
49    net::HttpRequestHeaders* headers) {
50  // Note the criteria for attaching client experiment headers:
51  // 1. We only transmit to Google owned domains which can evaluate experiments.
52  //    1a. These include hosts which have a standard postfix such as:
53  //         *.doubleclick.net or *.googlesyndication.com or
54  //         exactly www.googleadservices.com or
55  //         international TLD domains *.google.<TLD> or *.youtube.<TLD>.
56  // 2. Only transmit for non-Incognito profiles.
57  // 3. For the X-Chrome-UMA-Enabled bit, only set it if UMA is in fact enabled
58  //    for this install of Chrome.
59  // 4. For the X-Client-Data header, only include non-empty variation IDs.
60  if (incognito || !ShouldAppendHeaders(url))
61    return;
62
63  if (uma_enabled)
64    headers->SetHeaderIfMissing("X-Chrome-UMA-Enabled", "1");
65
66  // Lazily initialize the header, if not already done, before attempting to
67  // transmit it.
68  InitVariationIDsCacheIfNeeded();
69
70  std::string variation_ids_header_copy;
71  {
72    base::AutoLock scoped_lock(lock_);
73    variation_ids_header_copy = variation_ids_header_;
74  }
75
76  if (!variation_ids_header_copy.empty()) {
77    // Note that prior to M33 this header was named X-Chrome-Variations.
78    headers->SetHeaderIfMissing("X-Client-Data",
79                                variation_ids_header_copy);
80  }
81}
82
83bool VariationsHttpHeaderProvider::SetDefaultVariationIds(
84    const std::string& variation_ids) {
85  default_variation_ids_set_.clear();
86  default_trigger_id_set_.clear();
87  std::vector<std::string> entries;
88  base::SplitString(variation_ids, ',', &entries);
89  for (std::vector<std::string>::const_iterator it = entries.begin();
90       it != entries.end(); ++it) {
91    if (it->empty()) {
92      default_variation_ids_set_.clear();
93      default_trigger_id_set_.clear();
94      return false;
95    }
96    bool trigger_id = StartsWithASCII(*it, "t", true);
97    // Remove the "t" prefix if it's there.
98    std::string entry = trigger_id ? it->substr(1) : *it;
99
100    int variation_id = 0;
101    if (!base::StringToInt(entry, &variation_id)) {
102      default_variation_ids_set_.clear();
103      default_trigger_id_set_.clear();
104      return false;
105    }
106    if (trigger_id)
107      default_trigger_id_set_.insert(variation_id);
108    else
109      default_variation_ids_set_.insert(variation_id);
110  }
111  return true;
112}
113
114VariationsHttpHeaderProvider::VariationsHttpHeaderProvider()
115    : variation_ids_cache_initialized_(false) {
116}
117
118VariationsHttpHeaderProvider::~VariationsHttpHeaderProvider() {
119}
120
121void VariationsHttpHeaderProvider::OnFieldTrialGroupFinalized(
122    const std::string& trial_name,
123    const std::string& group_name) {
124  VariationID new_id =
125      GetGoogleVariationID(GOOGLE_WEB_PROPERTIES, trial_name, group_name);
126  VariationID new_trigger_id = GetGoogleVariationID(
127      GOOGLE_WEB_PROPERTIES_TRIGGER, trial_name, group_name);
128  if (new_id == EMPTY_ID && new_trigger_id == EMPTY_ID)
129    return;
130
131  base::AutoLock scoped_lock(lock_);
132  if (new_id != EMPTY_ID)
133    variation_ids_set_.insert(new_id);
134  if (new_trigger_id != EMPTY_ID)
135    variation_trigger_ids_set_.insert(new_trigger_id);
136
137  UpdateVariationIDsHeaderValue();
138}
139
140void VariationsHttpHeaderProvider::InitVariationIDsCacheIfNeeded() {
141  base::AutoLock scoped_lock(lock_);
142  if (variation_ids_cache_initialized_)
143    return;
144
145  // Register for additional cache updates. This is done first to avoid a race
146  // that could cause registered FieldTrials to be missed.
147  DCHECK(base::MessageLoop::current());
148  base::FieldTrialList::AddObserver(this);
149
150  base::TimeTicks before_time = base::TimeTicks::Now();
151
152  base::FieldTrial::ActiveGroups initial_groups;
153  base::FieldTrialList::GetActiveFieldTrialGroups(&initial_groups);
154  for (base::FieldTrial::ActiveGroups::const_iterator it =
155           initial_groups.begin();
156       it != initial_groups.end(); ++it) {
157    const VariationID id =
158        GetGoogleVariationID(GOOGLE_WEB_PROPERTIES, it->trial_name,
159                             it->group_name);
160    if (id != EMPTY_ID)
161      variation_ids_set_.insert(id);
162
163    const VariationID trigger_id =
164        GetGoogleVariationID(GOOGLE_WEB_PROPERTIES_TRIGGER, it->trial_name,
165                             it->group_name);
166    if (trigger_id != EMPTY_ID)
167      variation_trigger_ids_set_.insert(trigger_id);
168  }
169  UpdateVariationIDsHeaderValue();
170
171  UMA_HISTOGRAM_CUSTOM_COUNTS(
172      "Variations.HeaderConstructionTime",
173      (base::TimeTicks::Now() - before_time).InMicroseconds(),
174      0,
175      base::TimeDelta::FromSeconds(1).InMicroseconds(),
176      50);
177
178  variation_ids_cache_initialized_ = true;
179}
180
181void VariationsHttpHeaderProvider::UpdateVariationIDsHeaderValue() {
182  lock_.AssertAcquired();
183
184  // The header value is a serialized protobuffer of Variation IDs which is
185  // base64 encoded before transmitting as a string.
186  variation_ids_header_.clear();
187
188  if (variation_ids_set_.empty() && default_variation_ids_set_.empty() &&
189      variation_trigger_ids_set_.empty() && default_trigger_id_set_.empty()) {
190    return;
191  }
192
193  // This is the bottleneck for the creation of the header, so validate the size
194  // here. Force a hard maximum on the ID count in case the Variations server
195  // returns too many IDs and DOSs receiving servers with large requests.
196  const size_t total_id_count =
197      variation_ids_set_.size() + variation_trigger_ids_set_.size();
198  DCHECK_LE(total_id_count, 10U);
199  UMA_HISTOGRAM_COUNTS_100("Variations.Headers.ExperimentCount",
200                           total_id_count);
201  if (total_id_count > 20)
202    return;
203
204  // Merge the two sets of experiment ids.
205  std::set<VariationID> all_variation_ids_set = default_variation_ids_set_;
206  for (std::set<VariationID>::const_iterator it = variation_ids_set_.begin();
207       it != variation_ids_set_.end(); ++it) {
208    all_variation_ids_set.insert(*it);
209  }
210  ClientVariations proto;
211  for (std::set<VariationID>::const_iterator it = all_variation_ids_set.begin();
212       it != all_variation_ids_set.end(); ++it) {
213    proto.add_variation_id(*it);
214  }
215
216  std::set<VariationID> all_trigger_ids_set = default_trigger_id_set_;
217  for (std::set<VariationID>::const_iterator it =
218           variation_trigger_ids_set_.begin();
219       it != variation_trigger_ids_set_.end(); ++it) {
220    all_trigger_ids_set.insert(*it);
221  }
222  for (std::set<VariationID>::const_iterator it = all_trigger_ids_set.begin();
223       it != all_trigger_ids_set.end(); ++it) {
224    proto.add_trigger_variation_id(*it);
225  }
226
227  std::string serialized;
228  proto.SerializeToString(&serialized);
229
230  std::string hashed;
231  base::Base64Encode(serialized, &hashed);
232  // If successful, swap the header value with the new one.
233  // Note that the list of IDs and the header could be temporarily out of sync
234  // if IDs are added as the header is recreated. The receiving servers are OK
235  // with such discrepancies.
236  variation_ids_header_ = hashed;
237}
238
239// static
240bool VariationsHttpHeaderProvider::ShouldAppendHeaders(const GURL& url) {
241  if (google_util::IsGoogleDomainUrl(url, google_util::ALLOW_SUBDOMAIN,
242                                     google_util::ALLOW_NON_STANDARD_PORTS)) {
243    return true;
244  }
245
246  if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS())
247    return false;
248
249  // Some domains don't have international TLD extensions, so testing for them
250  // is very straight forward.
251  const std::string host = url.host();
252  for (size_t i = 0; i < arraysize(kSuffixesToSetHeadersFor); ++i) {
253    if (EndsWith(host, kSuffixesToSetHeadersFor[i], false))
254      return true;
255  }
256
257  return google_util::IsYoutubeDomainUrl(url, google_util::ALLOW_SUBDOMAIN,
258                                         google_util::ALLOW_NON_STANDARD_PORTS);
259}
260
261}  // namespace variations
262