blob: de22877a5b2188610424daee0a9d0c9238a18204 [file] [log] [blame]
// Copyright 2012 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/service/variations_service.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/command_line.h"
#include "base/debug/crash_logging.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/observer_list.h"
#include "base/strings/string_util.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "base/version.h"
#include "base/version_info/version_info.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "components/encrypted_messages/encrypted_message.pb.h"
#include "components/encrypted_messages/message_encrypter.h"
#include "components/metrics/metrics_state_manager.h"
#include "components/network_time/network_time_tracker.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/variations/field_trial_internals_utils.h"
#include "components/variations/pref_names.h"
#include "components/variations/proto/variations_seed.pb.h"
#include "components/variations/seed_response.h"
#include "components/variations/sticky_activation_manager.h"
#include "components/variations/variations_safe_seed_store_local_state.h"
#include "components/variations/variations_seed_simulator.h"
#include "components/variations/variations_switches.h"
#include "components/variations/variations_url_constants.h"
#include "components/version_info/channel.h"
#include "components/version_info/version_info.h"
#include "net/base/net_errors.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
namespace variations {
namespace {
// Constants used for encrypting the if-none-match header if we are retrieving a
// seed over http.
const char kEncryptedMessageLabel[] = "chrome variations";
// TODO(crbug.com/41359527): Change this key to a unique VariationsService one,
// once the matching private key is changed server side.
// Key is used to encrypt headers in seed retrieval requests that happen over
// HTTP connections (when retrying after an unsuccessful HTTPS retrieval
// attempt).
const uint8_t kServerPublicKey[] = {
0x51, 0xcc, 0x52, 0x67, 0x42, 0x47, 0x3b, 0x10, 0xe8, 0x63, 0x18,
0x3c, 0x61, 0xa7, 0x96, 0x76, 0x86, 0x91, 0x40, 0x71, 0x39, 0x5f,
0x31, 0x1a, 0x39, 0x5b, 0x76, 0xb1, 0x6b, 0x3d, 0x6a, 0x2b};
const uint32_t kServerPublicKeyVersion = 1;
// For the HTTP date headers, the resolution of the server time is 1 second.
const uint32_t kServerTimeResolutionInSeconds = 1;
// Whether the VariationsService should fetch the seed for testing.
bool g_should_fetch_for_testing = false;
// Returns a string that will be used for the value of the 'osname' URL param
// to the variations server.
std::string GetPlatformString() {
#if BUILDFLAG(IS_WIN)
return "win";
#elif BUILDFLAG(IS_IOS)
return "ios";
#elif BUILDFLAG(IS_MAC)
return "mac";
#elif BUILDFLAG(IS_CHROMEOS)
return "chromeos";
#elif BUILDFLAG(IS_ANDROID)
return "android";
#elif BUILDFLAG(IS_FUCHSIA)
return "fuchsia";
#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_BSD) || BUILDFLAG(IS_SOLARIS)
// Default BSD and SOLARIS to Linux to not break those builds, although these
// platforms are not officially supported by Chrome.
return "linux";
#else
#error Unknown platform
#endif
}
// Gets the restrict parameter from either the passed override, the client or
// |policy_pref_service|.
std::string GetRestrictParameterValue(const std::string& restrict_mode_override,
VariationsServiceClient* client,
PrefService* policy_pref_service) {
if (!restrict_mode_override.empty()) {
return restrict_mode_override;
}
std::string parameter;
if (client->OverridesRestrictParameter(&parameter) || !policy_pref_service) {
return parameter;
}
return policy_pref_service->GetString(prefs::kVariationsRestrictParameter);
}
// Reported to UMA, keep in sync with enums.xml and don't renumber entries.
enum ResourceRequestsAllowedState {
RESOURCE_REQUESTS_ALLOWED,
RESOURCE_REQUESTS_NOT_ALLOWED,
RESOURCE_REQUESTS_ALLOWED_NOTIFIED,
RESOURCE_REQUESTS_NOT_ALLOWED_EULA_NOT_ACCEPTED,
RESOURCE_REQUESTS_NOT_ALLOWED_NETWORK_DOWN,
RESOURCE_REQUESTS_NOT_ALLOWED_COMMAND_LINE_DISABLED,
RESOURCE_REQUESTS_NOT_ALLOWED_NETWORK_STATE_NOT_INITIALIZED,
RESOURCE_REQUESTS_ALLOWED_ENUM_SIZE,
};
// Records UMA histogram with the current resource requests allowed state.
void RecordRequestsAllowedHistogram(ResourceRequestsAllowedState state) {
UMA_HISTOGRAM_ENUMERATION("Variations.ResourceRequestsAllowed", state,
RESOURCE_REQUESTS_ALLOWED_ENUM_SIZE);
}
// Converts ResourceRequestAllowedNotifier::State to the corresponding
// ResourceRequestsAllowedState value.
ResourceRequestsAllowedState ResourceRequestStateToHistogramValue(
web_resource::ResourceRequestAllowedNotifier::State state) {
using web_resource::ResourceRequestAllowedNotifier;
switch (state) {
case ResourceRequestAllowedNotifier::DISALLOWED_EULA_NOT_ACCEPTED:
return RESOURCE_REQUESTS_NOT_ALLOWED_EULA_NOT_ACCEPTED;
case ResourceRequestAllowedNotifier::DISALLOWED_NETWORK_DOWN:
return RESOURCE_REQUESTS_NOT_ALLOWED_NETWORK_DOWN;
case ResourceRequestAllowedNotifier::DISALLOWED_COMMAND_LINE_DISABLED:
return RESOURCE_REQUESTS_NOT_ALLOWED_COMMAND_LINE_DISABLED;
case ResourceRequestAllowedNotifier::
DISALLOWED_NETWORK_STATE_NOT_INITIALIZED:
return RESOURCE_REQUESTS_NOT_ALLOWED_NETWORK_STATE_NOT_INITIALIZED;
case ResourceRequestAllowedNotifier::ALLOWED:
return RESOURCE_REQUESTS_ALLOWED;
}
NOTREACHED();
}
// Returns the header value for |name| from |headers| or an empty string_view if
// not set.
std::string_view GetHeaderValue(const net::HttpResponseHeaders* headers,
std::string_view name) {
return headers->EnumerateHeader(nullptr, name).value_or(std::string_view());
}
// Returns the list of values for |name| from |headers|. If the header in not
// set, return an empty list.
std::vector<std::string> GetHeaderValuesList(
const net::HttpResponseHeaders* headers,
std::string_view name) {
std::vector<std::string> values;
size_t iter = 0;
while (std::optional<std::string_view> value =
headers->EnumerateHeader(&iter, name)) {
values.emplace_back(*value);
}
return values;
}
// Looks for delta and gzip compression instance manipulation flags set by the
// server in |headers|. Checks the order of flags and presence of unknown
// instance manipulations. If successful, |is_delta_compressed| and
// |is_gzip_compressed| contain compression flags and true is returned.
bool GetInstanceManipulations(const net::HttpResponseHeaders* headers,
bool* is_delta_compressed,
bool* is_gzip_compressed) {
std::vector<std::string> ims = GetHeaderValuesList(headers, "IM");
const auto delta_im = std::ranges::find(ims, "x-bm");
const auto gzip_im = std::ranges::find(ims, "gzip");
*is_delta_compressed = delta_im != ims.end();
*is_gzip_compressed = gzip_im != ims.end();
// The IM field should not have anything but x-bm and gzip.
size_t im_count =
(*is_delta_compressed ? 1 : 0) + (*is_gzip_compressed ? 1 : 0);
if (im_count != ims.size()) {
DVLOG(1) << "Unrecognized instance manipulations in "
<< base::JoinString(ims, ",")
<< "; only x-bm and gzip are supported";
return false;
}
// The IM field defines order in which instance manipulations were applied.
// The client requests and supports gzip-compressed delta-compressed seeds,
// but not vice versa.
if (*is_delta_compressed && *is_gzip_compressed && delta_im > gzip_im) {
DVLOG(1) << "Unsupported instance manipulations order: "
<< "requested x-bm,gzip but received gzip,x-bm";
return false;
}
return true;
}
// Variations seed fetching is only enabled in official Chrome builds, if a URL
// is specified on the command line, and for testing.
bool IsFetchingEnabled() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisableVariationsSeedFetch)) {
return false;
}
#else
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kVariationsServerURL) &&
!g_should_fetch_for_testing) {
DVLOG(1)
<< "Not performing repeated fetching in unofficial build without --"
<< switches::kVariationsServerURL << " specified.";
return false;
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
return true;
}
// Returns the already downloaded first run seed, and clear the seed from the
// native-side prefs. At this point, the seed has already been fetched from the
// native seed storage, so it's no longer needed there. This is done regardless
// if we fail or succeed below - since if we succeed, we're good to go and if we
// fail, we probably don't want to keep around the bad content anyway.
std::unique_ptr<SeedResponse> MaybeImportFirstRunSeed(
VariationsServiceClient* client,
PrefService* local_state) {
if (!local_state->HasPrefPath(prefs::kVariationsSeedSignature)) {
DVLOG(1) << "Importing first run seed from native preferences.";
return client->TakeSeedFromNativeVariationsSeedStore();
}
return nullptr;
}
} // namespace
#if BUILDFLAG(IS_CHROMEOS)
// This is a utility which syncs the policy-managed value of
// |prefs::kDeviceVariationsRestrictionsByPolicy| into
// |prefs::kVariationsRestrictionsByPolicy|.
// TODO(crbug.com/40121933): Remove this workaround and implement a better long
// term solution.
class DeviceVariationsRestrictionByPolicyApplicator {
public:
DeviceVariationsRestrictionByPolicyApplicator(
PrefService* policy_pref_service)
: policy_pref_service_(policy_pref_service) {
DCHECK(policy_pref_service_);
const PrefService::PrefInitializationStatus prefs_init_status =
policy_pref_service_->GetAllPrefStoresInitializationStatus();
if (prefs_init_status == PrefService::INITIALIZATION_STATUS_WAITING) {
policy_pref_service_->AddPrefInitObserver(
base::BindOnce(&DeviceVariationsRestrictionByPolicyApplicator::
OnPolicyPrefServiceInitialized,
weak_ptr_factory_.GetWeakPtr()));
return;
}
OnPolicyPrefServiceInitialized(prefs_init_status ==
PrefService::INITIALIZATION_STATUS_SUCCESS);
}
~DeviceVariationsRestrictionByPolicyApplicator() = default;
DeviceVariationsRestrictionByPolicyApplicator(
const DeviceVariationsRestrictionByPolicyApplicator& other) = delete;
DeviceVariationsRestrictionByPolicyApplicator& operator=(
const DeviceVariationsRestrictionByPolicyApplicator& other) = delete;
private:
void OnPolicyPrefServiceInitialized(bool successful) {
// If PrefService initialization was not successful, another component will
// display an error message to the user.
if (!successful) {
return;
}
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(policy_pref_service_);
pref_change_registrar_->Add(
prefs::kDeviceVariationsRestrictionsByPolicy,
base::BindRepeating(&DeviceVariationsRestrictionByPolicyApplicator::
OnDevicePolicyChange,
weak_ptr_factory_.GetWeakPtr()));
// Also process the initial value.
OnDevicePolicyChange();
}
// Observes the changes in prefs::kDeviceVariationsRestrictionsByPolicy,
// and saves and retrieve its local state value, then sets
// prefs::kVariationsRestrictParameter with that new value. That's to
// reflect the changes of chromeos policy into the user policy.
// TODO(crbug.com/40121933): Remove that workaround, and make a better long
// term solution.
void OnDevicePolicyChange() {
const std::string& device_policy =
prefs::kDeviceVariationsRestrictionsByPolicy;
const std::string& user_policy = prefs::kVariationsRestrictionsByPolicy;
if (policy_pref_service_->IsManagedPreference(device_policy)) {
const int device_value = policy_pref_service_->GetInteger(device_policy);
policy_pref_service_->SetInteger(user_policy, device_value);
} else {
policy_pref_service_->ClearPref(user_policy);
}
}
const raw_ptr<PrefService> policy_pref_service_;
// Watch the changes of the variations prefs.
std::unique_ptr<PrefChangeRegistrar> pref_change_registrar_;
base::WeakPtrFactory<DeviceVariationsRestrictionByPolicyApplicator>
weak_ptr_factory_{this};
};
#endif // BUILDFLAG(IS_CHROMEOS)
VariationsService::VariationsService(
std::unique_ptr<VariationsServiceClient> client,
std::unique_ptr<web_resource::ResourceRequestAllowedNotifier> notifier,
PrefService* local_state,
metrics::MetricsStateManager* state_manager,
const UIStringOverrider& ui_string_overrider)
: client_(std::move(client)),
local_state_(local_state),
state_manager_(state_manager),
policy_pref_service_(local_state),
resource_request_allowed_notifier_(std::move(notifier)),
safe_seed_manager_(local_state),
// TODO(crbug.com/421912603): Verify whether all callers should pass
// `true` here.
entropy_providers_(state_manager_->CreateEntropyProviders(
/*enable_limited_entropy_mode=*/true)),
field_trial_creator_(
client_.get(),
std::make_unique<VariationsSeedStore>(
local_state,
MaybeImportFirstRunSeed(client_.get(), local_state),
/*signature_verification_enabled=*/true,
std::make_unique<VariationsSafeSeedStoreLocalState>(
local_state,
client_.get()->GetVariationsSeedFileDir(),
client_.get()->GetChannelForVariations(),
entropy_providers_.get()),
client_.get()->GetChannelForVariations(),
client_.get()->GetVariationsSeedFileDir(),
entropy_providers_.get()),
ui_string_overrider) {
DCHECK(client_);
DCHECK(resource_request_allowed_notifier_);
#if BUILDFLAG(IS_CHROMEOS)
device_variations_restrictions_by_policy_applicator_ =
std::make_unique<DeviceVariationsRestrictionByPolicyApplicator>(
policy_pref_service_);
#endif
}
VariationsService::~VariationsService() = default;
void VariationsService::PerformPreMainMessageLoopStartup() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(field_trial_creator_.IsOverrideResourceMapEmpty());
InitResourceRequestedAllowedNotifier();
// Android instead calls OnAppEnterForeground() which then calls
// StartRepeatedVariationsSeedFetch(). This is too early to do it on Android
// because at this point the |restrict_mode_| hasn't been set yet. See also
// the CHECK in SetRestrictMode().
#if !BUILDFLAG(IS_ANDROID)
if (!IsFetchingEnabled()) {
return;
}
StartRepeatedVariationsSeedFetch();
#endif // !BUILDFLAG(IS_ANDROID)
}
bool VariationsService::EncryptString(const std::string& plaintext,
std::string* encrypted) {
encrypted_messages::EncryptedMessage encrypted_message;
if (!encrypted_messages::EncryptSerializedMessage(
kServerPublicKey, kServerPublicKeyVersion, kEncryptedMessageLabel,
plaintext, &encrypted_message) ||
!encrypted_message.SerializeToString(encrypted)) {
return false;
}
return true;
}
void VariationsService::AddObserver(Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observer_list_.AddObserver(observer);
}
void VariationsService::RemoveObserver(Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observer_list_.RemoveObserver(observer);
}
void VariationsService::OnAppEnterForeground() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!IsFetchingEnabled()) {
return;
}
// On mobile platforms, initialize the fetch scheduler when we receive the
// first app foreground notification.
if (!request_scheduler_) {
StartRepeatedVariationsSeedFetch();
}
request_scheduler_->OnAppEnterForeground();
}
void VariationsService::SetRestrictMode(const std::string& restrict_mode) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This should be called before the server URL has been computed. Note: This
// uses a CHECK because this is relevant for the behavior in release official
// builds that talk to the variations server - which don't enable DCHECKs.
CHECK(variations_server_url_.is_empty());
restrict_mode_ = restrict_mode;
}
bool VariationsService::IsLikelyDogfoodClient() const {
// The param is typically only set for dogfood clients, though in principle it
// could be set in other rare contexts as well.
const std::string restrict_mode = GetRestrictParameterValue(
restrict_mode_, client_.get(), policy_pref_service_);
return !restrict_mode.empty();
}
void VariationsService::SetIsLikelyDogfoodClientForTesting(
bool is_dogfood_client) {
// Any non-empty value for the `restrict_mode_` is treated as a dogfood client
// (see above).
if (is_dogfood_client) {
restrict_mode_ = "nonempty";
} else {
restrict_mode_ = std::string();
}
}
GURL VariationsService::GetVariationsServerURL(HttpOptions http_options) {
const bool secure = http_options == USE_HTTPS;
const std::string restrict_mode = GetRestrictParameterValue(
restrict_mode_, client_.get(), policy_pref_service_);
// If there's a restrict mode, we don't want to fall back to HTTP to avoid
// toggling restrict mode state.
if (!secure && !restrict_mode.empty()) {
return GURL();
}
std::string server_url_string(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
secure ? switches::kVariationsServerURL
: switches::kVariationsInsecureServerURL));
if (server_url_string.empty()) {
server_url_string = secure ? kDefaultServerUrl : kDefaultInsecureServerUrl;
}
GURL server_url = GURL(server_url_string);
if (!restrict_mode.empty()) {
DCHECK(secure);
server_url = net::AppendOrReplaceQueryParameter(server_url, "restrict",
restrict_mode);
}
server_url = net::AppendOrReplaceQueryParameter(
server_url, "osname",
osname_server_param_override_.empty() ? GetPlatformString()
: osname_server_param_override_);
// Add channel to the request URL.
version_info::Channel channel = client_->GetChannelForVariations();
if (channel != version_info::Channel::UNKNOWN) {
server_url = net::AppendOrReplaceQueryParameter(
server_url, "channel", version_info::GetChannelString(channel));
}
// Add milestone to the request URL.
const std::string milestone = version_info::GetMajorVersionNumber();
if (!milestone.empty()) {
server_url =
net::AppendOrReplaceQueryParameter(server_url, "milestone", milestone);
}
DCHECK(server_url.is_valid());
return server_url;
}
void VariationsService::EnsureLocaleEquals(const std::string& locale) {
#if BUILDFLAG(IS_CHROMEOS)
// Chrome OS may switch language on the fly.
return;
#else
#if BUILDFLAG(IS_ANDROID)
// TODO(asvitkine): Speculative early return to silence CHECK failures on
// Android, see crbug.com/912320.
if (locale.empty()) {
return;
}
#endif
// Uses a CHECK rather than a DCHECK to ensure that issues are caught since
// problems in this area may only appear in the wild due to official builds
// and end user machines.
if (locale != field_trial_creator_.application_locale()) {
// TODO(crbug.com/41430274): Report the two values in crash keys.
static auto* lhs_key = base::debug::AllocateCrashKeyString(
"mismatched_locale_lhs", base::debug::CrashKeySize::Size256);
static auto* rhs_key = base::debug::AllocateCrashKeyString(
"mismatched_locale_rhs", base::debug::CrashKeySize::Size256);
base::debug::ScopedCrashKeyString scoped_lhs(lhs_key, locale);
base::debug::ScopedCrashKeyString scoped_rhs(
rhs_key, field_trial_creator_.application_locale());
CHECK_EQ(locale, field_trial_creator_.application_locale());
}
#endif
}
// static
std::string VariationsService::GetDefaultVariationsServerURLForTesting() {
return kDefaultServerUrl;
}
// static
void VariationsService::RegisterPrefs(PrefRegistrySimple* registry) {
SafeSeedManager::RegisterPrefs(registry);
VariationsSeedStore::RegisterPrefs(registry);
RegisterFieldTrialInternalsPrefs(*registry);
StickyActivationManager::RegisterPrefs(*registry);
registry->RegisterIntegerPref(
prefs::kDeviceVariationsRestrictionsByPolicy,
static_cast<int>(RestrictionPolicy::NO_RESTRICTIONS));
registry->RegisterDictionaryPref(
prefs::kVariationsGoogleGroups,
static_cast<int>(RestrictionPolicy::NO_RESTRICTIONS));
// This preference is used to override the variations country code which is
// consistent across different chrome version.
registry->RegisterStringPref(prefs::kVariationsPermanentOverriddenCountry,
std::string());
// This preference keeps track of ChromeVariations enum policy which
// allows the admin to restrict the set of variations applied.
registry->RegisterIntegerPref(
prefs::kVariationsRestrictionsByPolicy,
static_cast<int>(RestrictionPolicy::NO_RESTRICTIONS));
// This preference will only be written by the policy service, which will fill
// it according to a value stored in the User Policy.
registry->RegisterStringPref(prefs::kVariationsRestrictParameter,
std::string());
}
// static
void VariationsService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
// This preference will only be written by the policy service, which will fill
// it according to a value stored in the User Policy.
registry->RegisterStringPref(prefs::kVariationsRestrictParameter,
std::string());
}
// static
std::unique_ptr<VariationsService> VariationsService::Create(
std::unique_ptr<VariationsServiceClient> client,
PrefService* local_state,
metrics::MetricsStateManager* state_manager,
const char* disable_network_switch,
const UIStringOverrider& ui_string_overrider,
web_resource::ResourceRequestAllowedNotifier::NetworkConnectionTrackerGetter
network_connection_tracker_getter) {
return base::WrapUnique(new VariationsService(
std::move(client),
std::make_unique<web_resource::ResourceRequestAllowedNotifier>(
local_state, disable_network_switch,
std::move(network_connection_tracker_getter)),
local_state, state_manager, ui_string_overrider));
}
// static
void VariationsService::EnableFetchForTesting() {
g_should_fetch_for_testing = true;
}
void VariationsService::DoActualFetch() {
DoFetchFromURL(variations_server_url_, false);
}
const std::string& VariationsService::GetLatestSerialNumber() {
return field_trial_creator_.seed_store()->GetLatestSerialNumber();
}
bool VariationsService::DoFetchFromURL(const GURL& url, bool is_http_retry) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(IsFetchingEnabled());
safe_seed_manager_.RecordFetchStarted();
// Normally, there shouldn't be a |pending_seed_request_| when this fires.
// However it's not impossible - for example if Chrome was paused (e.g. in a
// debugger or if the machine was suspended) and OnURLFetchComplete() hasn't
// had a chance to run yet from the previous request. In this case, don't
// start a new request and just let the previous one finish.
if (pending_seed_request_) {
return false;
}
last_request_was_http_retry_ = is_http_retry;
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("chrome_variations_service", R"(
semantics {
sender: "Chrome Variations Service"
description:
"Retrieves the list of Google Chrome's Variations from the server, "
"which will apply to the next Chrome session upon a restart."
trigger:
"Requests are made periodically while Google Chrome is running."
data: "The operating system name."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting: "This feature cannot be disabled by settings."
policy_exception_justification:
"The ChromeVariations policy prevents Variations from applying, "
"but Google Chrome still downloads Variations from the server "
"periodically. This way, the downloaded Variations apply "
"immediately on restart if you unset the ChromeVariations policy."
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = url;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
std::string serial_number = GetLatestSerialNumber();
if (!serial_number.empty()) {
// Get the seed only if its serial number doesn't match what we have.
// If the fetch is an HTTP retry, encrypt the If-None-Match header.
if (is_http_retry) {
if (!EncryptString(serial_number, &serial_number)) {
return false;
}
serial_number = base::Base64Encode(serial_number);
}
resource_request->headers.SetHeader("If-None-Match", serial_number);
}
const bool enable_deltas =
!serial_number.empty() && !delta_error_since_last_success_;
// Tell the server that delta-compressed and gzipped seeds are supported.
const char* supported_im = enable_deltas ? "x-bm,gzip" : "gzip";
resource_request->headers.SetHeader("A-IM", supported_im);
pending_seed_request_ = network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation);
// Ensure our callback is called even with "304 Not Modified" responses.
pending_seed_request_->SetAllowHttpErrorResults(true);
pending_seed_request_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
client_->GetURLLoaderFactory().get(),
base::BindOnce(&VariationsService::OnSimpleLoaderComplete,
weak_ptr_factory_.GetWeakPtr()));
const base::TimeTicks now = base::TimeTicks::Now();
base::TimeDelta time_since_last_fetch;
// Record a time delta of 0 (default value) if there was no previous fetch.
if (!last_request_started_time_.is_null()) {
time_since_last_fetch = now - last_request_started_time_;
}
UMA_HISTOGRAM_CUSTOM_COUNTS("Variations.TimeSinceLastFetchAttempt",
time_since_last_fetch.InMinutes(), 1,
base::Days(7).InMinutes(), 50);
++request_count_;
last_request_started_time_ = now;
delta_error_since_last_success_ = false;
return true;
}
void VariationsService::StoreSeed(std::string seed_data,
std::string seed_signature,
std::string country_code,
base::Time date_fetched,
bool is_delta_compressed,
bool is_gzip_compressed) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::OnceCallback<void(bool, VariationsSeed)> done_callback =
base::BindOnce(&VariationsService::OnSeedStoreResult,
weak_ptr_factory_.GetWeakPtr(), is_delta_compressed);
field_trial_creator_.seed_store()->StoreSeedData(
std::move(seed_data), std::move(seed_signature), std::move(country_code),
date_fetched, is_delta_compressed, is_gzip_compressed,
std::move(done_callback));
}
void VariationsService::OnSeedStoreResult(bool is_delta_compressed,
bool store_success,
VariationsSeed seed) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!store_success && is_delta_compressed) {
delta_error_since_last_success_ = true;
// |request_scheduler_| will be null during unit tests.
if (request_scheduler_) {
request_scheduler_->ScheduleFetchShortly();
}
}
if (store_success) {
// When the new seed is stored, the active seed will be stored as the safe
// seed.
RecordSuccessfulFetchNewSeed();
// Now, do simulation to determine if there are any kill-switches that were
// activated by this seed.
PerformSimulationWithVersion(seed, client_->GetVersionForSimulation());
}
}
void VariationsService::InitResourceRequestedAllowedNotifier() {
// ResourceRequestAllowedNotifier does not install an observer if there is no
// NetworkChangeNotifier, which results in never being notified of changes to
// network status.
resource_request_allowed_notifier_->Init(this, /*leaky=*/false,
/*wait_for_eula=*/false);
}
void VariationsService::StartRepeatedVariationsSeedFetch() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Initialize Variations server URLs.
variations_server_url_ = GetVariationsServerURL(USE_HTTPS);
insecure_variations_server_url_ = GetVariationsServerURL(USE_HTTP);
DCHECK(!request_scheduler_);
request_scheduler_.reset(VariationsRequestScheduler::Create(
base::BindRepeating(&VariationsService::FetchVariationsSeed,
weak_ptr_factory_.GetWeakPtr()),
local_state_));
// Note that the act of starting the scheduler will start the fetch, if the
// scheduler deems appropriate.
request_scheduler_->Start();
}
void VariationsService::FetchVariationsSeed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const web_resource::ResourceRequestAllowedNotifier::State state =
resource_request_allowed_notifier_->GetResourceRequestsAllowedState();
RecordRequestsAllowedHistogram(ResourceRequestStateToHistogramValue(state));
if (state != web_resource::ResourceRequestAllowedNotifier::ALLOWED) {
DVLOG(1) << "Resource requests were not allowed. Waiting for notification.";
return;
}
DoActualFetch();
}
void VariationsService::NotifyObservers(const SeedSimulationResult& result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (result.kill_critical_group_change_count > 0) {
for (auto& observer : observer_list_) {
observer.OnExperimentChangesDetected(Observer::CRITICAL);
}
} else if (result.kill_best_effort_group_change_count > 0) {
for (auto& observer : observer_list_) {
observer.OnExperimentChangesDetected(Observer::BEST_EFFORT);
}
}
}
void VariationsService::OnSimpleLoaderComplete(
std::optional<std::string> response_body) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
TRACE_EVENT0("browser", "VariationsService::OnSimpleLoaderComplete");
const bool is_first_request = !initial_request_completed_;
initial_request_completed_ = true;
const base::TimeTicks now = base::TimeTicks::Now();
if (is_first_request &&
!local_state_->HasPrefPath(prefs::kVariationsSeedSignature)) {
base::UmaHistogramTimes("Variations.SeedFetchTimeOnFirstRun",
now - last_request_started_time_);
}
const network::mojom::URLResponseHead* response_info =
pending_seed_request_->ResponseInfo();
const scoped_refptr<net::HttpResponseHeaders> headers =
response_info ? response_info->headers : nullptr;
const int response_code = headers ? headers->response_code() : -1;
const int net_error = pending_seed_request_->NetError();
const bool is_success = headers && response_body && (net_error == net::OK);
pending_seed_request_.reset();
if (last_request_was_http_retry_) {
base::UmaHistogramSparse("Variations.SeedFetchResponseOrErrorCode.HTTP",
is_success ? response_code : net_error);
} else {
base::UmaHistogramSparse("Variations.SeedFetchResponseOrErrorCode",
is_success ? response_code : net_error);
}
if (!is_success) {
DVLOG(1) << "Variations server request failed with error: " << net_error
<< ": " << net::ErrorToString(net_error);
// It's common for the very first fetch attempt to fail (e.g. the network
// may not yet be available). In such a case, try again soon, rather than
// waiting the full time interval.
// |request_scheduler_| will be null during unit tests.
if (is_first_request && request_scheduler_) {
request_scheduler_->ScheduleFetchShortly();
return;
}
if (MaybeRetryOverHTTP()) {
// If the retry was successfully started, return immediately,
// OnSimpleLoaderComplete will be called again when the new fetch
// finishes.
return;
}
}
// Return if there was a failure. Note that we check both |is_success| which
// is set above and the response code. There could be a case where there's a
// HTTP_OK response code but |is_success| is false, for example if the fetch
// download was interrupted after having been started.
if (!is_success || (response_code != net::HTTP_OK &&
response_code != net::HTTP_NOT_MODIFIED)) {
DVLOG(1) << "Variations server request failed: is_success=" << is_success
<< " response_code=" << response_code
<< " net_error=" << net_error;
return;
}
// At this point, |headers| and |response_body| should exist.
DCHECK(headers);
DCHECK(response_body);
std::optional<base::Time> response_date = headers->GetDateValue();
// If the seed was fetched securely, opportunistically update the network time
// tracker with the headers time.
if (response_date && !last_request_was_http_retry_) {
DCHECK(!response_date->is_null());
const base::TimeDelta latency = now - last_request_started_time_;
client_->GetNetworkTimeTracker()->UpdateNetworkTime(
response_date.value(), base::Seconds(kServerTimeResolutionInSeconds),
latency, now);
}
if (response_code == net::HTTP_NOT_MODIFIED) {
// TODO(crbug.com/420652919): Reject responses without a date.
RecordSuccessfulFetchSeedNotModified(response_date.value_or(base::Time()));
return;
}
// We're now handling the HTTP_OK success case.
DCHECK_EQ(response_code, net::HTTP_OK);
bool is_delta_compressed;
bool is_gzip_compressed;
if (!GetInstanceManipulations(headers.get(), &is_delta_compressed,
&is_gzip_compressed)) {
// The header does not specify supported instance manipulations, unable to
// process data. Details of errors were logged by GetInstanceManipulations.
ReportUnsupportedSeedFormatError();
return;
}
std::string_view signature =
GetHeaderValue(headers.get(), "X-Seed-Signature");
std::string_view country_code = GetHeaderValue(headers.get(), "X-Country");
StoreSeed(std::move(*response_body), std::string(signature),
std::string(country_code), response_date.value_or(base::Time()),
is_delta_compressed, is_gzip_compressed);
}
bool VariationsService::MaybeRetryOverHTTP() {
// If the current fetch attempt was over an HTTPS connection, retry the
// fetch immediately over an HTTP connection. We only do this if an insecure
// variations URL is set and its scheme is HTTP.
if (!last_request_was_http_retry_ &&
!insecure_variations_server_url_.is_empty() &&
insecure_variations_server_url_.SchemeIs(url::kHttpScheme)) {
return DoFetchFromURL(insecure_variations_server_url_, true);
}
return false;
}
void VariationsService::OnResourceRequestsAllowed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Note that this only attempts to fetch the seed at most once per period
// (kSeedFetchPeriodHours). This works because
// |resource_request_allowed_notifier_| only calls this method if an
// attempt was made earlier that fails (which implies that the period had
// elapsed). After a successful attempt is made, the notifier will know not
// to call this method again until another failed attempt occurs.
RecordRequestsAllowedHistogram(RESOURCE_REQUESTS_ALLOWED_NOTIFIED);
DVLOG(1) << "Retrying fetch.";
DoActualFetch();
// This service must have created a scheduler in order for this to be called.
DCHECK(request_scheduler_);
request_scheduler_->Reset();
}
void VariationsService::PerformSimulationWithVersion(
const VariationsSeed& seed,
const base::Version& version) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!version.IsValid()) {
return;
}
std::unique_ptr<ClientFilterableState> client_state =
field_trial_creator_.GetClientFilterableStateForVersion(version);
auto result = SimulateSeedStudies(seed, *client_state, *entropy_providers_);
NotifyObservers(result);
}
bool VariationsService::CallMaybeRetryOverHTTPForTesting() {
return MaybeRetryOverHTTP();
}
void VariationsService::RecordSuccessfulFetchNewSeed() {
safe_seed_manager_.RecordSuccessfulFetch(field_trial_creator_.seed_store());
}
void VariationsService::RecordSuccessfulFetchSeedNotModified(
base::Time response_date) {
// Update the client-side fetch time to the current time.
field_trial_creator_.seed_store()->RecordLastFetchTime(base::Time::Now());
safe_seed_manager_.RecordSuccessfulFetch(field_trial_creator_.seed_store());
// Update the seed date value in local state (used for expiry check on
// next start up), since 304 is a successful response. Note that the
// serial number included in the request is always that of the latest
// seed, even when running in safe mode, so it's appropriate to always
// modify the latest seed's date.
field_trial_creator_.seed_store()->UpdateSeedDateAndLogDayChange(
response_date);
}
VariationsSeedStore* VariationsService::GetSeedStoreForTesting() {
return field_trial_creator_.seed_store();
}
base::Time VariationsService::GetLatestSeedFetchTime() {
return field_trial_creator_.seed_store()->GetLatestSeedFetchTime();
}
std::unique_ptr<ClientFilterableState>
VariationsService::GetClientFilterableStateForVersion() {
const base::Version current_version(version_info::GetVersionNumber());
DCHECK(current_version.IsValid());
return field_trial_creator_.GetClientFilterableStateForVersion(
current_version);
}
std::string VariationsService::GetLatestCountry() const {
return field_trial_creator_.GetLatestCountry();
}
bool VariationsService::SetUpFieldTrials(
const std::vector<std::string>& variation_ids,
const std::string& command_line_variation_ids,
const std::vector<base::FeatureList::FeatureOverrideInfo>& extra_overrides,
std::unique_ptr<base::FeatureList> feature_list,
PlatformFieldTrials* platform_field_trials) {
ForceTrialsAtStartup(*local_state_);
return field_trial_creator_.SetUpFieldTrials(
variation_ids, command_line_variation_ids, extra_overrides,
std::move(feature_list), state_manager_, platform_field_trials,
&safe_seed_manager_,
/*add_entropy_source_to_variations_ids=*/true, *entropy_providers_);
}
std::vector<StudyGroupNames> VariationsService::GetStudiesAvailableToForce() {
VariationsSeed seed;
std::string seed_data;
std::string base64_seed_signature;
if (!field_trial_creator_.seed_store()->LoadSeed(&seed, &seed_data,
&base64_seed_signature)) {
return {};
}
// TODO(crbug.com/41492213): chrome://field-trial-internals will not support
// studies that are constrained to a layer with LIMITED entropy mode before
// limited entropy randomization fully lands.
auto entropy_providers = state_manager_->CreateEntropyProviders(
/*enable_limited_entropy_mode=*/false);
return variations::GetStudiesAvailableToForce(
std::move(seed), *entropy_providers,
*GetClientFilterableStateForVersion());
}
SeedType VariationsService::GetSeedType() const {
return field_trial_creator_.seed_type();
}
void VariationsService::OverrideCachedUIStrings() {
field_trial_creator_.OverrideCachedUIStrings();
}
void VariationsService::CancelCurrentRequestForTesting() {
pending_seed_request_.reset();
}
void VariationsService::StartRepeatedVariationsSeedFetchForTesting() {
InitResourceRequestedAllowedNotifier();
return StartRepeatedVariationsSeedFetch();
}
void VariationsService::OverridePlatform(
Study::Platform platform,
const std::string& osname_server_param_override) {
field_trial_creator_.OverrideVariationsPlatform(platform);
osname_server_param_override_ = osname_server_param_override;
}
std::string VariationsService::GetOverriddenPermanentCountry() const {
return local_state_->GetString(prefs::kVariationsPermanentOverriddenCountry);
}
std::string VariationsService::GetStoredPermanentCountry() const {
return field_trial_creator_.GetPermanentConsistencyCountry();
}
bool VariationsService::OverrideStoredPermanentCountry(
const std::string& country_override) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const std::string country_override_lowercase =
base::ToLowerASCII(country_override);
const std::string stored_country =
local_state_->GetString(prefs::kVariationsPermanentOverriddenCountry);
if (stored_country == country_override_lowercase) {
return false;
}
field_trial_creator_.StoreVariationsOverriddenCountry(
country_override_lowercase);
return true;
}
} // namespace variations