blob: 43097591097dc7be343eac980b66f35a8bc8590a [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/updater/event_logger.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/threading/sequence_bound.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "chrome/enterprise_companion/telemetry_logger/proto/log_request.pb.h"
#include "chrome/updater/branded_constants.h"
#include "chrome/updater/configurator.h"
#include "chrome/updater/persisted_data.h"
#include "chrome/updater/protos/omaha_usage_stats_event.pb.h"
#include "chrome/updater/updater_scope.h"
#include "chrome/updater/updater_version.h"
#include "components/update_client/network.h"
#include "event_logger.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "url/gurl.h"
namespace updater {
namespace {
// Returns the logging cookie value which should be used in an event
// transmission. If a cookie is expired or not persisted, a special value is
// used to request a new token from the server. Expired cookies are cleared from
// the store.
std::string GetLoggingCookieValue(base::Time now,
scoped_refptr<PersistedData> persisted_data) {
static constexpr char kDefaultLoggingCookie[] = "\"\"";
std::optional<PersistedData::Cookie> logging_cookie =
persisted_data->GetRemoteLoggingCookie();
if (!logging_cookie) {
return kDefaultLoggingCookie;
}
if (logging_cookie->expiration <= now) {
VLOG(1) << "Clearing expired logging cookie";
persisted_data->ClearRemoteLoggingCookie();
return kDefaultLoggingCookie;
}
return logging_cookie->value;
}
proto::Omaha4UsageStatsMetadata GetMetadata(
UpdaterScope scope,
scoped_refptr<Configurator> configurator,
bool is_cloud_managed) {
proto::Omaha4UsageStatsMetadata metadata;
metadata.set_platform(configurator->GetOSLongName());
metadata.set_os_version(base::SysInfo::OperatingSystemVersion());
metadata.set_cpu_architecture(base::SysInfo::OperatingSystemArchitecture());
metadata.set_o4_omaha_cohort_id(
configurator->GetPersistedData()->GetCohort(kUpdaterAppId));
metadata.set_app_version(kUpdaterVersion);
metadata.set_is_machine(IsSystemInstall(scope));
metadata.set_is_cbcm_managed(is_cloud_managed);
std::optional<bool> externally_managed =
configurator->IsMachineExternallyManaged();
if (externally_managed.has_value()) {
metadata.set_is_domain_joined(*externally_managed);
}
return metadata;
}
} // namespace
using HttpRequestCallback =
base::OnceCallback<void(std::optional<int> http_status,
std::optional<std::string> response_body)>;
using ::updater::proto::Omaha4Metric;
using ::updater::proto::Omaha4UsageStatsExtension;
using ::updater::proto::Omaha4UsageStatsMetadata;
RemoteLoggingDelegate::RemoteLoggingDelegate(
UpdaterScope scope,
const GURL& event_logging_url,
bool is_cloud_managed,
scoped_refptr<Configurator> configurator,
std::unique_ptr<base::Clock> clock)
: event_logging_url_(event_logging_url),
metadata_(::updater::GetMetadata(scope, configurator, is_cloud_managed)),
minimum_cooldown_(configurator->MinimumEventLoggingCooldown()),
configurator_(configurator),
clock_(std::move(clock)),
persisted_data_(configurator_->GetUpdaterPersistedData()),
main_sequence_(base::SequencedTaskRunner::GetCurrentDefault()) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
RemoteLoggingDelegate::~RemoteLoggingDelegate() = default;
void RemoteLoggingDelegate::StoreNextAllowedAttemptTime(
base::Time time,
base::OnceClosure callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
main_sequence_->PostTaskAndReply(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<PersistedData> persisted_data, base::Time time) {
persisted_data->SetNextAllowedLoggingAttemptTime(time);
},
persisted_data_, time),
std::move(callback));
}
void RemoteLoggingDelegate::DoPostRequest(
const std::string& request_body,
base::OnceCallback<void(std::optional<int> http_status,
std::optional<std::string> response_body)>
callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
main_sequence_->PostTask(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<Configurator> configurator, const GURL& url,
const std::string& request_body, base::Time now,
base::OnceCallback<void(std::optional<int>,
std::optional<std::string> response_body)>
callback) {
std::unique_ptr<update_client::NetworkFetcher> fetcher =
configurator->GetNetworkFetcherFactory()->Create();
if (!fetcher) {
VLOG(1) << "Failed to create network fetcher for logging request";
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::nullopt,
std::nullopt));
return;
}
// The fetcher and response code are retained by
// the completion callback.
auto response_code =
std::make_unique<std::optional<int>>(std::nullopt);
std::optional<int>* response_code_ptr = response_code.get();
update_client::NetworkFetcher* fetcher_ptr = fetcher.get();
fetcher_ptr->PostRequest(
url, request_body, "application/x-protobuffer",
{{update_client::NetworkFetcher::kHeaderCookie,
base::StrCat(
{"NID=",
GetLoggingCookieValue(
now, configurator->GetUpdaterPersistedData())})}},
base::BindRepeating(
[](std::optional<int>* response_code_out, int response_code,
int64_t content_length) {
*response_code_out = response_code;
},
response_code_ptr),
/*progress_callback=*/base::DoNothing(),
base::BindOnce(
[](base::Time now,
scoped_refptr<PersistedData> persisted_data,
HttpRequestCallback callback,
std::unique_ptr<std::optional<int>> response_code,
std::unique_ptr<update_client::NetworkFetcher> fetcher,
std::optional<std::string> response_body, int net_error,
const std::string& header_etag,
const std::string& header_x_cup_server_proof,
const std::string& header_set_cookie,
int64_t xheader_retry_after_sec) {
if (net_error) {
VLOG(1)
<< "Upload failed due to net error " << net_error;
VLOG_IF(1, response_code->has_value())
<< "HTTP response code: " << response_code->value();
base::SequencedTaskRunner::GetCurrentDefault()
->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
std::nullopt, std::nullopt));
return;
}
std::optional<PersistedData::Cookie> logging_cookie =
ExtractEventLoggingCookie(now, header_set_cookie);
if (logging_cookie) {
persisted_data->SetRemoteLoggingCookie(*logging_cookie);
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), *response_code,
response_body));
},
now, configurator->GetUpdaterPersistedData(),
std::move(callback), std::move(response_code),
std::move(fetcher)));
},
configurator_, event_logging_url_, request_body, clock_->Now(),
base::BindPostTaskToCurrentDefault(std::move(callback))));
}
int RemoteLoggingDelegate::GetLogIdentifier() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return enterprise_companion::telemetry_logger::proto::CHROME_UPDATER;
}
std::string RemoteLoggingDelegate::AggregateAndSerializeEvents(
base::span<Omaha4Metric> events) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Omaha4UsageStatsExtension extension;
*extension.mutable_metadata() = GetMetadata();
for (const Omaha4Metric& metric : events) {
*extension.add_metric() = metric;
}
return extension.SerializeAsString();
}
base::TimeDelta RemoteLoggingDelegate::MinimumCooldownTime() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return minimum_cooldown_;
}
Omaha4UsageStatsMetadata RemoteLoggingDelegate::GetMetadata() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return metadata_;
}
// Parses the NID cookie from a returned Cookie: HTTP header.
std::optional<PersistedData::Cookie> ExtractEventLoggingCookie(
base::Time now,
const std::string& set_cookie_value) {
static constexpr base::TimeDelta kDefaultTtl = base::Days(180);
std::optional<std::string> cookie_value;
std::optional<base::Time> expiration;
for (const std::string& s :
base::SplitString(set_cookie_value, ";", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
std::optional<std::pair<std::string_view, std::string_view>> kv_pair =
base::SplitStringOnce(s, "=");
if (!kv_pair.has_value()) {
continue;
}
auto [key, value] = *kv_pair;
if (key == "NID") {
if (!value.empty()) {
cookie_value = value;
}
} else if (base::EqualsCaseInsensitiveASCII(key, "expires")) {
base::Time parsed_time;
if (base::Time::FromString(std::string(value).c_str(), &parsed_time)) {
expiration = parsed_time;
}
}
}
if (!cookie_value) {
return std::nullopt;
}
return PersistedData::Cookie{
.value = *std::move(cookie_value),
.expiration = expiration ? *expiration : now + kDefaultTtl,
};
}
} // namespace updater