chromium/chromeos/dbus/missive/missive_client.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/dbus/missive/missive_client.h"

#include <cstdlib>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/sequence_checker.h"
#include "base/strings/string_util.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "chromeos/dbus/missive/fake_missive_client.h"
#include "chromeos/dbus/missive/history_tracker.h"
#include "components/reporting/proto/synced/interface.pb.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/proto/synced/record_constants.pb.h"
#include "components/reporting/util/disconnectable_client.h"
#include "components/reporting/util/status.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
#include "google_apis/google_api_keys.h"
#include "third_party/cros_system_api/dbus/missive/dbus-constants.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

using std::literals::string_view_literals::operator""sv;

using ::reporting::Priority;
using ::reporting::Record;
using ::reporting::SequenceInformation;
using ::reporting::SignedEncryptionInfo;
using ::reporting::Status;

namespace chromeos {

// This feature enables retrying enqueueing records if dBus fails. The number of
// retries is controlled by the `kNumSecondsToRetry` parameter. Enabled by
// default because this is a bug fix. Only putting it behind a feature flag for
// kill switch in case of emergency.
// TODO(b/339059662): remove feature flag once retries are in stable channel.
BASE_FEATURE(kEnableRetryEnqueueRecord,
             "EnableRetryEnqueueRecord",
             base::FEATURE_ENABLED_BY_DEFAULT);

// Number of seconds we'll retry to enqueue a record if Missive is unavailable.
// If `kEnableRetryEnqueueRecord` is not enabled, the parameter is not set, or
// set to an invalid int value, then Get() will return the default value.
// TODO(b/339059662): remove feature parameter once retries are in stable
// channel.
const base::FeatureParam<int> kNumSecondsToRetry{
    &kEnableRetryEnqueueRecord, "num_seconds_to_retry",
    /*default seconds to retry=*/2};
namespace {

constexpr char kUmaMissiveClientDbusError[] =
    "Browser.ERP.MissiveClientDbusError";

constexpr char kUmaRetryEnqueueRecordStatus[] =
    "Browser.ERP.RetryEnqueueRecordStatus";

constexpr char kUmaTimeSpentRetryingEnqueueRecord[] =
    "Browser.ERP.TimeSpentRetryingEnqueueRecord";

constexpr char kErrorNoDbusResponse[] = "Returned no response";

MissiveClient* g_instance = nullptr;

// Returns `false` if the api_key is empty or known to be used for testing
// purposes, or by devices that are running unofficial builds.
bool IsApiKeyAccepted(std::string_view api_key) {
  static constexpr std::string_view kBlockListedKeys[] = {
      "dummykey"sv, "dummytoken"sv,
      // More keys or key fragments can be added.
  };
  if (api_key.empty()) {
    LOG(ERROR) << "API Key is empty";
    return false;
  }
  const std::string lowercase_api_key = base::ToLowerASCII(api_key);
  for (auto key : kBlockListedKeys) {
    if (base::Contains(lowercase_api_key, key)) {
      LOG(ERROR) << "API Key is block-listed: " << api_key;
      return false;
    }
  }
  return true;
}

class MissiveClientImpl : public MissiveClient {
 public:
  MissiveClientImpl() : client_(origin_task_runner()) {}
  MissiveClientImpl(const MissiveClientImpl& other) = delete;
  MissiveClientImpl& operator=(const MissiveClientImpl& other) = delete;
  ~MissiveClientImpl() override = default;

  void Init(dbus::Bus* const bus) {
    DCHECK(bus);
    origin_task_runner_ = bus->GetOriginTaskRunner();

    // Never changes after this moment.
    has_valid_api_key_ = IsApiKeyAccepted(google_apis::GetAPIKey());

    // Never changes back to `false`.
    is_initialized_ = true;

    DCHECK(!missive_service_proxy_);
    missive_service_proxy_ =
        bus->GetObjectProxy(missive::kMissiveServiceName,
                            dbus::ObjectPath(missive::kMissiveServicePath));
    missive_service_proxy_->SetNameOwnerChangedCallback(base::BindRepeating(
        &MissiveClientImpl::OwnerChanged, weak_ptr_factory_.GetWeakPtr()));
    missive_service_proxy_->WaitForServiceToBeAvailable(base::BindOnce(
        &MissiveClientImpl::ServiceAvailable, weak_ptr_factory_.GetWeakPtr()));
  }

  void MaybeRetryEnqueue(
      bool is_retry,
      base::TimeTicks time_record_was_enqueued,
      const reporting::Priority priority,
      reporting::Record record,
      base::OnceCallback<void(reporting::Status)> completion_callback,
      reporting::Status status) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);

    const base::TimeDelta time_elapased_since_record_was_originally_enqueued =
        base::TimeTicks::Now() - time_record_was_enqueued;

    const base::TimeDelta retry_window =
        base::Seconds(kNumSecondsToRetry.Get());

    // If missive is unavailable and we're within the retry window, retry
    // enqueueuing the record.
    if (time_elapased_since_record_was_originally_enqueued < retry_window &&
        status.error_code() == reporting::error::UNAVAILABLE) {
      EnqueueRecordInternal(/*is_retry=*/true, time_record_was_enqueued,
                            priority, std::move(record),
                            std::move(completion_callback));
      return;
    }

    if (is_retry) {
      base::UmaHistogramTimes(
          kUmaTimeSpentRetryingEnqueueRecord,
          time_elapased_since_record_was_originally_enqueued);
      base::UmaHistogramEnumeration(kUmaRetryEnqueueRecordStatus, status.code(),
                                    reporting::error::Code::MAX_VALUE);
    }
    std::move(completion_callback).Run(status);
  }

  void EnqueueRecordInternal(
      bool is_retry,
      base::TimeTicks time_record_was_enqueued,
      const reporting::Priority priority,
      reporting::Record record,
      base::OnceCallback<void(reporting::Status)> completion_callback) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);

    base::OnceCallback<void(reporting::Status)> maybe_retry_enqueue_cb;

    if (base::FeatureList::IsEnabled(kEnableRetryEnqueueRecord)) {
      // Make a copy of the record for the retry callback.
      reporting::Record record_copy(record);

      maybe_retry_enqueue_cb = base::BindPostTask(
          origin_task_runner_,
          base::BindOnce(
              &MissiveClientImpl::MaybeRetryEnqueue,
              weak_ptr_factory_.GetWeakPtr(), is_retry,
              time_record_was_enqueued, priority, std::move(record_copy),
              reporting::Scoped<reporting::Status>(
                  std::move(completion_callback),
                  reporting::Status(reporting::error::UNAVAILABLE,
                                    "Missive client destructed before "
                                    "record was enqueued"))));
    } else {
      maybe_retry_enqueue_cb = std::move(completion_callback);
    }

    auto delegate = std::make_unique<EnqueueRecordDelegate>(
        priority, std::move(record), this, std::move(maybe_retry_enqueue_cb));

    client_.MaybeMakeCall(std::move(delegate));
  }

  void EnqueueRecord(const reporting::Priority priority,
                     reporting::Record record,
                     base::OnceCallback<void(reporting::Status)>
                         completion_callback) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);
    if (!is_initialized_) {
      std::move(completion_callback)
          .Run(reporting::Status(reporting::error::FAILED_PRECONDITION,
                                 "Reporting not started yet"));
      return;
    }
    if (!has_valid_api_key()) {
      std::move(completion_callback)
          .Run(reporting::Status(reporting::error::FAILED_PRECONDITION,
                                 "Cannot report with unsupported API Key"));
      return;
    }

    EnqueueRecordInternal(
        /*is_retry=*/false, /*time_record_was_enqueued=*/base::TimeTicks::Now(),
        priority, std::move(record), std::move(completion_callback));
  }

  void Flush(const reporting::Priority priority,
             base::OnceCallback<void(reporting::Status)> completion_callback)
      override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);
    if (!is_initialized_) {
      std::move(completion_callback)
          .Run(reporting::Status(reporting::error::FAILED_PRECONDITION,
                                 "Reporting not started yet"));
      return;
    }
    if (!has_valid_api_key()) {
      std::move(completion_callback)
          .Run(reporting::Status(reporting::error::FAILED_PRECONDITION,
                                 "Cannot report with unsupported API Key"));
      return;
    }
    auto delegate = std::make_unique<FlushDelegate>(
        priority, this, std::move(completion_callback));
    client_.MaybeMakeCall(std::move(delegate));
  }

  void UpdateConfigInMissive(
      const reporting::ListOfBlockedDestinations& destinations) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);
    if (!is_initialized_ || !has_valid_api_key()) {
      return;
    }
    auto delegate =
        std::make_unique<UpdateConfigInMissiveDelegate>(destinations, this);
    client_.MaybeMakeCall(std::move(delegate));
  }

  void UpdateEncryptionKey(
      const reporting::SignedEncryptionInfo& encryption_info) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);
    if (!is_initialized_ || !has_valid_api_key()) {
      return;
    }
    auto delegate =
        std::make_unique<UpdateEncryptionKeyDelegate>(encryption_info, this);
    client_.MaybeMakeCall(std::move(delegate));
  }

  void ReportSuccess(const reporting::SequenceInformation& sequence_information,
                     bool force_confirm) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);
    if (!is_initialized_ || !has_valid_api_key()) {
      return;
    }
    auto delegate = std::make_unique<ReportSuccessDelegate>(
        sequence_information, force_confirm, this);
    client_.MaybeMakeCall(std::move(delegate));
  }

  MissiveClient::TestInterface* GetTestInterface() override { return nullptr; }

  base::WeakPtr<MissiveClient> GetWeakPtr() override {
    return weak_ptr_factory_.GetWeakPtr();
  }

 private:
  // Class implements DisconnectableClient::Delegate specifically for dBus
  // calls. Logic that handles dBus connect/disconnect cases remains with the
  // base class.
  class DBusDelegate : public reporting::DisconnectableClient::Delegate {
   public:
    // If this enum is changed, be sure to update
    // `EnterpriseReportingMissiveClientDbusError` in
    // tools/metrics/histograms/metadata/browser/enums.xml
    enum DbusErrorType : int32_t {
      OK = 0,
      SERVICE_UNAVAILABLE = 1,
      NO_RESPONSE = 2,
      UNKNOWN = 3,
      MAX_VALUE = UNKNOWN,
    };
    DBusDelegate(const DBusDelegate& other) = delete;
    DBusDelegate& operator=(const DBusDelegate& other) = delete;
    ~DBusDelegate() override = default;

    // Writes request into dBus message writer.
    virtual bool WriteRequest(dbus::MessageWriter* writer) = 0;

    // Parses response, retrieves status information from it and returns it.
    // Optional - returns OK if absent.
    virtual reporting::Status ParseResponse(dbus::MessageReader* reader) {
      return reporting::Status::StatusOK();
    }

   protected:
    DBusDelegate(
        const char* dbus_method,
        MissiveClientImpl* owner,
        base::OnceCallback<void(reporting::Status)> completion_callback)
        : dbus_method_(dbus_method),
          owner_(owner),
          completion_callback_(std::move(completion_callback)) {}

   private:
    // Implementation of DisconnectableClient::Delegate.
    void DoCall(base::OnceClosure cb) final {
      DCHECK_CALLED_ON_VALID_SEQUENCE(owner_->origin_checker_);
      base::ScopedClosureRunner autorun(std::move(cb));
      dbus::MethodCall method_call(missive::kMissiveServiceInterface,
                                   dbus_method_);
      dbus::MessageWriter writer(&method_call);
      if (!WriteRequest(&writer)) {
        reporting::Status status(
            reporting::error::UNKNOWN,
            "MessageWriter was unable to append the request.");
        LOG(ERROR) << status;
        std::move(completion_callback_).Run(status);
        return;
      }

      // Make a dBus call.
      owner_->missive_service_proxy_->CallMethod(
          &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
          base::BindOnce(
              [](base::ScopedClosureRunner autorun,
                 base::WeakPtr<DBusDelegate> self, dbus::Response* response) {
                if (!self) {
                  return;  // Delegate already deleted.
                }
                DCHECK_CALLED_ON_VALID_SEQUENCE(self->owner_->origin_checker_);
                if (!response) {
                  self->Respond(Status(reporting::error::UNAVAILABLE,
                                       kErrorNoDbusResponse));
                  return;
                }
                self->response_ = response;
              },
              std::move(autorun), weak_ptr_factory_.GetWeakPtr()));
    }

    // Process dBus response, if status is OK, or error otherwise.
    void Respond(reporting::Status status) final {
      DCHECK_CALLED_ON_VALID_SEQUENCE(owner_->origin_checker_);
      if (!completion_callback_) {
        return;
      }
      if (status.ok()) {
        dbus::MessageReader reader(response_);
        status = ParseResponse(&reader);
        base::UmaHistogramEnumeration(kUmaMissiveClientDbusError,
                                      DbusErrorType::OK,
                                      DbusErrorType::MAX_VALUE);
      } else if (status.error_message() ==
                 reporting::disconnectable_client::kErrorServiceUnavailable) {
        base::UmaHistogramEnumeration(kUmaMissiveClientDbusError,
                                      DbusErrorType::SERVICE_UNAVAILABLE,
                                      DbusErrorType::MAX_VALUE);
      } else if (status.error_message() == kErrorNoDbusResponse) {
        base::UmaHistogramEnumeration(kUmaMissiveClientDbusError,
                                      DbusErrorType::NO_RESPONSE,
                                      DbusErrorType::MAX_VALUE);
      } else {
        base::UmaHistogramEnumeration(kUmaMissiveClientDbusError,
                                      DbusErrorType::UNKNOWN,
                                      DbusErrorType::MAX_VALUE);
      }
      std::move(completion_callback_).Run(status);
    }

    const char* const dbus_method_;
    raw_ptr<dbus::Response> response_;
    const raw_ptr<MissiveClientImpl> owner_;
    base::OnceCallback<void(reporting::Status)> completion_callback_;

    // Weak pointer factory - must be last member of the class.
    base::WeakPtrFactory<DBusDelegate> weak_ptr_factory_{this};
  };

  class EnqueueRecordDelegate : public DBusDelegate {
   public:
    EnqueueRecordDelegate(
        reporting::Priority priority,
        reporting::Record record,
        MissiveClientImpl* owner,
        base::OnceCallback<void(reporting::Status)> completion_callback)
        : DBusDelegate(missive::kEnqueueRecord,
                       owner,
                       std::move(completion_callback)) {
      *request_.mutable_record() = std::move(record);
      request_.set_priority(priority);

#if BUILDFLAG(IS_CHROMEOS_ASH)
      // Turn on/off the debug state flag (for Ash only).
      request_.set_health_data_logging_enabled(
          ::reporting::HistoryTracker::Get()->debug_state());
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
    }

    bool WriteRequest(dbus::MessageWriter* writer) override {
      return writer->AppendProtoAsArrayOfBytes(request_);
    }

    reporting::Status ParseResponse(dbus::MessageReader* reader) override {
      reporting::EnqueueRecordResponse response_body;
      if (!reader->PopArrayOfBytesAsProto(&response_body)) {
        return reporting::Status(reporting::error::INTERNAL,
                                 "Response was not parsable.");
      }

#if BUILDFLAG(IS_CHROMEOS_ASH)
      // Accept health data if present (ChromeOS only)
      if (response_body.has_health_data()) {
        ::reporting::HistoryTracker::Get()->set_data(
            std::move(response_body.health_data()), base::DoNothing());
      }
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

      reporting::Status status;
      status.RestoreFrom(response_body.status());
      return status;
    }

   private:
    reporting::EnqueueRecordRequest request_;
  };

  class FlushDelegate : public DBusDelegate {
   public:
    FlushDelegate(
        reporting::Priority priority,
        MissiveClientImpl* owner,
        base::OnceCallback<void(reporting::Status)> completion_callback)
        : DBusDelegate(missive::kFlushPriority,
                       owner,
                       std::move(completion_callback)) {
      request_.set_priority(priority);

#if BUILDFLAG(IS_CHROMEOS_ASH)
      // Turn on/off the debug state flag (for Ash only).
      request_.set_health_data_logging_enabled(
          ::reporting::HistoryTracker::Get()->debug_state());
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
    }

    bool WriteRequest(dbus::MessageWriter* writer) override {
      return writer->AppendProtoAsArrayOfBytes(request_);
    }

    reporting::Status ParseResponse(dbus::MessageReader* reader) override {
      reporting::FlushPriorityResponse response_body;
      if (!reader->PopArrayOfBytesAsProto(&response_body)) {
        return reporting::Status(reporting::error::INTERNAL,
                                 "Response was not parsable.");
      }

#if BUILDFLAG(IS_CHROMEOS_ASH)
      // Accept health data if present (ChromeOS only)
      if (response_body.has_health_data()) {
        ::reporting::HistoryTracker::Get()->set_data(
            std::move(response_body.health_data()), base::DoNothing());
      }
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

      reporting::Status status;
      status.RestoreFrom(response_body.status());
      return status;
    }

   private:
    reporting::FlushPriorityRequest request_;
  };

  class UpdateConfigInMissiveDelegate : public DBusDelegate {
   public:
    UpdateConfigInMissiveDelegate(
        const reporting::ListOfBlockedDestinations& destinations,
        MissiveClientImpl* owner)
        : DBusDelegate(missive::kUpdateConfigInMissive,
                       owner,
                       base::DoNothing()) {
      *request_.mutable_list_of_blocked_destinations() = destinations;
    }

    bool WriteRequest(dbus::MessageWriter* writer) override {
      return writer->AppendProtoAsArrayOfBytes(request_);
    }

   private:
    reporting::UpdateConfigInMissiveRequest request_;
  };

  class UpdateEncryptionKeyDelegate : public DBusDelegate {
   public:
    UpdateEncryptionKeyDelegate(
        const reporting::SignedEncryptionInfo& encryption_info,
        MissiveClientImpl* owner)
        : DBusDelegate(missive::kUpdateEncryptionKey,
                       owner,
                       base::DoNothing()) {
      *request_.mutable_signed_encryption_info() = encryption_info;
    }

    bool WriteRequest(dbus::MessageWriter* writer) override {
      return writer->AppendProtoAsArrayOfBytes(request_);
    }

   private:
    reporting::UpdateEncryptionKeyRequest request_;
  };

  class ReportSuccessDelegate : public DBusDelegate {
   public:
    ReportSuccessDelegate(
        const reporting::SequenceInformation& sequence_information,
        bool force_confirm,
        MissiveClientImpl* owner)
        : DBusDelegate(missive::kConfirmRecordUpload,
                       owner,
                       base::DoNothing()) {
      *request_.mutable_sequence_information() = sequence_information;
      request_.set_force_confirm(force_confirm);

#if BUILDFLAG(IS_CHROMEOS_ASH)
      // Turn on/off the debug state flag (for Ash only).
      request_.set_health_data_logging_enabled(
          ::reporting::HistoryTracker::Get()->debug_state());
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
    }

    bool WriteRequest(dbus::MessageWriter* writer) override {
      return writer->AppendProtoAsArrayOfBytes(request_);
    }

    reporting::Status ParseResponse(dbus::MessageReader* reader) override {
      reporting::ConfirmRecordUploadResponse response_body;
      if (!reader->PopArrayOfBytesAsProto(&response_body)) {
        return reporting::Status(reporting::error::INTERNAL,
                                 "Response was not parsable.");
      }

#if BUILDFLAG(IS_CHROMEOS_ASH)
      // Accept health data if present (ChromeOS only)
      if (response_body.has_health_data()) {
        ::reporting::HistoryTracker::Get()->set_data(
            std::move(response_body.health_data()), base::DoNothing());
      }
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

      reporting::Status status;
      status.RestoreFrom(response_body.status());
      return status;
    }

   private:
    reporting::ConfirmRecordUploadRequest request_;
  };

  void OwnerChanged(const std::string& old_owner,
                    const std::string& new_owner) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);
    ServiceAvailable(/*service_is_available=*/!new_owner.empty());
  }

  void ServiceAvailable(bool service_is_available) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(origin_checker_);
    client_.SetAvailability(/*is_available=*/service_is_available);
  }

  scoped_refptr<dbus::ObjectProxy> missive_service_proxy_;

  reporting::DisconnectableClient client_;

  // Weak pointer factory - must be last member of the class.
  base::WeakPtrFactory<MissiveClientImpl> weak_ptr_factory_{this};
};

}  // namespace

MissiveClient::MissiveClient() {
  DCHECK(!g_instance);
  g_instance = this;
}

MissiveClient::~MissiveClient() {
  DCHECK_EQ(this, g_instance);
  g_instance = nullptr;
}

bool MissiveClient::has_valid_api_key() const {
  return has_valid_api_key_;
}

scoped_refptr<base::SequencedTaskRunner> MissiveClient::origin_task_runner()
    const {
  return origin_task_runner_;
}

// static
void MissiveClient::Initialize(dbus::Bus* bus) {
  DCHECK(bus);
  (new MissiveClientImpl())->Init(bus);
}

// static
void MissiveClient::InitializeFake() {
  (new FakeMissiveClient())->Init();
}

// static
void MissiveClient::Shutdown() {
  DCHECK(g_instance);
  delete g_instance;
}

// static
MissiveClient* MissiveClient::Get() {
  return g_instance;
}

}  // namespace chromeos