chromium/chromeos/ash/components/dbus/shill/shill_client_helper.cc

// Copyright 2012 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/ash/components/dbus/shill/shill_client_helper.h"

#include <stddef.h>

#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "components/device_event_log/device_event_log.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
#include "dbus/values_util.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {

// Class to hold onto a reference to a ShillClientHelper. This class
// is owned by callbacks and released once the callback completes.
// Note: Only success callbacks hold the reference. If an error callback is
// invoked instead, the success callback will still be destroyed and the
// RefHolder with it, once the callback chain completes.
class ShillClientHelper::RefHolder {
 public:
  explicit RefHolder(base::WeakPtr<ShillClientHelper> helper)
      : helper_(helper),
        origin_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {
    helper_->AddRef();
  }
  ~RefHolder() {
    // Release the helper on the origin thread.
    base::OnceClosure closure =
        base::BindOnce(&ShillClientHelper::Release, helper_);
    if (origin_task_runner_->BelongsToCurrentThread()) {
      std::move(closure).Run();
    } else {
      origin_task_runner_->PostTask(FROM_HERE, std::move(closure));
    }
  }

 private:
  base::WeakPtr<ShillClientHelper> helper_;
  scoped_refptr<base::SingleThreadTaskRunner> origin_task_runner_;
};

namespace {

const char kInvalidResponseErrorName[] = "";  // No error name.
const char kInvalidResponseErrorMessage[] = "Invalid response.";

// Note: here and below, |ref_holder| is unused in the function body. It only
// exists so that it will be destroyed (and the reference released) with the
// Callback object once completed.
void OnBooleanMethodWithErrorCallback(
    ShillClientHelper::RefHolder* ref_holder,
    ShillClientHelper::BooleanCallback callback,
    ShillClientHelper::ErrorCallback error_callback,
    dbus::Response* response) {
  if (!response) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  dbus::MessageReader reader(response);
  bool result;
  if (!reader.PopBool(&result)) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  std::move(callback).Run(result);
}

void OnStringMethodWithErrorCallback(
    ShillClientHelper::RefHolder* ref_holder,
    ShillClientHelper::StringCallback callback,
    ShillClientHelper::ErrorCallback error_callback,
    dbus::Response* response) {
  if (!response) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  dbus::MessageReader reader(response);
  std::string result;
  if (!reader.PopString(&result)) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  std::move(callback).Run(result);
}

// Handles responses for methods without results.
void OnVoidMethod(ShillClientHelper::RefHolder* ref_holder,
                  chromeos::VoidDBusMethodCallback callback,
                  dbus::Response* response) {
  std::move(callback).Run(response != nullptr);
}

// Handles responses for methods with ObjectPath results and no status.
void OnObjectPathMethodWithoutStatus(
    ShillClientHelper::RefHolder* ref_holder,
    chromeos::ObjectPathCallback callback,
    ShillClientHelper::ErrorCallback error_callback,
    dbus::Response* response) {
  if (!response) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  dbus::MessageReader reader(response);
  dbus::ObjectPath result;
  if (!reader.PopObjectPath(&result)) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  std::move(callback).Run(result);
}

// Handles responses for methods with base::Value results.
void OnValueMethod(ShillClientHelper::RefHolder* ref_holder,
                   chromeos::DBusMethodCallback<base::Value> callback,
                   dbus::Response* response,
                   dbus::ErrorResponse* error_response) {
  if (!response) {
    if (error_response) {
      dbus::MessageReader reader(error_response);
      std::string error_message;
      reader.PopString(&error_message);
      NET_LOG(ERROR) << "DBus call failed. Error: "
                     << error_response->GetErrorName()
                     << " Message: " << error_message;
    } else {
      NET_LOG(ERROR) << "DBus call failed with no error.";
    }
    std::move(callback).Run(std::nullopt);
    return;
  }
  dbus::MessageReader reader(response);
  base::Value value(dbus::PopDataAsValue(&reader));
  if (value.is_none()) {
    std::move(callback).Run(std::nullopt);
    return;
  }
  std::move(callback).Run(std::move(value));
}

// Handles responses for methods with base::Value::Dict results.
void OnDictValueMethod(ShillClientHelper::RefHolder* ref_holder,
                       chromeos::DBusMethodCallback<base::Value::Dict> callback,
                       dbus::Response* response,
                       dbus::ErrorResponse* error_response) {
  if (!response) {
    if (error_response) {
      dbus::MessageReader reader(error_response);
      std::string error_message;
      reader.PopString(&error_message);
      NET_LOG(ERROR) << "DBus call failed. Error: "
                     << error_response->GetErrorName()
                     << " Message: " << error_message;
    } else {
      NET_LOG(ERROR) << "DBus call failed with no error.";
    }
    std::move(callback).Run(std::nullopt);
    return;
  }
  dbus::MessageReader reader(response);
  base::Value value(dbus::PopDataAsValue(&reader));
  if (!value.is_dict()) {
    std::move(callback).Run(std::nullopt);
    return;
  }
  std::move(callback).Run(std::move(value.GetDict()));
}

// Handles responses for methods without results.
void OnVoidMethodWithErrorCallback(ShillClientHelper::RefHolder* ref_holder,
                                   base::OnceClosure callback,
                                   dbus::Response* response) {
  std::move(callback).Run();
}

// Handles responses for methods with base::Value::Dict results.
// Used by CallDictValueMethodWithErrorCallback().
void OnDictValueMethodWithErrorCallback(
    ShillClientHelper::RefHolder* ref_holder,
    base::OnceCallback<void(base::Value::Dict result)> callback,
    ShillClientHelper::ErrorCallback error_callback,
    dbus::Response* response) {
  dbus::MessageReader reader(response);
  base::Value value(dbus::PopDataAsValue(&reader));
  if (!value.is_dict()) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  std::move(callback).Run(std::move(value).TakeDict());
}

// Handles responses for methods with ListValue results.
void OnListValueMethodWithErrorCallback(
    ShillClientHelper::RefHolder* ref_holder,
    ShillClientHelper::ListValueCallback callback,
    ShillClientHelper::ErrorCallback error_callback,
    dbus::Response* response) {
  dbus::MessageReader reader(response);
  base::Value value(dbus::PopDataAsValue(&reader));
  if (!value.is_list()) {
    std::move(error_callback)
        .Run(kInvalidResponseErrorName, kInvalidResponseErrorMessage);
    return;
  }
  std::move(callback).Run(value.GetList());
}

// Handles running appropriate error callbacks.
void OnError(ShillClientHelper::ErrorCallback error_callback,
             dbus::ErrorResponse* response) {
  std::string error_name;
  std::string error_message;
  if (response) {
    // Error message may contain the error message as string.
    dbus::MessageReader reader(response);
    error_name = response->GetErrorName();
    reader.PopString(&error_message);
  }
  std::move(error_callback).Run(error_name, error_message);
}

}  // namespace

ShillClientHelper::ShillClientHelper(dbus::ObjectProxy* proxy)
    : proxy_(proxy), active_refs_(0) {}

ShillClientHelper::~ShillClientHelper() {
  if (!observer_list_.empty())
    NET_LOG(ERROR) << "ShillClientHelper destroyed with active observers";
}

void ShillClientHelper::SetReleasedCallback(ReleasedCallback callback) {
  CHECK(released_callback_.is_null());
  released_callback_ = std::move(callback);
}

void ShillClientHelper::AddPropertyChangedObserver(
    ShillPropertyChangedObserver* observer) {
  if (observer_list_.HasObserver(observer))
    return;
  AddRef();
  // Execute all the pending MonitorPropertyChanged calls.
  for (const auto& interface : interfaces_to_be_monitored_) {
    MonitorPropertyChangedInternal(interface);
  }
  interfaces_to_be_monitored_.clear();

  observer_list_.AddObserver(observer);
}

void ShillClientHelper::RemovePropertyChangedObserver(
    ShillPropertyChangedObserver* observer) {
  if (!observer_list_.HasObserver(observer))
    return;
  observer_list_.RemoveObserver(observer);
  Release();
}

void ShillClientHelper::MonitorPropertyChanged(
    const std::string& interface_name) {
  if (!observer_list_.empty()) {
    // Effectively monitor the PropertyChanged now.
    MonitorPropertyChangedInternal(interface_name);
  } else {
    // Delay the ConnectToSignal until an observer is added.
    interfaces_to_be_monitored_.push_back(interface_name);
  }
}

void ShillClientHelper::MonitorPropertyChangedInternal(
    const std::string& interface_name) {
  // We are not using dbus::PropertySet to monitor PropertyChanged signal
  // because the interface is not "org.freedesktop.DBus.Properties".
  proxy_->ConnectToSignal(
      interface_name, shill::kMonitorPropertyChanged,
      base::BindRepeating(&ShillClientHelper::OnPropertyChanged,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&ShillClientHelper::OnSignalConnected,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ShillClientHelper::CallVoidMethod(
    dbus::MethodCall* method_call,
    chromeos::VoidDBusMethodCallback callback) {
  DCHECK(!callback.is_null());
  proxy_->CallMethod(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnVoidMethod,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback)));
}

void ShillClientHelper::CallObjectPathMethodWithErrorCallback(
    dbus::MethodCall* method_call,
    chromeos::ObjectPathCallback callback,
    ErrorCallback error_callback) {
  DCHECK(!callback.is_null());
  DCHECK(!error_callback.is_null());
  auto split_callback = base::SplitOnceCallback(std::move(error_callback));
  proxy_->CallMethodWithErrorCallback(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnObjectPathMethodWithoutStatus,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback), std::move(split_callback.first)),
      base::BindOnce(&OnError, std::move(split_callback.second)));
}

void ShillClientHelper::CallValueMethod(
    dbus::MethodCall* method_call,
    chromeos::DBusMethodCallback<base::Value> callback) {
  DCHECK(!callback.is_null());
  proxy_->CallMethodWithErrorResponse(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnValueMethod,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback)));
}

void ShillClientHelper::CallDictValueMethod(
    dbus::MethodCall* method_call,
    chromeos::DBusMethodCallback<base::Value::Dict> callback) {
  DCHECK(!callback.is_null());
  proxy_->CallMethodWithErrorResponse(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnDictValueMethod,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback)));
}

void ShillClientHelper::CallVoidMethodWithErrorCallback(
    dbus::MethodCall* method_call,
    base::OnceClosure callback,
    ErrorCallback error_callback) {
  DCHECK(!callback.is_null());
  DCHECK(!error_callback.is_null());
  proxy_->CallMethodWithErrorCallback(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnVoidMethodWithErrorCallback,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback)),
      base::BindOnce(&OnError, std::move(error_callback)));
}

void ShillClientHelper::CallBooleanMethodWithErrorCallback(
    dbus::MethodCall* method_call,
    BooleanCallback callback,
    ErrorCallback error_callback) {
  DCHECK(!callback.is_null());
  DCHECK(!error_callback.is_null());
  auto split_callback = base::SplitOnceCallback(std::move(error_callback));
  proxy_->CallMethodWithErrorCallback(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnBooleanMethodWithErrorCallback,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback), std::move(split_callback.first)),
      base::BindOnce(&OnError, std::move(split_callback.second)));
}

void ShillClientHelper::CallStringMethodWithErrorCallback(
    dbus::MethodCall* method_call,
    StringCallback callback,
    ErrorCallback error_callback) {
  DCHECK(!callback.is_null());
  DCHECK(!error_callback.is_null());
  auto split_callback = base::SplitOnceCallback(std::move(error_callback));
  proxy_->CallMethodWithErrorCallback(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnStringMethodWithErrorCallback,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback), std::move(split_callback.first)),
      base::BindOnce(&OnError, std::move(split_callback.second)));
}

void ShillClientHelper::CallDictValueMethodWithErrorCallback(
    dbus::MethodCall* method_call,
    base::OnceCallback<void(base::Value::Dict result)> callback,
    ErrorCallback error_callback) {
  DCHECK(!callback.is_null());
  DCHECK(!error_callback.is_null());
  auto split_callback = base::SplitOnceCallback(std::move(error_callback));
  proxy_->CallMethodWithErrorCallback(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnDictValueMethodWithErrorCallback,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback), std::move(split_callback.first)),
      base::BindOnce(&OnError, std::move(split_callback.second)));
}

void ShillClientHelper::CallListValueMethodWithErrorCallback(
    dbus::MethodCall* method_call,
    ListValueCallback callback,
    ErrorCallback error_callback) {
  DCHECK(!callback.is_null());
  DCHECK(!error_callback.is_null());
  auto split_callback = base::SplitOnceCallback(std::move(error_callback));
  proxy_->CallMethodWithErrorCallback(
      method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&OnListValueMethodWithErrorCallback,
                     base::Owned(new RefHolder(weak_ptr_factory_.GetWeakPtr())),
                     std::move(callback), std::move(split_callback.first)),
      base::BindOnce(&OnError, std::move(split_callback.second)));
}

namespace {

enum DictionaryType { DICTIONARY_TYPE_VARIANT, DICTIONARY_TYPE_STRING };

// Appends an a{ss} dictionary to |writer|. |dictionary| must only contain
// strings.
void AppendStringDictionary(const base::Value::Dict& dictionary,
                            dbus::MessageWriter* writer) {
  dbus::MessageWriter array_writer(nullptr);
  writer->OpenArray("{ss}", &array_writer);
  for (const auto it : dictionary) {
    dbus::MessageWriter entry_writer(nullptr);
    array_writer.OpenDictEntry(&entry_writer);
    entry_writer.AppendString(it.first);
    const base::Value& value = it.second;
    std::string value_string;
    if (value.is_string()) {
      value_string = value.GetString();
    } else {
      NET_LOG(ERROR) << "Dictionary value not a string: " << it.first;
    }
    entry_writer.AppendString(value_string);
    array_writer.CloseContainer(&entry_writer);
  }
  writer->CloseContainer(&array_writer);
}

void AppendValueDataAsVariantInternal(dbus::MessageWriter* writer,
                                      const std::string& name,
                                      const base::Value& value,
                                      DictionaryType dictionary_type) {
  // Support basic types and string-to-string dictionary.
  switch (value.type()) {
    case base::Value::Type::DICT: {
      if (dictionary_type == DICTIONARY_TYPE_STRING) {
        // AppendStringDictionary uses a{ss} to support Cellular.APN which
        // expects a string -> string dictionary.
        dbus::MessageWriter variant_writer(nullptr);
        writer->OpenVariant("a{ss}", &variant_writer);
        AppendStringDictionary(value.GetDict(), &variant_writer);
        writer->CloseContainer(&variant_writer);
      } else {
        dbus::MessageWriter variant_writer(nullptr);
        writer->OpenVariant("a{sv}", &variant_writer);
        ShillClientHelper::AppendServiceProperties(&variant_writer,
                                                   value.GetDict());
        writer->CloseContainer(&variant_writer);
      }
      break;
    }
    case base::Value::Type::LIST: {
      // Support list of string and list of string-to-string dictionary.
      // For properties that might receive an empty list of dictionaries, always
      // use aa{ss}.
      const auto& list_view = value.GetList();
      if ((list_view.size() > 0 && list_view.front().is_dict()) ||
          name == shill::kCellularCustomApnListProperty) {
        // aa{ss} to support WireGuard.Peers
        dbus::MessageWriter variant_writer(nullptr);
        writer->OpenVariant("aa{ss}", &variant_writer);
        dbus::MessageWriter array_writer(nullptr);
        variant_writer.OpenArray("a{ss}", &array_writer);
        for (const auto& item : list_view) {
          AppendStringDictionary(item.GetDict(), &array_writer);
        }
        variant_writer.CloseContainer(&array_writer);
        writer->CloseContainer(&variant_writer);
        break;
      }
      dbus::MessageWriter variant_writer(nullptr);
      writer->OpenVariant("as", &variant_writer);
      dbus::MessageWriter array_writer(nullptr);
      variant_writer.OpenArray("s", &array_writer);
      for (const auto& inner_value : list_view) {
        std::string value_string;
        if (inner_value.is_string()) {
          value_string = inner_value.GetString();
        } else {
          NET_LOG(ERROR) << "List value not a string: " << value;
        }
        array_writer.AppendString(value_string);
      }
      variant_writer.CloseContainer(&array_writer);
      writer->CloseContainer(&variant_writer);
      break;
    }
    case base::Value::Type::BOOLEAN:
    case base::Value::Type::INTEGER:
    case base::Value::Type::DOUBLE:
    case base::Value::Type::STRING:
      dbus::AppendBasicTypeValueDataAsVariant(writer, value);
      break;
    default:
      NET_LOG(ERROR) << "Unexpected value type: " << value.type();
  }
}

}  // namespace

// static
void ShillClientHelper::AppendValueDataAsVariant(dbus::MessageWriter* writer,
                                                 const std::string& name,
                                                 const base::Value& value) {
  AppendValueDataAsVariantInternal(writer, name, value,
                                   DICTIONARY_TYPE_VARIANT);
}

// static
void ShillClientHelper::AppendServiceProperties(
    dbus::MessageWriter* writer,
    const base::Value::Dict& dictionary) {
  dbus::MessageWriter array_writer(nullptr);
  writer->OpenArray("{sv}", &array_writer);
  for (auto it : dictionary) {
    dbus::MessageWriter entry_writer(nullptr);
    array_writer.OpenDictEntry(&entry_writer);
    entry_writer.AppendString(it.first);
    // Shill expects Cellular.APN to be a string dictionary, a{ss}. All other
    // properties use a variant dictionary, a{sv}. TODO(stevenjb): Remove this
    // hack if/when we change Shill to accept a{sv} for Cellular.APN.
    DictionaryType dictionary_type = (it.first == shill::kCellularApnProperty)
                                         ? DICTIONARY_TYPE_STRING
                                         : DICTIONARY_TYPE_VARIANT;
    AppendValueDataAsVariantInternal(&entry_writer, it.first, it.second,
                                     dictionary_type);
    array_writer.CloseContainer(&entry_writer);
  }
  writer->CloseContainer(&array_writer);
}

void ShillClientHelper::AddRef() {
  ++active_refs_;
}

void ShillClientHelper::Release() {
  --active_refs_;
  if (active_refs_ == 0 && !released_callback_.is_null())
    std::move(released_callback_).Run(this);  // May delete this
}

void ShillClientHelper::OnSignalConnected(const std::string& interface,
                                          const std::string& signal,
                                          bool success) {
  if (!success)
    NET_LOG(ERROR) << "Connect to " << interface << " " << signal << " failed.";
}

void ShillClientHelper::OnPropertyChanged(dbus::Signal* signal) {
  if (observer_list_.empty())
    return;

  dbus::MessageReader reader(signal);
  std::string name;
  if (!reader.PopString(&name))
    return;
  base::Value value(dbus::PopDataAsValue(&reader));
  if (value.is_none())
    return;

  for (auto& observer : observer_list_)
    observer.OnPropertyChanged(name, value);
}

}  // namespace ash