#include "chrome/enterprise_companion/event_logger.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/platform_file.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/strings/stringprintf.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 "base/timer/timer.h"
#include "build/build_config.h"
#include "chrome/enterprise_companion/enterprise_companion_branding.h"
#include "chrome/enterprise_companion/enterprise_companion_client.h"
#include "chrome/enterprise_companion/enterprise_companion_version.h"
#include "chrome/enterprise_companion/flags.h"
#include "chrome/enterprise_companion/global_constants.h"
#include "chrome/enterprise_companion/installer_paths.h"
#include "chrome/enterprise_companion/proto/enterprise_companion_event.pb.h"
#include "chrome/enterprise_companion/telemetry_logger/proto/log_request.pb.h"
#include "chrome/enterprise_companion/telemetry_logger/telemetry_logger.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "net/base/net_errors.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.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/cookie_manager.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#if BUILDFLAG(IS_WIN)
#include "base/win/windows_version.h"
#endif
namespace enterprise_companion {
namespace {
using HttpRequestCallback =
base::OnceCallback<void(std::optional<int> http_status,
std::optional<std::string> response_body)>;
constexpr size_t kCookieValueBufferSize = 1024;
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("enterprise_companion_event_logging",
R"(
semantics {
sender: "Chrome Enterprise Companion App"
description:
"Service logging for the Chrome Enterprise Companion App."
trigger: "Periodic tasks."
data: "Metrics about the Chrome Enterprise Companion App."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
email: "noahrose@google.com"
}
contacts {
email: "chrome-updates-dev@chromium.org"
}
}
last_reviewed: "2024-07-08"
user_data {
type: NONE
}
}
policy {
cookies_allowed: YES
cookies_store: "Chrome Enterprise Companion App cookie store"
setting:
"This feature cannot be disabled other than by uninstalling the "
"Chrome Enterprise Companion App."
policy_exception_justification:
"This request is made by the Chrome Enterprise Companion App, not "
"Chrome itself."
})");
proto::EnterpriseCompanionMetadata GetMetadata() {
proto::EnterpriseCompanionMetadata metadata;
metadata.set_app_version(kEnterpriseCompanionVersion);
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(kCohortIdSwitch)) {
metadata.set_omaha_cohort_id(
command_line->GetSwitchValueASCII(kCohortIdSwitch));
}
#if defined(ARCH_CPU_X86)
metadata.set_application_arch(proto::X86);
#elif defined(ARCH_CPU_X86_64)
metadata.set_application_arch(proto::X86_64);
#elif defined(ARCH_CPU_ARM64)
metadata.set_application_arch(proto::ARM64);
#endif
#if BUILDFLAG(IS_LINUX)
metadata.set_os_platform(proto::LINUX);
#elif BUILDFLAG(IS_MAC)
metadata.set_os_platform(proto::MAC);
#elif BUILDFLAG(IS_WIN)
metadata.set_os_platform(proto::WINDOWS);
#endif
#if BUILDFLAG(IS_WIN)
const base::win::OSInfo::VersionNumber v =
base::win::OSInfo::GetInstance()->version_number();
metadata.set_os_version(
base::StringPrintf("%u.%u.%u", v.major, v.minor, v.build));
#elif BUILDFLAG(IS_MAC)
int32_t os_major = 0, os_minor = 0, os_bugfix = 0;
base::SysInfo::OperatingSystemVersionNumbers(&os_major, &os_minor,
&os_bugfix);
metadata.set_os_version(base::StringPrintf("%u.%u", os_major, os_minor));
#endif
const std::string os_arch = base::SysInfo::OperatingSystemArchitecture();
if (os_arch == "x86") {
metadata.set_os_arch(proto::X86);
} else if (os_arch == "x86_64") {
metadata.set_os_arch(proto::X86_64);
} else if (os_arch == "arm64") {
metadata.set_os_arch(proto::ARM64);
}
return metadata;
}
proto::Status ToProtoStatus(EnterpriseCompanionStatus status) {
proto::Status proto_status;
proto_status.set_space(status.space());
proto_status.set_code(status.code());
return proto_status;
}
std::unique_ptr<net::CanonicalCookie> MakeLoggingCookieFromValue(
const GURL& url,
const std::string& cookie_value) {
net::CookieInclusionStatus cookie_status;
std::unique_ptr<net::CanonicalCookie> cookie =
net::CanonicalCookie::CreateSanitizedCookie(
url, "NID", cookie_value,
base::StrCat({".", url::SchemeHostPort(url).host()}),
"/", base::Time::Now(),
base::Time::Now() + base::Days(180),
base::Time::Now(), false,
true, net::CookieSameSite::UNSPECIFIED,
net::CookiePriority::COOKIE_PRIORITY_DEFAULT,
std::nullopt, &cookie_status);
if (!cookie) {
VLOG(1) << "Failed to initialize logging cookie: " << cookie_status;
return nullptr;
}
if (!cookie->IsCanonical()) {
VLOG(1) << "Failed to initialize logging cookie. Not canonical: "
<< cookie->DebugString();
return nullptr;
}
return cookie;
}
std::unique_ptr<net::CanonicalCookie> MakeLoggingCookieFromFile(
const GURL& url,
base::File& file) {
std::string cookie_value(kCookieValueBufferSize, 0);
std::optional<size_t> bytes_read =
file.Read(0, base::as_writable_byte_span(cookie_value));
if (!bytes_read || bytes_read == 0) {
return nullptr;
}
cookie_value.resize(*bytes_read);
return MakeLoggingCookieFromValue(url, cookie_value);
}
std::unique_ptr<net::CanonicalCookie> MakeLoggingCookie(const GURL& url,
base::File& file) {
std::unique_ptr<net::CanonicalCookie> file_cookie =
MakeLoggingCookieFromFile(url, file);
return file_cookie
? std::move(file_cookie)
: MakeLoggingCookieFromValue(url, kLoggingCookieDefaultValue);
}
class EventUploader {
public:
explicit EventUploader(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(url_loader_factory) {}
void DoPostRequest(const GURL& url,
const std::string& request_body,
HttpRequestCallback callback) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::unique_ptr<network::ResourceRequest> resource_request =
std::make_unique<network::ResourceRequest>();
resource_request->url = url;
resource_request->request_initiator = url::Origin::Create(url);
resource_request->site_for_cookies = net::SiteForCookies::FromUrl(url);
resource_request->method = net::HttpRequestHeaders::kPostMethod;
std::unique_ptr<network::SimpleURLLoader> url_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kTrafficAnnotation);
url_loader->SetAllowHttpErrorResults(true);
url_loader->AttachStringForUpload(request_body);
url_loader->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&EventUploader::OnLogResponseReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
std::move(callback)),
1024 * 1024 );
}
virtual ~EventUploader() {
VLOG(1) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
private:
void OnLogResponseReceived(
std::unique_ptr<network::SimpleURLLoader> url_loader,
HttpRequestCallback callback,
std::optional<std::string> response_body) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (url_loader->NetError() != net::OK) {
VLOG(1) << "Logging request failed "
<< net::ErrorToString(url_loader->NetError());
}
network::mojom::URLResponseHeadPtr response_head =
url_loader->TakeResponseInfo();
std::optional<int> http_status;
if (response_head && response_head.get()->headers) {
http_status = response_head.get()->headers->response_code();
}
std::move(callback).Run(http_status, response_body);
}
SEQUENCE_CHECKER(sequence_checker_);
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
base::WeakPtrFactory<EventUploader> weak_ptr_factory_{this};
};
class EventLoggerDelegate : public EventTelemetryLogger::Delegate {
public:
explicit EventLoggerDelegate(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: uploader_(base::SequenceBound<EventUploader>(
base::SequencedTaskRunner::GetCurrentDefault(),
url_loader_factory)) {}
void StoreNextAllowedAttemptTime(base::Time,
base::OnceClosure callback) override {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(callback));
}
void DoPostRequest(const std::string& request_body,
HttpRequestCallback callback) override {
uploader_.AsyncCall(&EventUploader::DoPostRequest)
.WithArgs(GetGlobalConstants()->EnterpriseCompanionEventLoggingURL(),
request_body,
base::BindPostTaskToCurrentDefault(std::move(callback)));
}
int GetLogIdentifier() const override {
return telemetry_logger::proto::CHROME_ENTERPRISE_COMPANION_APP;
}
std::string AggregateAndSerializeEvents(
base::span<proto::EnterpriseCompanionEvent> events) const override {
proto::ChromeEnterpriseCompanionAppExtension extension;
*extension.mutable_metadata() = GetMetadata();
for (const proto::EnterpriseCompanionEvent& event : events) {
*extension.add_event() = event;
}
return extension.SerializeAsString();
}
base::TimeDelta MinimumCooldownTime() const override {
return GetGlobalConstants()->EventLoggerMinTimeout();
}
private:
base::SequenceBound<EventUploader> uploader_;
base::WeakPtrFactory<EventLoggerDelegate> weak_ptr_factory_{this};
};
class EventLoggerCookieHandlerImpl : public EventLoggerCookieHandler,
network::mojom::CookieChangeListener {
public:
explicit EventLoggerCookieHandlerImpl(base::File cookie_file)
: cookie_file_(std::move(cookie_file)) {}
~EventLoggerCookieHandlerImpl() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void Init(mojo::PendingRemote<network::mojom::CookieManager>
cookie_manager_pending_remote,
base::OnceClosure callback) override {
cookie_manager_remote_ =
mojo::Remote(std::move(cookie_manager_pending_remote));
cookie_manager_remote_->AddCookieChangeListener(
GetGlobalConstants()->EnterpriseCompanionEventLoggingURL(),
kLoggingCookieName,
cookie_listener_receiver_.BindNewPipeAndPassRemote());
InitLoggingCookie(std::move(callback));
}
private:
void InitLoggingCookie(base::OnceClosure callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const GURL event_logging_url =
GetGlobalConstants()->EnterpriseCompanionEventLoggingURL();
std::unique_ptr<net::CanonicalCookie> cookie =
MakeLoggingCookie(event_logging_url, cookie_file_);
if (!cookie) {
VLOG(1) << "Failed to initialize logging cookie.";
std::move(callback).Run();
return;
}
net::CookieOptions cookie_options;
cookie_options.set_include_httponly();
cookie_options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext(
net::CookieOptions::SameSiteCookieContext::ContextType::
SAME_SITE_STRICT));
cookie_manager_remote_->SetCanonicalCookie(
*cookie, event_logging_url, cookie_options,
base::BindOnce([](net::CookieAccessResult result) {
LOG_IF(ERROR, !result.status.IsInclude())
<< "Failed to set logging cookie: " << result.status;
}).Then(std::move(callback)));
}
void OnCookieChange(const net::CookieChangeInfo& change) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (change.cause == net::CookieChangeCause::INSERTED) {
if (!cookie_file_.WriteAndCheck(
0, base::as_byte_span(change.cookie.Value())) ||
!cookie_file_.SetLength(change.cookie.Value().length())) {
VLOG(1) << "Failed to write logging cookie: "
<< change.cookie.DebugString();
}
}
}
SEQUENCE_CHECKER(sequence_checker_);
base::File cookie_file_;
mojo::Remote<network::mojom::CookieManager> cookie_manager_remote_;
mojo::Receiver<network::mojom::CookieChangeListener>
cookie_listener_receiver_{this};
};
class EnterpriseCompanionEventLoggerImpl
: public EnterpriseCompanionEventLogger {
public:
explicit EnterpriseCompanionEventLoggerImpl(
std::unique_ptr<EventTelemetryLogger::Delegate> logger_delegate)
: impl_(EventTelemetryLogger::Create(
std::move(logger_delegate),
/*first_allowed_attempt_time=*/std::nullopt,
true)) {}
EnterpriseCompanionEventLoggerImpl() = delete;
EnterpriseCompanionEventLoggerImpl(
const EnterpriseCompanionEventLoggerImpl&) = delete;
EnterpriseCompanionEventLoggerImpl& operator=(
const EnterpriseCompanionEventLoggerImpl) = delete;
void LogRegisterPolicyAgentEvent(
base::Time start_time,
StatusCallback callback,
const EnterpriseCompanionStatus& status) override {
VLOG(2) << __func__ << ": status=" << status.description();
proto::EnterpriseCompanionEvent event;
*event.mutable_status() = ToProtoStatus(status);
event.set_duration_ms((base::Time::Now() - start_time).InMilliseconds());
*event.mutable_browser_enrollment_event() = proto::BrowserEnrollmentEvent();
impl_->Log(event);
std::move(callback).Run(status);
}
void LogPolicyFetchEvent(base::Time start_time,
StatusCallback callback,
const EnterpriseCompanionStatus& status) override {
VLOG(2) << __func__ << ": status=" << status.description();
proto::EnterpriseCompanionEvent event;
*event.mutable_status() = ToProtoStatus(status);
event.set_duration_ms((base::Time::Now() - start_time).InMilliseconds());
*event.mutable_policy_fetch_event() = proto::PolicyFetchEvent();
impl_->Log(event);
std::move(callback).Run(status);
}
void Flush(base::OnceClosure callback) override {
return impl_->Flush(std::move(callback));
}
protected:
friend class base::RefCountedThreadSafe<EnterpriseCompanionEventLogger>;
~EnterpriseCompanionEventLoggerImpl() override = default;
private:
scoped_refptr<EventTelemetryLogger> impl_;
};
}
const char kLoggingCookieName[] = "NID";
const char kLoggingCookieDefaultValue[] = "\"\"";
std::optional<base::File> OpenDefaultEventLoggerCookieFile() {
std::optional<base::FilePath> install_dir = GetInstallDirectory();
if (!install_dir) {
return std::nullopt;
}
return base::File(install_dir->Append(FILE_PATH_LITERAL("logging_cookie")),
base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ |
base::File::FLAG_WRITE);
}
base::SequenceBound<EventLoggerCookieHandler> CreateEventLoggerCookieHandler(
std::optional<base::File> logging_cookie_file) {
if (!logging_cookie_file || !logging_cookie_file->IsValid()) {
return {};
}
return base::SequenceBound<EventLoggerCookieHandlerImpl>(
base::SequencedTaskRunner::GetCurrentDefault(),
std::move(*logging_cookie_file));
}
scoped_refptr<EnterpriseCompanionEventLogger>
EnterpriseCompanionEventLogger::Create(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
return base::MakeRefCounted<EnterpriseCompanionEventLoggerImpl>(
std::make_unique<EventLoggerDelegate>(url_loader_factory));
}
}