blob: 9832a36b53f5dc22a35e4ca0a24e90affae05ab7 [file] [log] [blame]
// Copyright 2024 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/enterprise_companion/event_logger.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/platform_file.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.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/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 "chrome/enterprise_companion/enterprise_companion_branding.h"
#include "chrome/enterprise_companion/installer_paths.h"
#include "chrome/enterprise_companion/proto/enterprise_companion_event.pb.h"
#include "chrome/enterprise_companion/proto/log_request.pb.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/cookie_access_result.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_request_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/origin.h"
namespace enterprise_companion {
namespace {
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."
})");
// An individual event logger. Events are locally batched and flushed to the
// manager, which performs global batching.
class EventLoggerImpl : public EventLogger {
public:
using EventConsumerCallback = base::RepeatingCallback<void(
std::vector<proto::EnterpriseCompanionEvent> events)>;
EventLoggerImpl(EventConsumerCallback event_consumer,
const base::Clock* clock)
: event_consumer_(event_consumer), clock_(clock) {}
void Flush() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
event_consumer_.Run(std::move(events_));
}
OnEnrollmentFinishCallback OnEnrollmentStart() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::BindPostTaskToCurrentDefault(
base::BindOnce(&EventLoggerImpl::OnEnrollmentFinish,
base::WrapRefCounted(this), clock_->Now()));
}
OnPolicyFetchFinishCallback OnPolicyFetchStart() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::BindPostTaskToCurrentDefault(
base::BindOnce(&EventLoggerImpl::OnPolicyFetchFinish,
base::WrapRefCounted(this), clock_->Now()));
}
private:
SEQUENCE_CHECKER(sequence_checker_);
EventConsumerCallback event_consumer_;
const raw_ptr<const base::Clock> clock_;
std::vector<proto::EnterpriseCompanionEvent> events_;
void OnEnrollmentFinish(base::Time start_time,
const EnterpriseCompanionStatus& status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeDelta duration = clock_->Now() - start_time;
proto::EnterpriseCompanionEvent event;
*event.mutable_status() = status.ToProtoStatus();
event.set_duration_ms(duration.InMilliseconds());
*event.mutable_browser_enrollment_event() = proto::BrowserEnrollmentEvent();
events_.push_back(std::move(event));
}
void OnPolicyFetchFinish(base::Time start_time,
const EnterpriseCompanionStatus& status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeDelta duration = clock_->Now() - start_time;
proto::EnterpriseCompanionEvent event;
*event.mutable_status() = status.ToProtoStatus();
event.set_duration_ms(duration.InMilliseconds());
*event.mutable_policy_fetch_event() = proto::PolicyFetchEvent();
events_.push_back(std::move(event));
}
private:
~EventLoggerImpl() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Flush();
}
};
class EventLogUploaderImpl : public EventLogUploader {
public:
explicit EventLogUploaderImpl(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(url_loader_factory) {}
~EventLogUploaderImpl() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void DoLogRequest(proto::LogRequest request,
LogRequestCallback callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!url_loader_) << "Overlapping log requests are not permitted.";
std::unique_ptr<network::ResourceRequest> resource_request =
std::make_unique<network::ResourceRequest>();
resource_request->url = GURL(ENTERPRISE_COMPANION_EVENT_LOGGING_URL);
resource_request->request_initiator =
url::Origin::Create(GURL(ENTERPRISE_COMPANION_EVENT_LOGGING_URL));
resource_request->site_for_cookies = net::SiteForCookies::FromUrl(
GURL(ENTERPRISE_COMPANION_EVENT_LOGGING_URL));
resource_request->method = net::HttpRequestHeaders::kPostMethod;
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
kTrafficAnnotation);
url_loader_->SetAllowHttpErrorResults(true);
url_loader_->AttachStringForUpload(request.SerializeAsString());
url_loader_->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&EventLogUploaderImpl::OnLogResponseReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
1024 * 1024 /* 1 MiB */);
}
private:
SEQUENCE_CHECKER(sequence_checker_);
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
std::unique_ptr<network::SimpleURLLoader> url_loader_;
void OnLogResponseReceived(LogRequestCallback callback,
std::optional<std::string> response_body) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::unique_ptr<network::SimpleURLLoader> url_loader =
std::move(url_loader_);
CHECK(url_loader);
if (url_loader->NetError() != net::OK) {
LOG(ERROR) << "Logging request failed "
<< net::ErrorToString(url_loader->NetError());
}
std::move(callback).Run(url_loader->TakeResponseInfo(),
std::move(response_body));
}
base::WeakPtrFactory<EventLogUploaderImpl> weak_ptr_factory_{this};
};
class EventLoggerManagerImpl : public EventLoggerManager {
public:
EventLoggerManagerImpl(const base::Clock* clock,
std::unique_ptr<EventLogUploader> uploader)
: clock_(clock), uploader_(std::move(uploader)) {}
~EventLoggerManagerImpl() override = default;
scoped_refptr<EventLogger> CreateEventLogger() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::MakeRefCounted<EventLoggerImpl>(
base::BindRepeating(&EventLoggerManagerImpl::AcceptLogs,
weak_ptr_factory_.GetWeakPtr()),
clock_);
}
private:
SEQUENCE_CHECKER(sequence_checker_);
const raw_ptr<const base::Clock> clock_;
std::unique_ptr<EventLogUploader> uploader_;
bool can_make_request_ = true;
std::vector<proto::EnterpriseCompanionEvent> events_;
base::OneShotTimer cooldown_timer_;
// Called by EventLoggers to ingest a batch of logs. If not rate-limited, this
// will synchronously trigger a transmission. Otherwise, the logs are queued
// to be uploaded.
void AcceptLogs(std::vector<proto::EnterpriseCompanionEvent> events) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
events_.insert(events_.end(), std::make_move_iterator(events.begin()),
std::make_move_iterator(events.end()));
if (can_make_request_) {
Transmit();
}
}
// Transmits logs to the remote endpoint, if there are any. Schedules the
// next transmission using the timeout provided by the server.
void Transmit() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (events_.empty()) {
return;
}
can_make_request_ = false;
int64_t now_ms = clock_->Now().InMillisecondsSinceUnixEpoch();
proto::LogRequest request;
request.set_request_time_ms(now_ms);
request.mutable_client_info()->set_client_type(
proto::ClientInfo_ClientType_CHROME_ENTERPRISE_COMPANION);
request.set_log_source(proto::CHROME_ENTERPRISE_COMPANION_APP);
proto::ChromeEnterpriseCompanionAppExtension extension;
for (const proto::EnterpriseCompanionEvent& event : events_) {
*extension.add_event() = event;
}
proto::LogEvent* log_event = request.add_log_event();
log_event->set_event_time_ms(now_ms);
log_event->set_source_extension(extension.SerializeAsString());
uploader_->DoLogRequest(
std::move(request),
base::BindOnce(&EventLoggerManagerImpl::OnLogResponseReceived,
weak_ptr_factory_.GetWeakPtr()));
}
void OnLogResponseReceived(
mojo::StructPtr<network::mojom::URLResponseHead> response_info,
std::optional<std::string> response_body) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (ShouldDeleteEvents(response_info.get())) {
events_.clear();
}
if (!response_info) {
SetCooldown(kMinLogTransmissionCooldown);
return;
}
proto::LogResponse response;
if (!response.ParseFromString(*response_body)) {
LOG(ERROR) << "Failed to parse log response proto";
if (response_info->mime_type != "text/plain") {
LOG(ERROR) << "Log response: " << *response_body;
}
SetCooldown(kMinLogTransmissionCooldown);
return;
}
SetCooldown(
std::max(base::Milliseconds(response.next_request_wait_millis()),
kMinLogTransmissionCooldown));
}
void SetCooldown(const base::TimeDelta& cooldown) {
cooldown_timer_.Start(
FROM_HERE, cooldown,
base::BindOnce(&EventLoggerManagerImpl::OnCooldownExhausted,
weak_ptr_factory_.GetWeakPtr()));
}
void OnCooldownExhausted() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
can_make_request_ = true;
if (!events_.empty()) {
Transmit();
}
}
bool ShouldDeleteEvents(
const network::mojom::URLResponseHead* response_info) {
if (!response_info || !response_info->headers) {
return false;
}
int response_code = response_info->headers->response_code();
// Delete logs for the 2xx and 4xx family of responses.
return (response_code >= 200 && response_code < 300) ||
(response_code >= 400 && response_code < 500);
}
base::WeakPtrFactory<EventLoggerManagerImpl> 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(
GURL(ENTERPRISE_COMPANION_EVENT_LOGGING_URL), kLoggingCookieName,
cookie_listener_receiver_.BindNewPipeAndPassRemote());
InitLoggingCookie(std::move(callback));
}
private:
// Adds the persisted event logging cookie to the store, if one is present.
// Otherwise, adds an empty cookie, prompting the server to provision a new
// one on the next request.
void InitLoggingCookie(base::OnceClosure callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::string cookie_value(kCookieValueBufferSize, 0);
std::optional<size_t> bytes_read =
cookie_file_.Read(0, base::as_writable_byte_span(cookie_value));
if (!bytes_read || bytes_read == 0) {
// If no logging cookie is present, the default value will signal to the
// server to provision a new one.
cookie_value = kLoggingCookieDefaultValue;
} else {
cookie_value.resize(*bytes_read);
}
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));
url::SchemeHostPort event_logging_scheme_host_port =
url::SchemeHostPort(GURL(ENTERPRISE_COMPANION_EVENT_LOGGING_URL));
std::unique_ptr<net::CanonicalCookie> cookie =
net::CanonicalCookie::CreateSanitizedCookie(
GURL(ENTERPRISE_COMPANION_EVENT_LOGGING_URL), "NID", cookie_value,
base::StrCat({".", event_logging_scheme_host_port.host()}),
/*path=*/"/", /*creation_time=*/base::Time::Now(),
/*expiration_time=*/base::Time::Now() + base::Days(180),
/*last_access_time=*/base::Time::Now(), /*secure=*/false,
/*http_only=*/true, net::CookieSameSite::UNSPECIFIED,
net::CookiePriority::COOKIE_PRIORITY_DEFAULT,
/*partition_key=*/std::nullopt, /*status=*/nullptr);
if (cookie->IsCanonical()) {
cookie_manager_remote_->SetCanonicalCookie(
*cookie, GURL(ENTERPRISE_COMPANION_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)));
} else {
LOG(ERROR) << "Failed to initialize logging cookie. Not canonical: "
<< cookie->DebugString();
std::move(callback).Run();
}
}
// Overrides for network::mojom::CookieChangeListener
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_bytes(base::make_span(change.cookie.Value()))) ||
!cookie_file_.SetLength(change.cookie.Value().length())) {
LOG(ERROR) << "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};
};
} // namespace
const char kLoggingCookieName[] = "NID";
const char kLoggingCookieDefaultValue[] = "\"\"";
std::unique_ptr<EventLoggerManager> CreateEventLoggerManager(
std::unique_ptr<EventLogUploader> uploader,
const base::Clock* clock) {
return std::make_unique<EventLoggerManagerImpl>(clock, std::move(uploader));
}
std::unique_ptr<EventLogUploader> CreateEventLogUploader(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
return std::make_unique<EventLogUploaderImpl>(std::move(url_loader_factory));
}
std::optional<base::File> OpenDefaultEventLoggerCookieFile() {
std::optional<base::FilePath> install_dir = GetInstallDirectory();
if (!install_dir) {
return std::nullopt;
}
return base::File(install_dir->AppendASCII("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));
}
} // namespace enterprise_companion