blob: 7fb3e4f870708c623f7153c529fc4513825937f6 [file] [log] [blame]
// Copyright 2022 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cryptohome/uss_experiment_config_fetcher.h"
#include <memory>
#include <string>
#include <utility>
#include <base/logging.h>
#include <base/rand_util.h>
#include <base/strings/string_util.h>
#include <base/system/sys_info.h>
#include <base/values.h>
#include <brillo/http/http_transport.h>
#include <brillo/http/http_utils.h>
#include <shill/dbus-constants.h>
#include <shill/dbus-proxies.h>
#include "cryptohome/cryptohome_metrics.h"
#include "cryptohome/user_secret_stash.h"
namespace cryptohome {
namespace {
constexpr char kGstaticUrlPrefix[] =
"https://www.gstatic.com/uss-experiment/v1.json";
constexpr char kConnectionStateOnline[] = "online";
constexpr char kDefaultConfigKey[] = "default";
constexpr char kConfigPopulationKey[] = "population";
constexpr char kConfigLastInvalidKey[] = "last_invalid";
void LogUssExperimentConfig(int last_invalid, double population) {
LOG(INFO) << "USS experiment config fetched from server: last_inavlid = "
<< last_invalid << ", population = " << population;
}
void SetUssExperimentFlag(int last_invalid, double population) {
LogUssExperimentConfig(last_invalid, population);
bool enabled;
if (last_invalid >= UserSecretStashExperimentVersion()) {
enabled = false;
} else {
// `population` is directly interpreted as the probability to enable the
// experiment. This will result in roughly `population` portion of the total
// population enabling the experiment.
enabled = base::RandDouble() < population;
}
FetchUssExperimentConfigStatus status =
enabled ? FetchUssExperimentConfigStatus::kEnabled
: FetchUssExperimentConfigStatus::kDisabled;
ReportFetchUssExperimentConfigStatus(status);
SetUserSecretStashExperimentFlag(enabled);
}
void ReportFetchError() {
ReportFetchUssExperimentConfigStatus(FetchUssExperimentConfigStatus::kError);
}
} // namespace
std::unique_ptr<UssExperimentConfigFetcher> UssExperimentConfigFetcher::Create(
const scoped_refptr<dbus::Bus>& bus) {
auto uss_experiment_config_fetcher =
std::make_unique<UssExperimentConfigFetcher>();
uss_experiment_config_fetcher->Initialize(bus);
return uss_experiment_config_fetcher;
}
void UssExperimentConfigFetcher::Initialize(
const scoped_refptr<dbus::Bus>& bus) {
base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_TRACK",
&chromeos_release_track_);
transport_ = brillo::http::Transport::CreateDefault();
manager_proxy_ = std::make_unique<org::chromium::flimflam::ManagerProxy>(bus);
manager_proxy_->RegisterPropertyChangedSignalHandler(
base::BindRepeating(&UssExperimentConfigFetcher::OnManagerPropertyChange,
weak_factory_.GetWeakPtr()),
base::BindOnce(
&UssExperimentConfigFetcher::OnManagerPropertyChangeRegistration,
weak_factory_.GetWeakPtr()));
}
void UssExperimentConfigFetcher::OnManagerPropertyChangeRegistration(
const std::string& /*interface*/,
const std::string& /*signal_name*/,
bool success) {
if (!success) {
LOG(WARNING) << "Unable to register for shill manager change events.";
return;
}
brillo::VariantDictionary properties;
if (!manager_proxy_->GetProperties(&properties, nullptr)) {
LOG(WARNING) << "Unable to get shill manager properties.";
return;
}
auto it = properties.find(shill::kConnectionStateProperty);
if (it == properties.end()) {
return;
}
OnManagerPropertyChange(shill::kConnectionStateProperty, it->second);
}
void UssExperimentConfigFetcher::OnManagerPropertyChange(
const std::string& property_name, const brillo::Any& property_value) {
// Only handle changes to the connection state.
if (property_name != shill::kConnectionStateProperty) {
return;
}
std::string connection_state;
if (!property_value.GetValue(&connection_state)) {
LOG(WARNING)
<< "Connection state fetched from shill manager is not a string.";
return;
}
if (base::EqualsCaseInsensitiveASCII(connection_state,
kConnectionStateOnline)) {
Fetch(base::BindRepeating(&SetUssExperimentFlag));
}
}
void UssExperimentConfigFetcher::Fetch(
UssExperimentConfigFetcher::FetchSuccessCallback success_callback) {
brillo::ErrorPtr error;
std::unique_ptr<brillo::http::Response> response;
// TODO(https://crbug.com/714018): This should actually be a OnceCallback but
// the brillo http interface hasn't migrated. Switch to BindOnce after
// migrated.
brillo::http::Get(
kGstaticUrlPrefix, {}, transport_,
base::BindRepeating(&UssExperimentConfigFetcher::OnFetchSuccess,
weak_factory_.GetWeakPtr(), success_callback),
base::BindRepeating([](brillo::http::RequestID, const brillo::Error*) {
ReportFetchError();
}));
}
void UssExperimentConfigFetcher::OnFetchSuccess(
UssExperimentConfigFetcher::FetchSuccessCallback success_callback,
brillo::http::RequestID /*request_id*/,
std::unique_ptr<brillo::http::Response> response) {
// If we didn't successfully parse the device's release track, we can't
// determine which channel we are in to parse corresponding config fields.
if (chromeos_release_track_.empty()) {
LOG(WARNING) << "Failed to determine which channel the device is in.";
ReportFetchError();
return;
}
int status_code = response->GetStatusCode();
if (status_code != brillo::http::status_code::Ok) {
LOG(WARNING) << "Fetch USS config failed with status code: " << status_code;
ReportFetchError();
return;
}
// The fetched config should be a valid json file.
brillo::ErrorPtr error;
const std::optional<base::Value> json =
brillo::http::ParseJsonResponse(response.get(), nullptr, &error);
if (error || !json.has_value()) {
LOG(WARNING) << "The fetched USS config is not a valid json file.";
ReportFetchError();
return;
}
// Check whether the `last_invalid` field is present in the config that
// corresponds to this device's channel. If not, fallback to the default
// config.
const std::string last_invalid_path =
base::JoinString({chromeos_release_track_, kConfigLastInvalidKey}, ".");
std::optional<int> last_invalid = json->FindIntPath(last_invalid_path);
if (!last_invalid.has_value()) {
const std::string default_last_invalid_path =
base::JoinString({kDefaultConfigKey, kConfigLastInvalidKey}, ".");
last_invalid = json->FindIntPath(default_last_invalid_path);
}
// Check whether the `population` field is present in the config that
// corresponds to this device's channel. If not, fallback to the default
// config.
const std::string population_path =
base::JoinString({chromeos_release_track_, kConfigPopulationKey}, ".");
std::optional<double> population = json->FindDoublePath(population_path);
if (!population.has_value()) {
const std::string default_population_path =
base::JoinString({kDefaultConfigKey, kConfigPopulationKey}, ".");
population = json->FindDoublePath(default_population_path);
}
// Check that both fields are parsed successfully.
if (!last_invalid.has_value()) {
LOG(WARNING)
<< "Failed to parse `last_inavlid` field in the fetched USS config.";
ReportFetchError();
return;
}
if (!population.has_value()) {
LOG(WARNING)
<< "Failed to parse `population` field in the fetched USS config.";
ReportFetchError();
return;
}
success_callback.Run(*last_invalid, *population);
}
void UssExperimentConfigFetcher::SetReleaseTrackForTesting(std::string track) {
chromeos_release_track_ = track;
}
void UssExperimentConfigFetcher::SetTransportForTesting(
std::shared_ptr<brillo::http::Transport> transport) {
transport_ = transport;
}
void UssExperimentConfigFetcher::SetProxyForTesting(
std::unique_ptr<org::chromium::flimflam::ManagerProxyInterface>
manager_proxy) {
manager_proxy_ = std::move(manager_proxy);
}
} // namespace cryptohome