buffet: Move privetd sources into buffet No functional changes, only renaming, fixed include paths and include guards to avoid resubmit warnings. BUG=brillo:1161 CQ-DEPEND=CL:276521 TEST=none Change-Id: Icacff92aef47fdd46542bc96eba3ffbb4df6241a Reviewed-on: https://chromium-review.googlesource.com/276319 Reviewed-by: Vitaly Buka <vitalybuka@chromium.org> Commit-Queue: Vitaly Buka <vitalybuka@chromium.org> Tested-by: Vitaly Buka <vitalybuka@chromium.org>
diff --git a/buffet/privet/ap_manager_client.cc b/buffet/privet/ap_manager_client.cc new file mode 100644 index 0000000..b123f1f --- /dev/null +++ b/buffet/privet/ap_manager_client.cc
@@ -0,0 +1,115 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/ap_manager_client.h" + +namespace privetd { + +using org::chromium::apmanager::ConfigProxy; +using org::chromium::apmanager::ManagerProxy; +using org::chromium::apmanager::ServiceProxy; + +ApManagerClient::ApManagerClient(const scoped_refptr<dbus::Bus>& bus) + : bus_(bus) { +} + +ApManagerClient::~ApManagerClient() { + Stop(); +} + +void ApManagerClient::Start(const std::string& ssid) { + if (service_path_.IsValid()) { + return; + } + + ssid_ = ssid; + + object_manager_proxy_.reset( + new org::chromium::apmanager::ObjectManagerProxy{bus_}); + object_manager_proxy_->SetManagerAddedCallback(base::Bind( + &ApManagerClient::OnManagerAdded, weak_ptr_factory_.GetWeakPtr())); + object_manager_proxy_->SetServiceAddedCallback(base::Bind( + &ApManagerClient::OnServiceAdded, weak_ptr_factory_.GetWeakPtr())); + + object_manager_proxy_->SetServiceRemovedCallback(base::Bind( + &ApManagerClient::OnServiceRemoved, weak_ptr_factory_.GetWeakPtr())); + object_manager_proxy_->SetManagerRemovedCallback(base::Bind( + &ApManagerClient::OnManagerRemoved, weak_ptr_factory_.GetWeakPtr())); +} + +void ApManagerClient::Stop() { + if (manager_proxy_ && service_path_.IsValid()) { + RemoveService(service_path_); + } + service_path_ = dbus::ObjectPath(); + service_proxy_ = nullptr; + manager_proxy_ = nullptr; + object_manager_proxy_.reset(); + ssid_.clear(); +} + +void ApManagerClient::RemoveService(const dbus::ObjectPath& object_path) { + CHECK(object_path.IsValid()); + chromeos::ErrorPtr error; + if (!manager_proxy_->RemoveService(object_path, &error)) { + LOG(ERROR) << "RemoveService failed: " << error->GetMessage(); + } +} + +void ApManagerClient::OnManagerAdded(ManagerProxy* manager_proxy) { + VLOG(1) << "manager added: " << manager_proxy->GetObjectPath().value(); + manager_proxy_ = manager_proxy; + + if (service_path_.IsValid()) + return; + + chromeos::ErrorPtr error; + if (!manager_proxy_->CreateService(&service_path_, &error)) { + LOG(ERROR) << "CreateService failed: " << error->GetMessage(); + } +} + +void ApManagerClient::OnServiceAdded(ServiceProxy* service_proxy) { + VLOG(1) << "service added: " << service_proxy->GetObjectPath().value(); + if (service_proxy->GetObjectPath() != service_path_) { + RemoveService(service_proxy->GetObjectPath()); + return; + } + service_proxy_ = service_proxy; + + ConfigProxy* config_proxy = + object_manager_proxy_->GetConfigProxy(service_proxy->config()); + ConfigProxy::PropertySet* properties = config_proxy->GetProperties(); + properties->ssid.Set(ssid_, base::Bind(&ApManagerClient::OnSsidSet, + weak_ptr_factory_.GetWeakPtr())); +} + +void ApManagerClient::OnSsidSet(bool success) { + if (!success || !service_proxy_) { + LOG(ERROR) << "Failed to set ssid."; + return; + } + VLOG(1) << "SSID is set: " << ssid_; + + chromeos::ErrorPtr error; + if (!service_proxy_->Start(&error)) { + LOG(ERROR) << "Service start failed: " << error->GetMessage(); + } +} + +void ApManagerClient::OnServiceRemoved(const dbus::ObjectPath& object_path) { + VLOG(1) << "service removed: " << object_path.value(); + if (object_path != service_path_) + return; + service_path_ = dbus::ObjectPath(); + service_proxy_ = nullptr; +} + +void ApManagerClient::OnManagerRemoved(const dbus::ObjectPath& object_path) { + VLOG(1) << "manager removed: " << object_path.value(); + manager_proxy_ = nullptr; + Stop(); +} + +} // namespace privetd
diff --git a/buffet/privet/ap_manager_client.h b/buffet/privet/ap_manager_client.h new file mode 100644 index 0000000..ec99a6a --- /dev/null +++ b/buffet/privet/ap_manager_client.h
@@ -0,0 +1,57 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_AP_MANAGER_CLIENT_H_ +#define BUFFET_PRIVET_AP_MANAGER_CLIENT_H_ + +#include <memory> +#include <string> + +#include <base/callback.h> +#include <base/memory/ref_counted.h> + +#include "apmanager/dbus-proxies.h" + +namespace privetd { + +// Manages soft AP for wifi bootstrapping. +// Once created can handle multiple Start/Stop requests. +class ApManagerClient final { + public: + explicit ApManagerClient(const scoped_refptr<dbus::Bus>& bus); + ~ApManagerClient(); + + void Start(const std::string& ssid); + void Stop(); + + std::string GetSsid() const { return ssid_; } + + private: + void RemoveService(const dbus::ObjectPath& object_path); + + void OnManagerAdded(org::chromium::apmanager::ManagerProxy* manager_proxy); + void OnServiceAdded(org::chromium::apmanager::ServiceProxy* service_proxy); + + void OnSsidSet(bool success); + + void OnServiceRemoved(const dbus::ObjectPath& object_path); + void OnManagerRemoved(const dbus::ObjectPath& object_path); + + scoped_refptr<dbus::Bus> bus_; + + std::unique_ptr<org::chromium::apmanager::ObjectManagerProxy> + object_manager_proxy_; + org::chromium::apmanager::ManagerProxy* manager_proxy_{nullptr}; + + dbus::ObjectPath service_path_; + org::chromium::apmanager::ServiceProxy* service_proxy_{nullptr}; + + std::string ssid_; + + base::WeakPtrFactory<ApManagerClient> weak_ptr_factory_{this}; +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_AP_MANAGER_CLIENT_H_
diff --git a/buffet/privet/cloud_delegate.cc b/buffet/privet/cloud_delegate.cc new file mode 100644 index 0000000..5de432e --- /dev/null +++ b/buffet/privet/cloud_delegate.cc
@@ -0,0 +1,532 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/cloud_delegate.h" + +#include <map> +#include <vector> + +#include <base/bind.h> +#include <base/json/json_reader.h> +#include <base/json/json_writer.h> +#include <base/logging.h> +#include <base/memory/weak_ptr.h> +#include <base/message_loop/message_loop.h> +#include <base/values.h> +#include <chromeos/errors/error.h> +#include <chromeos/variant_dictionary.h> +#include <dbus/bus.h> + +#include "buffet/dbus-proxies.h" +#include "buffet/privet/constants.h" + +namespace privetd { + +namespace { + +using chromeos::ErrorPtr; +using chromeos::VariantDictionary; +using org::chromium::Buffet::ManagerProxy; +using org::chromium::Buffet::ObjectManagerProxy; + +const int kMaxSetupRetries = 5; +const int kFirstRetryTimeoutSec = 1; + +class CloudDelegateImpl : public CloudDelegate { + public: + CloudDelegateImpl(const scoped_refptr<dbus::Bus>& bus, + bool is_gcd_setup_enabled) + : object_manager_{bus}, is_gcd_setup_enabled_(is_gcd_setup_enabled) { + object_manager_.SetManagerAddedCallback( + base::Bind(&CloudDelegateImpl::OnManagerAdded, + weak_factory_.GetWeakPtr())); + object_manager_.SetManagerRemovedCallback( + base::Bind(&CloudDelegateImpl::OnManagerRemoved, + weak_factory_.GetWeakPtr())); + object_manager_.SetCommandRemovedCallback(base::Bind( + &CloudDelegateImpl::OnCommandRemoved, weak_factory_.GetWeakPtr())); + } + + ~CloudDelegateImpl() override = default; + + bool GetModelId(std::string* id, chromeos::ErrorPtr* error) const override { + if (!IsManagerReady(error)) + return false; + if (manager_->model_id().size() != 5) { + chromeos::Error::AddToPrintf( + error, FROM_HERE, errors::kDomain, errors::kInvalidState, + "Model ID is invalid: %s", manager_->model_id().c_str()); + return false; + } + *id = manager_->model_id(); + return true; + } + + bool GetName(std::string* name, chromeos::ErrorPtr* error) const override { + if (!IsManagerReady(error)) + return false; + *name = manager_->name(); + return true; + } + + std::string GetDescription() const override { + return manager_ ? manager_->description() : std::string{}; + } + + std::string GetLocation() const override { + return manager_ ? manager_->location() : std::string{}; + } + + void UpdateDeviceInfo(const std::string& name, + const std::string& description, + const std::string& location, + const base::Closure& success_callback, + const ErrorCallback& error_callback) override { + chromeos::ErrorPtr error; + if (!IsManagerReady(&error)) + return error_callback.Run(error.get()); + + if (name == manager_->name() && description == manager_->description() && + location == manager_->location()) { + return success_callback.Run(); + } + + manager_->UpdateDeviceInfoAsync(name, description, location, + success_callback, error_callback); + } + + std::string GetOemName() const override { + return manager_ ? manager_->oem_name() : std::string{}; + } + + std::string GetModelName() const override { + return manager_ ? manager_->model_name() : std::string{}; + } + + std::set<std::string> GetServices() const override { + std::set<std::string> result; + for (base::DictionaryValue::Iterator it{command_defs_}; !it.IsAtEnd(); + it.Advance()) { + result.emplace(it.key()); + } + return result; + } + + AuthScope GetAnonymousMaxScope() const override { + if (manager_) { + AuthScope scope; + if (StringToAuthScope(manager_->anonymous_access_role(), &scope)) + return scope; + } + return AuthScope::kNone; + } + + const ConnectionState& GetConnectionState() const override { + return connection_state_; + } + + const SetupState& GetSetupState() const override { return setup_state_; } + + bool Setup(const std::string& ticket_id, + const std::string& user, + chromeos::ErrorPtr* error) override { + if (!is_gcd_setup_enabled_) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kSetupUnavailable, + "GCD setup unavailible"); + return false; + } + if (!object_manager_.GetManagerProxy()) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kDeviceBusy, "Buffet is not ready"); + return false; + } + if (setup_state_.IsStatusEqual(SetupState::kInProgress)) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kDeviceBusy, "Setup in progress"); + return false; + } + VLOG(1) << "GCD Setup started. ticket_id: " << ticket_id + << ", user:" << user; + setup_state_ = SetupState(SetupState::kInProgress); + setup_weak_factory_.InvalidateWeakPtrs(); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&CloudDelegateImpl::CallManagerRegisterDevice, + setup_weak_factory_.GetWeakPtr(), ticket_id, 0), + base::TimeDelta::FromSeconds(kSetupDelaySeconds)); + // Return true because we tried setup. + return true; + } + + std::string GetCloudId() const override { + return manager_ ? manager_->device_id() : std::string{}; + } + + const base::DictionaryValue& GetState() const override { return state_; } + + const base::DictionaryValue& GetCommandDef() const override { + return command_defs_; + } + + void AddCommand(const base::DictionaryValue& command, + const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) override { + CHECK(user_info.scope() != AuthScope::kNone); + + chromeos::ErrorPtr error; + if (!IsManagerReady(&error)) + return error_callback.Run(error.get()); + + std::string command_str; + base::JSONWriter::Write(&command, &command_str); + manager_->AddCommandAsync( + command_str, AuthScopeToString(user_info.scope()), + base::Bind(&CloudDelegateImpl::OnAddCommandSucceeded, + weak_factory_.GetWeakPtr(), success_callback, + error_callback), + error_callback); + } + + void GetCommand(const std::string& id, + const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) override { + CHECK(user_info.scope() != AuthScope::kNone); + chromeos::ErrorPtr error; + if (!CanAccessCommand(id, user_info)) { + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kAccessDenied, + "Need to be owner of the command."); + return error_callback.Run(error.get()); + } + + GetCommandInternal(id, success_callback, error_callback); + } + + void CancelCommand(const std::string& id, + const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) override { + CHECK(user_info.scope() != AuthScope::kNone); + chromeos::ErrorPtr error; + if (!CanAccessCommand(id, user_info)) { + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kAccessDenied, + "Need to be owner of the command."); + return error_callback.Run(error.get()); + } + + for (auto command : object_manager_.GetCommandInstances()) { + if (command->id() == id) { + return command->CancelAsync( + base::Bind(&CloudDelegateImpl::GetCommandInternal, + weak_factory_.GetWeakPtr(), id, success_callback, + error_callback), + error_callback); + } + } + + chromeos::Error::AddToPrintf(&error, FROM_HERE, errors::kDomain, + errors::kNotFound, + "Command not found, ID='%s'", id.c_str()); + error_callback.Run(error.get()); + } + + void ListCommands(const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) override { + CHECK(user_info.scope() != AuthScope::kNone); + + std::vector<org::chromium::Buffet::CommandProxy*> commands{ + object_manager_.GetCommandInstances()}; + + auto ids = std::make_shared<std::vector<std::string>>(); + for (auto command : commands) { + if (CanAccessCommand(command->id(), user_info)) + ids->push_back(command->id()); + } + + GetNextCommand(ids, std::make_shared<base::ListValue>(), success_callback, + error_callback, base::DictionaryValue{}); + } + + private: + void GetCommandInternal(const std::string& id, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) { + chromeos::ErrorPtr error; + if (!IsManagerReady(&error)) + return error_callback.Run(error.get()); + manager_->GetCommandAsync( + id, base::Bind(&CloudDelegateImpl::OnGetCommandSucceeded, + weak_factory_.GetWeakPtr(), success_callback, + error_callback), + error_callback); + } + + void OnManagerAdded(ManagerProxy* manager) { + manager_ = manager; + manager_->SetPropertyChangedCallback( + base::Bind(&CloudDelegateImpl::OnManagerPropertyChanged, + weak_factory_.GetWeakPtr())); + // Read all initial values. + OnManagerPropertyChanged(manager, std::string{}); + } + + void OnCommandRemoved(const dbus::ObjectPath& object_path) { + command_owners_.erase(object_manager_.GetCommandProxy(object_path)->id()); + } + + void OnManagerPropertyChanged(ManagerProxy* manager, + const std::string& property_name) { + CHECK_EQ(manager_, manager); + + if (property_name.empty() || property_name == ManagerProxy::StatusName()) { + OnStatusPropertyChanged(); + } + + if (property_name.empty() || + property_name == ManagerProxy::DeviceIdName() || + property_name == ManagerProxy::OemNameName() || + property_name == ManagerProxy::ModelNameName() || + property_name == ManagerProxy::ModelIdName() || + property_name == ManagerProxy::NameName() || + property_name == ManagerProxy::DescriptionName() || + property_name == ManagerProxy::LocationName() || + property_name == ManagerProxy::AnonymousAccessRoleName()) { + NotifyOnDeviceInfoChanged(); + } + + if (property_name.empty() || property_name == ManagerProxy::StateName()) { + OnStatePropertyChanged(); + } + + if (property_name.empty() || + property_name == ManagerProxy::CommandDefsName()) { + OnCommandDefsPropertyChanged(); + } + } + + void OnStatusPropertyChanged() { + const std::string& status = manager_->status(); + if (status == "unconfigured") { + connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; + } else if (status == "connecting") { + // TODO(vitalybuka): Find conditions for kOffline. + connection_state_ = ConnectionState{ConnectionState::kConnecting}; + } else if (status == "connected") { + connection_state_ = ConnectionState{ConnectionState::kOnline}; + } else { + chromeos::ErrorPtr error; + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidState, + "Unexpected buffet status: %s", status.c_str()); + connection_state_ = ConnectionState{std::move(error)}; + } + NotifyOnDeviceInfoChanged(); + } + + void OnStatePropertyChanged() { + state_.Clear(); + std::unique_ptr<base::Value> value{ + base::JSONReader::Read(manager_->state())}; + const base::DictionaryValue* state{nullptr}; + if (value && value->GetAsDictionary(&state)) + state_.MergeDictionary(state); + NotifyOnStateChanged(); + } + + void OnCommandDefsPropertyChanged() { + command_defs_.Clear(); + std::unique_ptr<base::Value> value{ + base::JSONReader::Read(manager_->command_defs())}; + const base::DictionaryValue* defs{nullptr}; + if (value && value->GetAsDictionary(&defs)) + command_defs_.MergeDictionary(defs); + NotifyOnCommandDefsChanged(); + } + + void OnManagerRemoved(const dbus::ObjectPath& path) { + manager_ = nullptr; + connection_state_ = ConnectionState(ConnectionState::kDisabled); + state_.Clear(); + command_defs_.Clear(); + command_owners_.clear(); + NotifyOnDeviceInfoChanged(); + NotifyOnCommandDefsChanged(); + NotifyOnStateChanged(); + } + + void RetryRegister(const std::string& ticket_id, + int retries, + chromeos::Error* error) { + if (retries >= kMaxSetupRetries) { + chromeos::ErrorPtr new_error{error ? error->Clone() : nullptr}; + chromeos::Error::AddTo(&new_error, FROM_HERE, errors::kDomain, + errors::kInvalidState, + "Failed to register device"); + setup_state_ = SetupState{std::move(new_error)}; + return; + } + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&CloudDelegateImpl::CallManagerRegisterDevice, + setup_weak_factory_.GetWeakPtr(), ticket_id, retries + 1), + base::TimeDelta::FromSeconds(kFirstRetryTimeoutSec << retries)); + } + + void OnRegisterSuccess(const std::string& device_id) { + VLOG(1) << "Device registered: " << device_id; + setup_state_ = SetupState(SetupState::kSuccess); + } + + void CallManagerRegisterDevice(const std::string& ticket_id, int retries) { + auto manager_proxy = object_manager_.GetManagerProxy(); + if (!manager_proxy) { + LOG(ERROR) << "Couldn't register because Buffet was offline."; + RetryRegister(ticket_id, retries, nullptr); + return; + } + manager_proxy->RegisterDeviceAsync( + ticket_id, base::Bind(&CloudDelegateImpl::OnRegisterSuccess, + setup_weak_factory_.GetWeakPtr()), + base::Bind(&CloudDelegateImpl::RetryRegister, + setup_weak_factory_.GetWeakPtr(), ticket_id, retries)); + } + + void OnAddCommandSucceeded(const SuccessCallback& success_callback, + const ErrorCallback& error_callback, + const std::string& id) { + GetCommandInternal(id, success_callback, error_callback); + } + + void OnGetCommandSucceeded(const SuccessCallback& success_callback, + const ErrorCallback& error_callback, + const std::string& json_command) { + std::unique_ptr<base::Value> value{base::JSONReader::Read(json_command)}; + base::DictionaryValue* command{nullptr}; + if (!value || !value->GetAsDictionary(&command)) { + chromeos::ErrorPtr error; + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kInvalidFormat, + "Buffet returned invalid JSON"); + return error_callback.Run(error.get()); + } + success_callback.Run(*command); + } + + void GetNextCommandSkipError( + const std::shared_ptr<std::vector<std::string>>& ids, + const std::shared_ptr<base::ListValue>& commands, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback, + chromeos::Error*) { + // Ignore if we can't get some commands. Maybe they were removed. + GetNextCommand(ids, commands, success_callback, error_callback, + base::DictionaryValue{}); + } + + void GetNextCommand(const std::shared_ptr<std::vector<std::string>>& ids, + const std::shared_ptr<base::ListValue>& commands, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback, + const base::DictionaryValue& json) { + if (!json.empty()) + commands->Append(json.DeepCopy()); + + if (ids->empty()) { + base::DictionaryValue commands_json; + commands_json.Set("commands", commands->DeepCopy()); + return success_callback.Run(commands_json); + } + + std::string next_id = ids->back(); + ids->pop_back(); + + auto on_success = base::Bind(&CloudDelegateImpl::GetNextCommand, + weak_factory_.GetWeakPtr(), ids, commands, + success_callback, error_callback); + + auto on_error = base::Bind(&CloudDelegateImpl::GetNextCommandSkipError, + weak_factory_.GetWeakPtr(), ids, commands, + success_callback, error_callback); + + GetCommandInternal(next_id, on_success, on_error); + } + + bool IsManagerReady(chromeos::ErrorPtr* error) const { + if (!manager_) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kDeviceBusy, "Buffet is not ready"); + return false; + } + return true; + } + + bool CanAccessCommand(const std::string& command_id, + const UserInfo& user_info) const { + if (user_info.scope() == AuthScope::kOwner) + return true; + auto it = command_owners_.find(command_id); + return it != command_owners_.end() && it->second == user_info.user_id(); + } + + ObjectManagerProxy object_manager_; + + bool is_gcd_setup_enabled_{false}; + + ManagerProxy* manager_{nullptr}; + + // Primary state of GCD. + ConnectionState connection_state_{ConnectionState::kDisabled}; + + // State of the current or last setup. + SetupState setup_state_{SetupState::kNone}; + + // Current device state. + base::DictionaryValue state_; + + // Current commands definitions. + base::DictionaryValue command_defs_; + + // Map of command IDs to user IDs. + std::map<std::string, uint64_t> command_owners_; + + // |setup_weak_factory_| tracks the lifetime of callbacks used in connection + // with a particular invocation of Setup(). + base::WeakPtrFactory<CloudDelegateImpl> setup_weak_factory_{this}; + // |weak_factory_| tracks the lifetime of |this|. + base::WeakPtrFactory<CloudDelegateImpl> weak_factory_{this}; +}; + +} // namespace + +CloudDelegate::CloudDelegate() { +} + +CloudDelegate::~CloudDelegate() { +} + +// static +std::unique_ptr<CloudDelegate> CloudDelegate::CreateDefault( + const scoped_refptr<dbus::Bus>& bus, + bool is_gcd_setup_enabled) { + return std::unique_ptr<CloudDelegateImpl>{ + new CloudDelegateImpl{bus, is_gcd_setup_enabled}}; +} + +void CloudDelegate::NotifyOnDeviceInfoChanged() { + FOR_EACH_OBSERVER(Observer, observer_list_, OnDeviceInfoChanged()); +} + +void CloudDelegate::NotifyOnCommandDefsChanged() { + FOR_EACH_OBSERVER(Observer, observer_list_, OnCommandDefsChanged()); +} + +void CloudDelegate::NotifyOnStateChanged() { + FOR_EACH_OBSERVER(Observer, observer_list_, OnStateChanged()); +} + +} // namespace privetd
diff --git a/buffet/privet/cloud_delegate.h b/buffet/privet/cloud_delegate.h new file mode 100644 index 0000000..0d027f9 --- /dev/null +++ b/buffet/privet/cloud_delegate.h
@@ -0,0 +1,143 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_CLOUD_DELEGATE_H_ +#define BUFFET_PRIVET_CLOUD_DELEGATE_H_ + +#include <memory> +#include <set> +#include <string> + +#include <base/callback.h> +#include <base/memory/ref_counted.h> +#include <base/observer_list.h> + +#include "buffet/privet/privet_types.h" +#include "buffet/privet/security_delegate.h" + +namespace base { +class DictionaryValue; +} // namespace base + +namespace dbus { +class Bus; +} // namespace dbus + +namespace privetd { + +// Interface to provide GCD functionality for PrivetHandler. +// TODO(vitalybuka): Rename to BuffetDelegate. +class CloudDelegate { + public: + CloudDelegate(); + virtual ~CloudDelegate(); + + using SuccessCallback = base::Callback<void(const base::DictionaryValue&)>; + using ErrorCallback = base::Callback<void(chromeos::Error*)>; + + class Observer { + public: + virtual ~Observer() = default; + + virtual void OnDeviceInfoChanged() {} + virtual void OnCommandDefsChanged() {} + virtual void OnStateChanged() {} + }; + + // Returns the model ID of the device. + virtual bool GetModelId(std::string* id, chromeos::ErrorPtr* error) const = 0; + + // Returns the name of device. + virtual bool GetName(std::string* name, chromeos::ErrorPtr* error) const = 0; + + // Returns the description of the device. + virtual std::string GetDescription() const = 0; + + // Returns the location of the device. + virtual std::string GetLocation() const = 0; + + // Update basic device information. + virtual void UpdateDeviceInfo(const std::string& name, + const std::string& description, + const std::string& location, + const base::Closure& success_callback, + const ErrorCallback& error_callback) = 0; + + // Returns the name of the maker. + virtual std::string GetOemName() const = 0; + + // Returns the model name of the device. + virtual std::string GetModelName() const = 0; + + // Returns the list of services supported by device. + // E.g. printer, scanner etc. Should match services published on mDNS. + virtual std::set<std::string> GetServices() const = 0; + + // Returns max scope available for anonymous user. + virtual AuthScope GetAnonymousMaxScope() const = 0; + + // Returns status of the GCD connection. + virtual const ConnectionState& GetConnectionState() const = 0; + + // Returns status of the last setup. + virtual const SetupState& GetSetupState() const = 0; + + // Starts GCD setup. + virtual bool Setup(const std::string& ticket_id, + const std::string& user, + chromeos::ErrorPtr* error) = 0; + + // Returns cloud id if the registered device or empty string if unregistered. + virtual std::string GetCloudId() const = 0; + + // Returns dictionary with device state. + virtual const base::DictionaryValue& GetState() const = 0; + + // Returns dictionary with commands definitions. + virtual const base::DictionaryValue& GetCommandDef() const = 0; + + // Adds command created from the given JSON representation. + virtual void AddCommand(const base::DictionaryValue& command, + const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) = 0; + + // Returns command with the given ID. + virtual void GetCommand(const std::string& id, + const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) = 0; + + // Cancels command with the given ID. + virtual void CancelCommand(const std::string& id, + const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) = 0; + + // Lists commands. + virtual void ListCommands(const UserInfo& user_info, + const SuccessCallback& success_callback, + const ErrorCallback& error_callback) = 0; + + void AddObserver(Observer* observer) { observer_list_.AddObserver(observer); } + void RemoveObserver(Observer* observer) { + observer_list_.RemoveObserver(observer); + } + + void NotifyOnDeviceInfoChanged(); + void NotifyOnCommandDefsChanged(); + void NotifyOnStateChanged(); + + // Create default instance. + static std::unique_ptr<CloudDelegate> CreateDefault( + const scoped_refptr<dbus::Bus>& bus, + bool is_gcd_setup_enabled); + + private: + ObserverList<Observer> observer_list_; +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_CLOUD_DELEGATE_H_
diff --git a/buffet/privet/constants.cc b/buffet/privet/constants.cc new file mode 100644 index 0000000..e94b541 --- /dev/null +++ b/buffet/privet/constants.cc
@@ -0,0 +1,36 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/constants.h" + +namespace privetd { + +namespace errors { + +const char kDomain[] = "privetd"; + +const char kInvalidClientCommitment[] = "invalidClientCommitment"; +const char kInvalidFormat[] = "invalidFormat"; +const char kMissingAuthorization[] = "missingAuthorization"; +const char kInvalidAuthorization[] = "invalidAuthorization"; +const char kInvalidAuthorizationScope[] = "invalidAuthorizationScope"; +const char kAuthorizationExpired[] = "authorizationExpired"; +const char kCommitmentMismatch[] = "commitmentMismatch"; +const char kUnknownSession[] = "unknownSession"; +const char kInvalidAuthCode[] = "invalidAuthCode"; +const char kInvalidAuthMode[] = "invalidAuthMode"; +const char kInvalidRequestedScope[] = "invalidRequestedScope"; +const char kAccessDenied[] = "accessDenied"; +const char kInvalidParams[] = "invalidParams"; +const char kSetupUnavailable[] = "setupUnavailable"; +const char kDeviceBusy[] = "deviceBusy"; +const char kInvalidState[] = "invalidState"; +const char kInvalidSsid[] = "invalidSsid"; +const char kInvalidPassphrase[] = "invalidPassphrase"; +const char kNotFound[] = "notFound"; +const char kNotImplemented[] = "notImplemented"; + +} // namespace errors + +} // namespace privetd
diff --git a/buffet/privet/constants.h b/buffet/privet/constants.h new file mode 100644 index 0000000..c408ca1 --- /dev/null +++ b/buffet/privet/constants.h
@@ -0,0 +1,41 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_CONSTANTS_H_ +#define BUFFET_PRIVET_CONSTANTS_H_ + +namespace privetd { + +namespace errors { + +extern const char kDomain[]; + +extern const char kInvalidClientCommitment[]; +extern const char kInvalidFormat[]; +extern const char kMissingAuthorization[]; +extern const char kInvalidAuthorization[]; +extern const char kInvalidAuthorizationScope[]; +extern const char kAuthorizationExpired[]; +extern const char kCommitmentMismatch[]; +extern const char kUnknownSession[]; +extern const char kInvalidAuthCode[]; +extern const char kInvalidAuthMode[]; +extern const char kInvalidRequestedScope[]; +extern const char kAccessDenied[]; +extern const char kInvalidParams[]; +extern const char kSetupUnavailable[]; +extern const char kDeviceBusy[]; +extern const char kInvalidState[]; +extern const char kInvalidSsid[]; +extern const char kInvalidPassphrase[]; +extern const char kNotFound[]; +extern const char kNotImplemented[]; +} // namespace errors + +// Time to reply on privet HTTP. +const int kSetupDelaySeconds = 1; + +} // namespace privetd + +#endif // BUFFET_PRIVET_CONSTANTS_H_
diff --git a/buffet/privet/daemon_state.cc b/buffet/privet/daemon_state.cc new file mode 100644 index 0000000..cdbe808 --- /dev/null +++ b/buffet/privet/daemon_state.cc
@@ -0,0 +1,33 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/daemon_state.h" + +namespace privetd { + +namespace state_key { + +const char kDeviceId[] = "id"; +const char kDeviceName[] = "name"; +const char kDeviceDescription[] = "description"; +const char kDeviceLocation[] = "description"; + +const char kWifiHasBeenBootstrapped[] = "have_ever_been_bootstrapped"; +const char kWifiLastConfiguredSSID[] = "last_configured_ssid"; + +} // namespace state_key + +DaemonState::DaemonState(const base::FilePath& state_path) + : state_path_(state_path) { +} + +void DaemonState::Init() { + Load(state_path_); +} + +void DaemonState::Save() const { + KeyValueStore::Save(state_path_); +} + +} // namespace privetd
diff --git a/buffet/privet/daemon_state.h b/buffet/privet/daemon_state.h new file mode 100644 index 0000000..a2d7616 --- /dev/null +++ b/buffet/privet/daemon_state.h
@@ -0,0 +1,42 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_DAEMON_STATE_H_ +#define BUFFET_PRIVET_DAEMON_STATE_H_ + +#include <base/files/file_path.h> +#include <base/macros.h> +#include <chromeos/key_value_store.h> + +namespace privetd { + +namespace state_key { + +extern const char kDeviceId[]; +extern const char kDeviceName[]; +extern const char kDeviceDescription[]; +extern const char kDeviceLocation[]; + +extern const char kWifiHasBeenBootstrapped[]; +extern const char kWifiLastConfiguredSSID[]; + +} // namespace state_key + +class DaemonState : public chromeos::KeyValueStore { + public: + explicit DaemonState(const base::FilePath& state_path); + // Load initial state from disk. + void Init(); + // Save state to disk. + void Save() const; + + private: + const base::FilePath state_path_; + + DISALLOW_COPY_AND_ASSIGN(DaemonState); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_DAEMON_STATE_H_
diff --git a/buffet/privet/dbus_manager.cc b/buffet/privet/dbus_manager.cc new file mode 100644 index 0000000..a336a19 --- /dev/null +++ b/buffet/privet/dbus_manager.cc
@@ -0,0 +1,140 @@ +// Copyright 2015 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/dbus_manager.h" + +#include <base/memory/ref_counted.h> +#include <chromeos/any.h> + +#include "buffet/privet/constants.h" +#include "buffet/privet/security_delegate.h" +#include "buffet/privet/security_manager.h" + +using chromeos::Any; +using chromeos::dbus_utils::AsyncEventSequencer; +using chromeos::dbus_utils::DBusObject; +using chromeos::dbus_utils::ExportedObjectManager; +using org::chromium::privetd::ManagerAdaptor; + +namespace privetd { + +namespace { + +const char kPingResponse[] = "Hello world!"; +const char kPairingSessionIdKey[] = "sessionId"; +const char kPairingModeKey[] = "mode"; +const char kPairingCodeKey[] = "code"; + +} // namespace + +DBusManager::DBusManager(ExportedObjectManager* object_manager, + WifiBootstrapManager* wifi_bootstrap_manager, + CloudDelegate* cloud_delegate, + SecurityManager* security_manager) + : dbus_object_{new DBusObject{object_manager, + object_manager->GetBus(), + ManagerAdaptor::GetObjectPath()}} { + if (wifi_bootstrap_manager) { + wifi_bootstrap_manager->RegisterStateListener( + base::Bind(&DBusManager::UpdateWiFiBootstrapState, + weak_ptr_factory_.GetWeakPtr())); + } else { + UpdateWiFiBootstrapState(WifiBootstrapManager::kDisabled); + } + security_manager->RegisterPairingListeners( + base::Bind(&DBusManager::OnPairingStart, + weak_ptr_factory_.GetWeakPtr()), + base::Bind(&DBusManager::OnPairingEnd, + weak_ptr_factory_.GetWeakPtr())); + // TODO(wiley) Watch for appropriate state variables from |cloud_delegate|. +} + +void DBusManager::RegisterAsync(const CompletionAction& on_done) { + scoped_refptr<AsyncEventSequencer> sequencer(new AsyncEventSequencer()); + dbus_adaptor_.RegisterWithDBusObject(dbus_object_.get()); + dbus_object_->RegisterAsync( + sequencer->GetHandler("Failed exporting DBusManager.", true)); + sequencer->OnAllTasksCompletedCall({on_done}); +} + +bool DBusManager::EnableWiFiBootstrapping( + chromeos::ErrorPtr* error, + const dbus::ObjectPath& in_listener_path, + const chromeos::VariantDictionary& in_options) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kNotImplemented, + "Manual WiFi bootstrapping is not implemented"); + return false; +} + +bool DBusManager::DisableWiFiBootstrapping(chromeos::ErrorPtr* error) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kNotImplemented, + "Manual WiFi bootstrapping is not implemented"); + return false; +} + +bool DBusManager::EnableGCDBootstrapping( + chromeos::ErrorPtr* error, + const dbus::ObjectPath& in_listener_path, + const chromeos::VariantDictionary& in_options) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kNotImplemented, + "Manual GCD bootstrapping is not implemented"); + return false; +} + +bool DBusManager::DisableGCDBootstrapping(chromeos::ErrorPtr* error) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kNotImplemented, + "Manual GCD bootstrapping is not implemented"); + return false; +} + +std::string DBusManager::Ping() { + return kPingResponse; +} + +void DBusManager::UpdateWiFiBootstrapState(WifiBootstrapManager::State state) { + switch (state) { + case WifiBootstrapManager::kDisabled: + dbus_adaptor_.SetWiFiBootstrapState("disabled"); + break; + case WifiBootstrapManager::kBootstrapping: + dbus_adaptor_.SetWiFiBootstrapState("waiting"); + break; + case WifiBootstrapManager::kMonitoring: + dbus_adaptor_.SetWiFiBootstrapState("monitoring"); + break; + case WifiBootstrapManager::kConnecting: + dbus_adaptor_.SetWiFiBootstrapState("connecting"); + break; + } +} + +void DBusManager::OnPairingStart(const std::string& session_id, + PairingType pairing_type, + const std::vector<uint8_t>& code) { + // For now, just overwrite the exposed PairInfo with + // the most recent pairing attempt. + dbus_adaptor_.SetPairingInfo(chromeos::VariantDictionary{ + {kPairingSessionIdKey, session_id}, + {kPairingModeKey, PairingTypeToString(pairing_type)}, + {kPairingCodeKey, code}, + }); +} + +void DBusManager::OnPairingEnd(const std::string& session_id) { + auto exposed_pairing_attempt = dbus_adaptor_.GetPairingInfo(); + auto it = exposed_pairing_attempt.find(kPairingSessionIdKey); + if (it == exposed_pairing_attempt.end()) { + return; + } + std::string exposed_session{it->second.TryGet<std::string>()}; + if (exposed_session == session_id) { + dbus_adaptor_.SetPairingInfo(chromeos::VariantDictionary{}); + } +} + +} // namespace privetd
diff --git a/buffet/privet/dbus_manager.h b/buffet/privet/dbus_manager.h new file mode 100644 index 0000000..03cce45 --- /dev/null +++ b/buffet/privet/dbus_manager.h
@@ -0,0 +1,76 @@ +// Copyright 2015 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_DBUS_MANAGER_H_ +#define BUFFET_PRIVET_DBUS_MANAGER_H_ + +#include <memory> +#include <string> +#include <vector> + +#include <base/macros.h> +#include <chromeos/dbus/async_event_sequencer.h> +#include <chromeos/dbus/dbus_object.h> +#include <chromeos/errors/error.h> +#include <chromeos/variant_dictionary.h> +#include <dbus/object_path.h> + +#include "buffet/privet/org.chromium.privetd.Manager.h" +#include "buffet/privet/wifi_bootstrap_manager.h" + +namespace chromeos { +namespace dbus_utils { +class ExportedObjectManager; +} // dbus_utils +} // chromeos + +namespace privetd { + +class CloudDelegate; +class SecurityManager; +enum class PairingType; + +// Exposes most of the privetd DBus interface. +class DBusManager : public org::chromium::privetd::ManagerInterface { + public: + using CompletionAction = + chromeos::dbus_utils::AsyncEventSequencer::CompletionAction; + + DBusManager(chromeos::dbus_utils::ExportedObjectManager* object_manager, + WifiBootstrapManager* wifi_bootstrap_manager, + CloudDelegate* cloud_delegate, + SecurityManager* security_manager); + ~DBusManager() override = default; + void RegisterAsync(const CompletionAction& on_done); + + // DBus handlers + bool EnableWiFiBootstrapping( + chromeos::ErrorPtr* error, + const dbus::ObjectPath& in_listener_path, + const chromeos::VariantDictionary& in_options) override; + bool DisableWiFiBootstrapping(chromeos::ErrorPtr* error) override; + bool EnableGCDBootstrapping( + chromeos::ErrorPtr* error, + const dbus::ObjectPath& in_listener_path, + const chromeos::VariantDictionary& in_options) override; + bool DisableGCDBootstrapping(chromeos::ErrorPtr* error) override; + std::string Ping() override; + + private: + void UpdateWiFiBootstrapState(WifiBootstrapManager::State state); + void OnPairingStart(const std::string& session_id, + PairingType pairing_type, + const std::vector<uint8_t>& code); + void OnPairingEnd(const std::string& session_id); + + org::chromium::privetd::ManagerAdaptor dbus_adaptor_{this}; + std::unique_ptr<chromeos::dbus_utils::DBusObject> dbus_object_; + base::WeakPtrFactory<DBusManager> weak_ptr_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(DBusManager); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_DBUS_MANAGER_H_
diff --git a/buffet/privet/device_delegate.cc b/buffet/privet/device_delegate.cc new file mode 100644 index 0000000..2953284 --- /dev/null +++ b/buffet/privet/device_delegate.cc
@@ -0,0 +1,58 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/device_delegate.h" + +#include <base/files/file_util.h> +#include <base/guid.h> + +#include "buffet/privet/constants.h" + +namespace privetd { + +namespace { + +class DeviceDelegateImpl : public DeviceDelegate { + public: + DeviceDelegateImpl() = default; + ~DeviceDelegateImpl() override = default; + + std::pair<uint16_t, uint16_t> GetHttpEnpoint() const override { + return std::make_pair(http_port_, http_port_); + } + std::pair<uint16_t, uint16_t> GetHttpsEnpoint() const override { + return std::make_pair(https_port_, https_port_); + } + base::TimeDelta GetUptime() const override { + return base::Time::Now() - start_time_; + } + + void SetHttpPort(uint16_t port) override { + http_port_ = port; + } + + void SetHttpsPort(uint16_t port) override { + https_port_ = port; + } + + private: + uint16_t http_port_{0}; + uint16_t https_port_{0}; + base::Time start_time_{base::Time::Now()}; +}; + +} // namespace + +DeviceDelegate::DeviceDelegate() { +} + +DeviceDelegate::~DeviceDelegate() { +} + +// static +std::unique_ptr<DeviceDelegate> DeviceDelegate::CreateDefault() { + return std::unique_ptr<DeviceDelegate>(new DeviceDelegateImpl()); +} + +} // namespace privetd
diff --git a/buffet/privet/device_delegate.h b/buffet/privet/device_delegate.h new file mode 100644 index 0000000..9888dfd --- /dev/null +++ b/buffet/privet/device_delegate.h
@@ -0,0 +1,45 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_DEVICE_DELEGATE_H_ +#define BUFFET_PRIVET_DEVICE_DELEGATE_H_ + +#include <memory> +#include <utility> + +#include <base/time/time.h> + +namespace privetd { + +// Interface to provide access to general information about device. +class DeviceDelegate { + public: + DeviceDelegate(); + virtual ~DeviceDelegate(); + + // Returns HTTP ports for Privet. The first one is the primary port, + // the second is the port for a pooling updates requests. The second value + // could be 0. In this case the first port would be use for regular and for + // updates requests. + virtual std::pair<uint16_t, uint16_t> GetHttpEnpoint() const = 0; + + // The same |GetHttpEnpoint| but for HTTPS. + virtual std::pair<uint16_t, uint16_t> GetHttpsEnpoint() const = 0; + + // Returns device update. + virtual base::TimeDelta GetUptime() const = 0; + + // Updates the HTTP port value. + virtual void SetHttpPort(uint16_t port) = 0; + + // Updates the HTTPS port value. + virtual void SetHttpsPort(uint16_t port) = 0; + + // Create default instance. + static std::unique_ptr<DeviceDelegate> CreateDefault(); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_DEVICE_DELEGATE_H_
diff --git a/buffet/privet/identity_delegate.h b/buffet/privet/identity_delegate.h new file mode 100644 index 0000000..5ab97d1 --- /dev/null +++ b/buffet/privet/identity_delegate.h
@@ -0,0 +1,24 @@ +// Copyright 2015 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_IDENTITY_DELEGATE_H_ +#define BUFFET_PRIVET_IDENTITY_DELEGATE_H_ + +#include <string> + +namespace privetd { + +// Interface for an object that can identify ourselves. +class IdentityDelegate { + public: + IdentityDelegate() = default; + virtual ~IdentityDelegate() = default; + + // Returns a unique identifier for this device. + virtual std::string GetId() const = 0; +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_IDENTITY_DELEGATE_H_
diff --git a/buffet/privet/main.cc b/buffet/privet/main.cc new file mode 100644 index 0000000..a590776 --- /dev/null +++ b/buffet/privet/main.cc
@@ -0,0 +1,311 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <memory> +#include <set> +#include <string> +#include <sysexits.h> + +#include <base/bind.h> +#include <base/command_line.h> +#include <base/json/json_reader.h> +#include <base/memory/weak_ptr.h> +#include <base/scoped_observer.h> +#include <base/strings/string_number_conversions.h> +#include <base/values.h> +#include <chromeos/daemons/dbus_daemon.h> +#include <chromeos/flag_helper.h> +#include <chromeos/http/http_request.h> +#include <chromeos/key_value_store.h> +#include <chromeos/mime_utils.h> +#include <chromeos/strings/string_utils.h> +#include <chromeos/syslog_logging.h> +#include <libwebserv/protocol_handler.h> +#include <libwebserv/request.h> +#include <libwebserv/response.h> +#include <libwebserv/server.h> + +#include "buffet/privet/ap_manager_client.h" +#include "buffet/privet/cloud_delegate.h" +#include "buffet/privet/constants.h" +#include "buffet/privet/daemon_state.h" +#include "buffet/privet/dbus_manager.h" +#include "buffet/privet/device_delegate.h" +#include "buffet/privet/peerd_client.h" +#include "buffet/privet/privet_handler.h" +#include "buffet/privet/privetd_conf_parser.h" +#include "buffet/privet/security_manager.h" +#include "buffet/privet/shill_client.h" +#include "buffet/privet/wifi_bootstrap_manager.h" + +namespace privetd { + +namespace { + +using chromeos::dbus_utils::AsyncEventSequencer; +using libwebserv::ProtocolHandler; +using libwebserv::Request; +using libwebserv::Response; + +const char kDefaultConfigFilePath[] = "/etc/privetd/privetd.conf"; +const char kDefaultStateFilePath[] = "/var/lib/privetd/privetd.state"; + +std::string GetFirstHeader(const Request& request, const std::string& name) { + std::vector<std::string> headers = request.GetHeader(name); + return headers.empty() ? std::string() : headers.front(); +} + +const char kServiceName[] = "org.chromium.privetd"; +const char kRootPath[] = "/org/chromium/privetd"; + +class Daemon : public chromeos::DBusServiceDaemon, + public CloudDelegate::Observer { + public: + Daemon(bool disable_security, + bool enable_ping, + const std::set<std::string>& device_whitelist, + const base::FilePath& config_path, + const base::FilePath& state_path) + : DBusServiceDaemon(kServiceName, kRootPath), + disable_security_(disable_security), + enable_ping_(enable_ping), + device_whitelist_(device_whitelist), + config_path_(config_path), + state_store_(new DaemonState(state_path)) {} + + void RegisterDBusObjectsAsync(AsyncEventSequencer* sequencer) override { + chromeos::KeyValueStore config_store; + if (!config_store.Load(config_path_)) { + LOG(ERROR) << "Failed to read privetd config file from " + << config_path_.value(); + } else { + CHECK(parser_.Parse(config_store)) + << "Failed to read configuration file."; + } + state_store_->Init(); + // This state store key doesn't exist naturally, but developers + // sometime put it in their state store to cause the device to bring + // up WiFi bootstrapping while being connected to an ethernet interface. + std::string test_device_whitelist; + if (device_whitelist_.empty() && + state_store_->GetString(kWiFiBootstrapInterfaces, + &test_device_whitelist)) { + auto interfaces = + chromeos::string_utils::Split(test_device_whitelist, ",", true, true); + device_whitelist_.insert(interfaces.begin(), interfaces.end()); + } + device_ = DeviceDelegate::CreateDefault(); + cloud_ = CloudDelegate::CreateDefault( + bus_, parser_.gcd_bootstrap_mode() != GcdBootstrapMode::kDisabled); + cloud_observer_.Add(cloud_.get()); + security_.reset(new SecurityManager(parser_.pairing_modes(), + parser_.embedded_code_path(), + disable_security_)); + shill_client_.reset(new ShillClient( + bus_, device_whitelist_.empty() ? parser_.automatic_wifi_interfaces() + : device_whitelist_)); + shill_client_->RegisterConnectivityListener( + base::Bind(&Daemon::OnConnectivityChanged, base::Unretained(this))); + ap_manager_client_.reset(new ApManagerClient(bus_)); + + if (parser_.wifi_bootstrap_mode() != WiFiBootstrapMode::kDisabled) { + VLOG(1) << "Enabling WiFi bootstrapping."; + wifi_bootstrap_manager_.reset(new WifiBootstrapManager( + state_store_.get(), shill_client_.get(), ap_manager_client_.get(), + cloud_.get(), parser_.connect_timeout_seconds(), + parser_.bootstrap_timeout_seconds(), + parser_.monitor_timeout_seconds())); + wifi_bootstrap_manager_->Init(); + } + + peerd_client_.reset(new PeerdClient(bus_, device_.get(), cloud_.get(), + wifi_bootstrap_manager_.get())); + + privet_handler_.reset(new PrivetHandler(cloud_.get(), device_.get(), + security_.get(), + wifi_bootstrap_manager_.get(), + peerd_client_.get())); + + web_server_.OnProtocolHandlerConnected( + base::Bind(&Daemon::OnProtocolHandlerConnected, + weak_ptr_factory_.GetWeakPtr())); + web_server_.OnProtocolHandlerDisconnected( + base::Bind(&Daemon::OnProtocolHandlerDisconnected, + weak_ptr_factory_.GetWeakPtr())); + + web_server_.Connect( + bus_, + kServiceName, + sequencer->GetHandler("Server::Connect failed.", true), + base::Bind(&base::DoNothing), + base::Bind(&base::DoNothing)); + + web_server_.GetDefaultHttpHandler()->AddHandlerCallback( + "/privet/", "", + base::Bind(&Daemon::PrivetRequestHandler, base::Unretained(this))); + web_server_.GetDefaultHttpsHandler()->AddHandlerCallback( + "/privet/", "", + base::Bind(&Daemon::PrivetRequestHandler, base::Unretained(this))); + if (enable_ping_) { + web_server_.GetDefaultHttpHandler()->AddHandlerCallback( + "/privet/ping", chromeos::http::request_type::kGet, + base::Bind(&Daemon::HelloWorldHandler, base::Unretained(this))); + web_server_.GetDefaultHttpsHandler()->AddHandlerCallback( + "/privet/ping", chromeos::http::request_type::kGet, + base::Bind(&Daemon::HelloWorldHandler, base::Unretained(this))); + } + + dbus_manager_.reset(new DBusManager{object_manager_.get(), + wifi_bootstrap_manager_.get(), + cloud_.get(), + security_.get()}); + dbus_manager_->RegisterAsync( + sequencer->GetHandler("DBusManager.RegisterAsync() failed.", true)); + } + + void OnShutdown(int* return_code) override { + web_server_.Disconnect(); + DBusDaemon::OnShutdown(return_code); + } + + void OnDeviceInfoChanged() override { OnChanged(); }; + + private: + void PrivetRequestHandler(std::unique_ptr<Request> request, + std::unique_ptr<Response> response) { + std::string auth_header = GetFirstHeader( + *request, chromeos::http::request_header::kAuthorization); + if (auth_header.empty() && disable_security_) + auth_header = "Privet anonymous"; + std::string data(request->GetData().begin(), request->GetData().end()); + VLOG(3) << "Input: " << data; + + base::DictionaryValue empty; + std::unique_ptr<base::Value> value; + const base::DictionaryValue* dictionary = nullptr; + + if (data.empty()) { + dictionary = ∅ + } else { + std::string content_type = + chromeos::mime::RemoveParameters(GetFirstHeader( + *request, chromeos::http::request_header::kContentType)); + if (content_type == chromeos::mime::application::kJson) { + value.reset(base::JSONReader::Read(data)); + if (value) + value->GetAsDictionary(&dictionary); + } + } + + privet_handler_->HandleRequest( + request->GetPath(), auth_header, dictionary, + base::Bind(&Daemon::PrivetResponseHandler, base::Unretained(this), + base::Passed(&response))); + } + + void PrivetResponseHandler(std::unique_ptr<Response> response, + int status, + const base::DictionaryValue& output) { + VLOG(3) << "status: " << status << ", Output: " << output; + response->ReplyWithJson(status, &output); + } + + void HelloWorldHandler(std::unique_ptr<Request> request, + std::unique_ptr<Response> response) { + response->ReplyWithText(chromeos::http::status_code::Ok, "Hello, world!", + chromeos::mime::text::kPlain); + } + + void OnChanged() { + if (peerd_client_) + peerd_client_->Update(); + } + + void OnConnectivityChanged(bool online) { + OnChanged(); + } + + void OnProtocolHandlerConnected(ProtocolHandler* protocol_handler) { + if (protocol_handler->GetName() == ProtocolHandler::kHttp) { + device_->SetHttpPort(*protocol_handler->GetPorts().begin()); + if (peerd_client_) + peerd_client_->Update(); + } else if (protocol_handler->GetName() == ProtocolHandler::kHttps) { + device_->SetHttpsPort(*protocol_handler->GetPorts().begin()); + security_->SetCertificateFingerprint( + protocol_handler->GetCertificateFingerprint()); + } + } + + void OnProtocolHandlerDisconnected(ProtocolHandler* protocol_handler) { + if (protocol_handler->GetName() == ProtocolHandler::kHttp) { + device_->SetHttpPort(0); + if (peerd_client_) + peerd_client_->Update(); + } else if (protocol_handler->GetName() == ProtocolHandler::kHttps) { + device_->SetHttpsPort(0); + security_->SetCertificateFingerprint({}); + } + } + + bool disable_security_; + bool enable_ping_; + PrivetdConfigParser parser_; + std::set<std::string> device_whitelist_; + base::FilePath config_path_; + std::unique_ptr<DaemonState> state_store_; + std::unique_ptr<CloudDelegate> cloud_; + std::unique_ptr<DeviceDelegate> device_; + std::unique_ptr<SecurityManager> security_; + std::unique_ptr<ShillClient> shill_client_; + std::unique_ptr<ApManagerClient> ap_manager_client_; + std::unique_ptr<WifiBootstrapManager> wifi_bootstrap_manager_; + std::unique_ptr<PeerdClient> peerd_client_; + std::unique_ptr<PrivetHandler> privet_handler_; + ScopedObserver<CloudDelegate, CloudDelegate::Observer> cloud_observer_{this}; + std::unique_ptr<DBusManager> dbus_manager_; + libwebserv::Server web_server_; + + base::WeakPtrFactory<Daemon> weak_ptr_factory_{this}; + DISALLOW_COPY_AND_ASSIGN(Daemon); +}; + +} // anonymous namespace + +} // namespace privetd + +int main(int argc, char* argv[]) { + DEFINE_bool(disable_security, false, "disable Privet security for tests"); + DEFINE_bool(enable_ping, false, "enable test HTTP handler at /privet/ping"); + DEFINE_bool(log_to_stderr, false, "log trace messages to stderr as well"); + DEFINE_string(config_path, privetd::kDefaultConfigFilePath, + "Path to file containing config information."); + DEFINE_string(state_path, privetd::kDefaultStateFilePath, + "Path to file containing state information."); + DEFINE_string(device_whitelist, "", + "Comma separated list of network interfaces to monitor for " + "connectivity (an empty list enables all interfaces)."); + + chromeos::FlagHelper::Init(argc, argv, "Privet protocol handler daemon"); + + int flags = chromeos::kLogToSyslog; + if (FLAGS_log_to_stderr) + flags |= chromeos::kLogToStderr; + chromeos::InitLog(flags | chromeos::kLogHeader); + + if (FLAGS_config_path.empty()) + FLAGS_config_path = privetd::kDefaultConfigFilePath; + + if (FLAGS_state_path.empty()) + FLAGS_state_path = privetd::kDefaultStateFilePath; + + auto device_whitelist = + chromeos::string_utils::Split(FLAGS_device_whitelist, ",", true, true); + + privetd::Daemon daemon( + FLAGS_disable_security, FLAGS_enable_ping, + std::set<std::string>(device_whitelist.begin(), device_whitelist.end()), + base::FilePath(FLAGS_config_path), base::FilePath(FLAGS_state_path)); + return daemon.Run(); +}
diff --git a/buffet/privet/mock_delegates.h b/buffet/privet/mock_delegates.h new file mode 100644 index 0000000..ff8c11b --- /dev/null +++ b/buffet/privet/mock_delegates.h
@@ -0,0 +1,229 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_MOCK_DELEGATES_H_ +#define BUFFET_PRIVET_MOCK_DELEGATES_H_ + +#include <set> +#include <string> +#include <utility> + +#include <base/values.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "buffet/privet/cloud_delegate.h" +#include "buffet/privet/device_delegate.h" +#include "buffet/privet/identity_delegate.h" +#include "buffet/privet/security_delegate.h" +#include "buffet/privet/wifi_delegate.h" + +using testing::_; +using testing::Return; +using testing::ReturnRef; +using testing::SetArgPointee; + +namespace privetd { + +ACTION_TEMPLATE(RunCallback, + HAS_1_TEMPLATE_PARAMS(int, k), + AND_0_VALUE_PARAMS()) { + return std::get<k>(args).Run(); +} + +ACTION_TEMPLATE(RunCallback, + HAS_1_TEMPLATE_PARAMS(int, k), + AND_1_VALUE_PARAMS(p0)) { + return std::get<k>(args).Run(p0); +} + +class MockDeviceDelegate : public DeviceDelegate { + using IntPair = std::pair<uint16_t, uint16_t>; + + public: + MOCK_CONST_METHOD0(GetHttpEnpoint, IntPair()); + MOCK_CONST_METHOD0(GetHttpsEnpoint, IntPair()); + MOCK_CONST_METHOD0(GetUptime, base::TimeDelta()); + MOCK_METHOD1(SetHttpPort, void(uint16_t)); + MOCK_METHOD1(SetHttpsPort, void(uint16_t)); + + MockDeviceDelegate() { + EXPECT_CALL(*this, GetHttpEnpoint()) + .WillRepeatedly(Return(std::make_pair(0, 0))); + EXPECT_CALL(*this, GetHttpsEnpoint()) + .WillRepeatedly(Return(std::make_pair(0, 0))); + EXPECT_CALL(*this, GetUptime()) + .WillRepeatedly(Return(base::TimeDelta::FromHours(1))); + } +}; + +class MockSecurityDelegate : public SecurityDelegate { + public: + MOCK_METHOD2(CreateAccessToken, + std::string(const UserInfo&, const base::Time&)); + MOCK_CONST_METHOD2(ParseAccessToken, + UserInfo(const std::string&, base::Time*)); + MOCK_CONST_METHOD0(GetPairingTypes, std::set<PairingType>()); + MOCK_CONST_METHOD0(GetCryptoTypes, std::set<CryptoType>()); + MOCK_CONST_METHOD1(IsValidPairingCode, bool(const std::string&)); + MOCK_METHOD5(StartPairing, + bool(PairingType, + CryptoType, + std::string*, + std::string*, + chromeos::ErrorPtr*)); + MOCK_METHOD5(ConfirmPairing, + bool(const std::string&, + const std::string&, + std::string*, + std::string*, + chromeos::ErrorPtr*)); + MOCK_METHOD2(CancelPairing, bool(const std::string&, chromeos::ErrorPtr*)); + + MockSecurityDelegate() { + EXPECT_CALL(*this, CreateAccessToken(_, _)) + .WillRepeatedly(Return("GuestAccessToken")); + + EXPECT_CALL(*this, ParseAccessToken(_, _)) + .WillRepeatedly(DoAll(SetArgPointee<1>(base::Time::Now()), + Return(UserInfo{AuthScope::kViewer, 1234567}))); + + EXPECT_CALL(*this, GetPairingTypes()) + .WillRepeatedly(Return(std::set<PairingType>{ + PairingType::kPinCode, + PairingType::kEmbeddedCode, + PairingType::kUltrasound32, + PairingType::kAudible32, + })); + + EXPECT_CALL(*this, GetCryptoTypes()) + .WillRepeatedly(Return(std::set<CryptoType>{ + CryptoType::kSpake_p224, CryptoType::kSpake_p256, + })); + + EXPECT_CALL(*this, StartPairing(_, _, _, _, _)) + .WillRepeatedly(DoAll(SetArgPointee<2>("testSession"), + SetArgPointee<3>("testCommitment"), + Return(true))); + + EXPECT_CALL(*this, ConfirmPairing(_, _, _, _, _)) + .WillRepeatedly(DoAll(SetArgPointee<2>("testFingerprint"), + SetArgPointee<3>("testSignature"), Return(true))); + EXPECT_CALL(*this, CancelPairing(_, _)).WillRepeatedly(Return(true)); + } +}; + +class MockWifiDelegate : public WifiDelegate { + public: + MOCK_CONST_METHOD0(GetConnectionState, const ConnectionState&()); + MOCK_CONST_METHOD0(GetSetupState, const SetupState&()); + MOCK_METHOD3(ConfigureCredentials, + bool(const std::string&, + const std::string&, + chromeos::ErrorPtr*)); + MOCK_CONST_METHOD0(GetCurrentlyConnectedSsid, std::string()); + MOCK_CONST_METHOD0(GetHostedSsid, std::string()); + MOCK_CONST_METHOD0(GetTypes, std::set<WifiType>()); + + MockWifiDelegate() { + EXPECT_CALL(*this, GetConnectionState()) + .WillRepeatedly(ReturnRef(connection_state_)); + EXPECT_CALL(*this, GetSetupState()).WillRepeatedly(ReturnRef(setup_state_)); + EXPECT_CALL(*this, GetCurrentlyConnectedSsid()) + .WillRepeatedly(Return("TestSsid")); + EXPECT_CALL(*this, GetHostedSsid()).WillRepeatedly(Return("")); + EXPECT_CALL(*this, GetTypes()) + .WillRepeatedly(Return(std::set<WifiType>{WifiType::kWifi24})); + } + + ConnectionState connection_state_{ConnectionState::kOffline}; + SetupState setup_state_{SetupState::kNone}; +}; + +class MockCloudDelegate : public CloudDelegate { + public: + MOCK_CONST_METHOD2(GetModelId, bool(std::string*, chromeos::ErrorPtr*)); + MOCK_CONST_METHOD2(GetName, bool(std::string*, chromeos::ErrorPtr*)); + MOCK_CONST_METHOD0(GetDescription, std::string()); + MOCK_CONST_METHOD0(GetLocation, std::string()); + MOCK_METHOD5(UpdateDeviceInfo, + void(const std::string&, + const std::string&, + const std::string&, + const base::Closure&, + const ErrorCallback&)); + MOCK_CONST_METHOD0(GetOemName, std::string()); + MOCK_CONST_METHOD0(GetModelName, std::string()); + MOCK_CONST_METHOD0(GetServices, std::set<std::string>()); + MOCK_CONST_METHOD0(GetAnonymousMaxScope, AuthScope()); + MOCK_CONST_METHOD0(GetConnectionState, const ConnectionState&()); + MOCK_CONST_METHOD0(GetSetupState, const SetupState&()); + MOCK_METHOD3(Setup, + bool(const std::string&, + const std::string&, + chromeos::ErrorPtr*)); + MOCK_CONST_METHOD0(GetCloudId, std::string()); + MOCK_CONST_METHOD0(GetState, const base::DictionaryValue&()); + MOCK_CONST_METHOD0(GetCommandDef, const base::DictionaryValue&()); + MOCK_METHOD4(AddCommand, + void(const base::DictionaryValue&, + const UserInfo&, + const SuccessCallback&, + const ErrorCallback&)); + MOCK_METHOD4(GetCommand, + void(const std::string&, + const UserInfo&, + const SuccessCallback&, + const ErrorCallback&)); + MOCK_METHOD4(CancelCommand, + void(const std::string&, + const UserInfo&, + const SuccessCallback&, + const ErrorCallback&)); + MOCK_METHOD3(ListCommands, + void(const UserInfo&, + const SuccessCallback&, + const ErrorCallback&)); + + MockCloudDelegate() { + EXPECT_CALL(*this, GetModelId(_, _)) + .WillRepeatedly(DoAll(SetArgPointee<0>("ABMID"), Return(true))); + EXPECT_CALL(*this, GetName(_, _)) + .WillRepeatedly(DoAll(SetArgPointee<0>("TestDevice"), Return(true))); + EXPECT_CALL(*this, GetDescription()).WillRepeatedly(Return("")); + EXPECT_CALL(*this, GetLocation()).WillRepeatedly(Return("")); + EXPECT_CALL(*this, UpdateDeviceInfo(_, _, _, _, _)) + .WillRepeatedly(RunCallback<3>()); + EXPECT_CALL(*this, GetOemName()).WillRepeatedly(Return("Chromium")); + EXPECT_CALL(*this, GetModelName()).WillRepeatedly(Return("Brillo")); + EXPECT_CALL(*this, GetServices()) + .WillRepeatedly(Return(std::set<std::string>{})); + EXPECT_CALL(*this, GetAnonymousMaxScope()) + .WillRepeatedly(Return(AuthScope::kUser)); + EXPECT_CALL(*this, GetConnectionState()) + .WillRepeatedly(ReturnRef(connection_state_)); + EXPECT_CALL(*this, GetSetupState()).WillRepeatedly(ReturnRef(setup_state_)); + EXPECT_CALL(*this, GetCloudId()).WillRepeatedly(Return("TestCloudId")); + test_dict_.Set("test", new base::DictionaryValue); + EXPECT_CALL(*this, GetCommandDef()).WillRepeatedly(ReturnRef(test_dict_)); + EXPECT_CALL(*this, GetState()).WillRepeatedly(ReturnRef(test_dict_)); + } + + ConnectionState connection_state_{ConnectionState::kOnline}; + SetupState setup_state_{SetupState::kNone}; + base::DictionaryValue test_dict_; +}; + +class MockIdentityDelegate : public IdentityDelegate { + public: + MOCK_CONST_METHOD0(GetId, std::string()); + + MockIdentityDelegate() { + EXPECT_CALL(*this, GetId()).WillRepeatedly(Return("TestId")); + } +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_MOCK_DELEGATES_H_
diff --git a/buffet/privet/openssl_utils.cc b/buffet/privet/openssl_utils.cc new file mode 100644 index 0000000..17cc16f --- /dev/null +++ b/buffet/privet/openssl_utils.cc
@@ -0,0 +1,26 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/openssl_utils.h" + +#include <algorithm> + +#include <openssl/evp.h> +#include <openssl/hmac.h> + +#include <base/logging.h> + +namespace privetd { + +chromeos::Blob HmacSha256(const chromeos::SecureBlob& key, + const chromeos::Blob& data) { + chromeos::Blob mac(kSha256OutputSize); + uint32_t len = 0; + CHECK(HMAC(EVP_sha256(), key.data(), key.size(), data.data(), + data.size(), mac.data(), &len)); + CHECK_EQ(len, kSha256OutputSize); + return mac; +} + +} // namespace privetd
diff --git a/buffet/privet/openssl_utils.h b/buffet/privet/openssl_utils.h new file mode 100644 index 0000000..587b60c --- /dev/null +++ b/buffet/privet/openssl_utils.h
@@ -0,0 +1,22 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_OPENSSL_UTILS_H_ +#define BUFFET_PRIVET_OPENSSL_UTILS_H_ + +#include <string> +#include <vector> + +#include <chromeos/secure_blob.h> + +namespace privetd { + +const size_t kSha256OutputSize = 32; + +chromeos::Blob HmacSha256(const chromeos::SecureBlob& key, + const chromeos::Blob& data); + +} // namespace privetd + +#endif // BUFFET_PRIVET_OPENSSL_UTILS_H_
diff --git a/buffet/privet/peerd_client.cc b/buffet/privet/peerd_client.cc new file mode 100644 index 0000000..76f88ce --- /dev/null +++ b/buffet/privet/peerd_client.cc
@@ -0,0 +1,170 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/peerd_client.h" + +#include <map> + +#include <base/message_loop/message_loop.h> +#include <chromeos/errors/error.h> +#include <chromeos/strings/string_utils.h> + +#include "buffet/privet/cloud_delegate.h" +#include "buffet/privet/device_delegate.h" +#include "buffet/privet/wifi_bootstrap_manager.h" +#include "buffet/privet/wifi_ssid_generator.h" + +using chromeos::string_utils::Join; +using org::chromium::peerd::PeerProxy; + +namespace privetd { + +namespace { + +// Commit changes only if no update request happened during the timeout. +// Usually updates happen in batches, so we don't want to flood network with +// updates relevant for a short amount of time. +const int kCommitTimeoutSeconds = 1; + +// The name of the service we'll expose via peerd. +const char kPrivetServiceId[] = "privet"; +const char kSelfPath[] = "/org/chromium/peerd/Self"; + +void OnError(const std::string& operation, chromeos::Error* error) { + LOG(ERROR) << operation << " failed:" << error->GetMessage(); +} + +} // namespace + +PeerdClient::PeerdClient(const scoped_refptr<dbus::Bus>& bus, + const DeviceDelegate* device, + const CloudDelegate* cloud, + const WifiDelegate* wifi) + : peerd_object_manager_proxy_{bus}, + device_{device}, + cloud_{cloud}, + wifi_{wifi} { + CHECK(device_); + CHECK(cloud_); + peerd_object_manager_proxy_.SetManagerAddedCallback( + base::Bind(&PeerdClient::OnPeerdOnline, weak_ptr_factory_.GetWeakPtr())); + peerd_object_manager_proxy_.SetManagerRemovedCallback( + base::Bind(&PeerdClient::OnPeerdOffline, weak_ptr_factory_.GetWeakPtr())); + peerd_object_manager_proxy_.SetPeerAddedCallback( + base::Bind(&PeerdClient::OnNewPeer, weak_ptr_factory_.GetWeakPtr())); +} + +PeerdClient::~PeerdClient() { + RemoveService(); +} + +std::string PeerdClient::GetId() const { + return device_id_; +} + +void PeerdClient::Update() { + // Abort pending updates, and wait for more changes. + restart_weak_ptr_factory_.InvalidateWeakPtrs(); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&PeerdClient::UpdateImpl, + restart_weak_ptr_factory_.GetWeakPtr()), + base::TimeDelta::FromSeconds(kCommitTimeoutSeconds)); +} + +void PeerdClient::OnNewPeer(PeerProxy* peer) { + if (!peer || peer->GetObjectPath().value() != kSelfPath) + return; + peer->SetPropertyChangedCallback( + base::Bind(&PeerdClient::OnPeerPropertyChanged, + weak_ptr_factory_.GetWeakPtr())); + OnPeerPropertyChanged(peer, PeerProxy::UUIDName()); +} + +void PeerdClient::OnPeerPropertyChanged( + PeerProxy* peer, + const std::string& property_name) { + if (property_name != PeerProxy::UUIDName() || + peer->GetObjectPath().value() != kSelfPath) + return; + const std::string new_id{peer->uuid()}; + if (new_id != device_id_) { + device_id_ = new_id; + Update(); + } +} + +void PeerdClient::OnPeerdOnline( + org::chromium::peerd::ManagerProxy* manager_proxy) { + peerd_manager_proxy_ = manager_proxy; + VLOG(1) << "Peerd manager is online at '" + << manager_proxy->GetObjectPath().value() << "'."; + Update(); +} + +void PeerdClient::OnPeerdOffline(const dbus::ObjectPath& object_path) { + peerd_manager_proxy_ = nullptr; + VLOG(1) << "Peerd manager is now offline."; +} + +void PeerdClient::ExposeService() { + // Do nothing if peerd hasn't started yet. + if (peerd_manager_proxy_ == nullptr) + return; + + std::string name; + std::string model_id; + if (!cloud_->GetName(&name, nullptr) || + !cloud_->GetModelId(&model_id, nullptr)) { + return; + } + DCHECK_EQ(model_id.size(), 5U); + + VLOG(1) << "Starting peerd advertising."; + const uint16_t port = device_->GetHttpEnpoint().first; + std::map<std::string, chromeos::Any> mdns_options{ + {"port", chromeos::Any{port}}, + }; + DCHECK_NE(port, 0); + + std::string services; + if (!cloud_->GetServices().empty()) + services += "_"; + services += Join(",_", cloud_->GetServices()); + + std::map<std::string, std::string> txt_record{ + {"txtvers", "3"}, + {"ty", name}, + {"services", services}, + {"id", GetId()}, + {"mmid", model_id}, + {"flags", WifiSsidGenerator{cloud_, wifi_}.GenerateFlags()}, + }; + + if (!cloud_->GetCloudId().empty()) + txt_record.emplace("gcd_id", cloud_->GetCloudId()); + + if (!cloud_->GetDescription().empty()) + txt_record.emplace("note", cloud_->GetDescription()); + + peerd_manager_proxy_->ExposeServiceAsync( + kPrivetServiceId, txt_record, {{"mdns", mdns_options}}, base::Closure(), + base::Bind(&OnError, "ExposeService")); +} + +void PeerdClient::RemoveService() { + if (peerd_manager_proxy_ == nullptr) + return; + + VLOG(1) << "Stopping peerd advertising."; + peerd_manager_proxy_->RemoveExposedServiceAsync( + kPrivetServiceId, base::Closure(), base::Bind(&OnError, "RemoveService")); +} + +void PeerdClient::UpdateImpl() { + if (device_->GetHttpEnpoint().first == 0) + return RemoveService(); + ExposeService(); +} + +} // namespace privetd
diff --git a/buffet/privet/peerd_client.h b/buffet/privet/peerd_client.h new file mode 100644 index 0000000..ae3d306 --- /dev/null +++ b/buffet/privet/peerd_client.h
@@ -0,0 +1,72 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_PEERD_CLIENT_H_ +#define BUFFET_PRIVET_PEERD_CLIENT_H_ + +#include <memory> +#include <string> + +#include <base/callback.h> +#include <base/memory/ref_counted.h> + +#include "buffet/privet/identity_delegate.h" +#include "peerd/dbus-proxies.h" + +namespace dbus { +class Bus; +} // namespace dbus + +namespace privetd { + +class CloudDelegate; +class DeviceDelegate; +class WifiDelegate; + +// Publishes prived service on mDns using peerd. +class PeerdClient : public IdentityDelegate { + public: + PeerdClient(const scoped_refptr<dbus::Bus>& bus, + const DeviceDelegate* device, + const CloudDelegate* cloud, + const WifiDelegate* wifi); + ~PeerdClient(); + + // Get the unique identifier for this device. Note that if peerd has + // never been seen, this may be the empty string. + std::string GetId() const override; + + // Updates published information. Removes service if HTTP is not alive. + void Update(); + + private: + void OnPeerdOnline(org::chromium::peerd::ManagerProxy* manager_proxy); + void OnPeerdOffline(const dbus::ObjectPath& object_path); + void OnNewPeer(org::chromium::peerd::PeerProxy* peer_proxy); + void OnPeerPropertyChanged(org::chromium::peerd::PeerProxy* peer_proxy, + const std::string& property_name); + + void ExposeService(); + void RemoveService(); + + void UpdateImpl(); + + org::chromium::peerd::ObjectManagerProxy peerd_object_manager_proxy_; + // |peerd_manager_proxy_| is owned by |peerd_object_manager_proxy_|. + org::chromium::peerd::ManagerProxy* peerd_manager_proxy_{nullptr}; + + const DeviceDelegate* device_{nullptr}; + const CloudDelegate* cloud_{nullptr}; + const WifiDelegate* wifi_{nullptr}; + + // Cached value of the device ID that we got from peerd. + std::string device_id_; + + base::WeakPtrFactory<PeerdClient> restart_weak_ptr_factory_{this}; + base::WeakPtrFactory<PeerdClient> weak_ptr_factory_{this}; +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_PEERD_CLIENT_H_
diff --git a/buffet/privet/privet_handler.cc b/buffet/privet/privet_handler.cc new file mode 100644 index 0000000..5c3d0c2 --- /dev/null +++ b/buffet/privet/privet_handler.cc
@@ -0,0 +1,948 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/privet_handler.h" + +#include <memory> +#include <set> +#include <string> +#include <utility> + +#include <base/bind.h> +#include <base/location.h> +#include <base/stl_util.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/stringprintf.h> +#include <base/values.h> +#include <chromeos/http/http_request.h> +#include <chromeos/strings/string_utils.h> + +#include "buffet/privet/cloud_delegate.h" +#include "buffet/privet/constants.h" +#include "buffet/privet/device_delegate.h" +#include "buffet/privet/identity_delegate.h" +#include "buffet/privet/security_delegate.h" +#include "buffet/privet/wifi_delegate.h" + +namespace privetd { + +namespace { + +const char kInfoVersionKey[] = "version"; +const char kInfoVersionValue[] = "3.0"; + +const char kNameKey[] = "name"; +const char kDescrptionKey[] = "description"; +const char kLocationKey[] = "location"; + +const char kGcdKey[] = "gcd"; +const char kWifiKey[] = "wifi"; +const char kStatusKey[] = "status"; +const char kErrorKey[] = "error"; +const char kCryptoKey[] = "crypto"; +const char kStatusErrorValue[] = "error"; + +const char kInfoIdKey[] = "id"; +const char kInfoServicesKey[] = "services"; + +const char kInfoEndpointsKey[] = "endpoints"; +const char kInfoEndpointsHttpPortKey[] = "httpPort"; +const char kInfoEndpointsHttpUpdatePortKey[] = "httpUpdatesPort"; +const char kInfoEndpointsHttpsPortKey[] = "httpsPort"; +const char kInfoEndpointsHttpsUpdatePortKey[] = "httpsUpdatesPort"; + +const char kInfoModelIdKey[] = "modelManifestId"; +const char kInfoModelManifestKey[] = "basicModelManifest"; +const char kInfoManifestUiDeviceKind[] = "uiDeviceKind"; +const char kInfoManifestOemName[] = "oemName"; +const char kInfoManifestModelName[] = "modelName"; + +const char kInfoAuthenticationKey[] = "authentication"; + +const char kInfoAuthAnonymousMaxScopeKey[] = "anonymousMaxScope"; + +const char kInfoWifiCapabilitiesKey[] = "capabilities"; +const char kInfoWifiSsidKey[] = "ssid"; +const char kInfoWifiHostedSsidKey[] = "hostedSsid"; + +const char kInfoUptimeKey[] = "uptime"; + +const char kPairingKey[] = "pairing"; +const char kPairingSessionIdKey[] = "sessionId"; +const char kPairingDeviceCommitmentKey[] = "deviceCommitment"; +const char kPairingClientCommitmentKey[] = "clientCommitment"; +const char kPairingFingerprintKey[] = "certFingerprint"; +const char kPairingSignatureKey[] = "certSignature"; + +const char kAuthTypeAnonymousValue[] = "anonymous"; +const char kAuthTypePairingValue[] = "pairing"; + +const char kAuthModeKey[] = "mode"; +const char kAuthCodeKey[] = "authCode"; +const char kAuthRequestedScopeKey[] = "requestedScope"; +const char kAuthScopeAutoValue[] = "auto"; + +const char kAuthAccessTokenKey[] = "accessToken"; +const char kAuthTokenTypeKey[] = "tokenType"; +const char kAuthExpiresInKey[] = "expiresIn"; +const char kAuthScopeKey[] = "scope"; + +const char kAuthorizationHeaderPrefix[] = "Privet"; + +const char kErrorCodeKey[] = "code"; +const char kErrorMessageKey[] = "message"; +const char kErrorDebugInfoKey[] = "debugInfo"; + +const char kSetupStartSsidKey[] = "ssid"; +const char kSetupStartPassKey[] = "passphrase"; +const char kSetupStartTicketIdKey[] = "ticketId"; +const char kSetupStartUserKey[] = "user"; + +const char kFingerprintKey[] = "fingerprint"; +const char kStateKey[] = "state"; +const char kCommandsKey[] = "commands"; +const char kCommandsIdKey[] = "id"; + +const char kInvalidParamValueFormat[] = "Invalid parameter: '%s'='%s'"; + +const int kAccessTokenExpirationSeconds = 3600; + +// Threshold to reduce probability of expiration because of clock difference +// between device and client. Value is just a guess. +const int kAccessTokenExpirationThresholdSeconds = 300; + +template <class Container> +std::unique_ptr<base::ListValue> ToValue(const Container& list) { + std::unique_ptr<base::ListValue> value_list(new base::ListValue()); + for (const std::string& val : list) + value_list->AppendString(val); + return value_list; +} + +template <typename T> +class EnumToStringMap final { + public: + static std::string FindNameById(T id) { + for (const Map& m : kMap) { + if (m.id == id) { + CHECK(m.name); + return m.name; + } + } + NOTREACHED() << static_cast<int>(id) << " is not part of " + << typeid(T).name(); + return std::string(); + } + + static bool FindIdByName(const std::string& name, T* id) { + for (const Map& m : kMap) { + if (m.name && m.name == name) { + *id = m.id; + return true; + } + } + return false; + } + + private: + struct Map { + const T id; + const char* const name; + }; + static const Map kMap[]; +}; + +template <> +const EnumToStringMap<ConnectionState::Status>::Map + EnumToStringMap<ConnectionState::Status>::kMap[] = { + {ConnectionState::kDisabled, "disabled"}, + {ConnectionState::kUnconfigured, "unconfigured"}, + {ConnectionState::kConnecting, "connecting"}, + {ConnectionState::kOnline, "online"}, + {ConnectionState::kOffline, "offline"}, +}; + +template <> +const EnumToStringMap<SetupState::Status>::Map + EnumToStringMap<SetupState::Status>::kMap[] = { + {SetupState::kNone, nullptr}, + {SetupState::kInProgress, "inProgress"}, + {SetupState::kSuccess, "success"}, +}; + +template <> +const EnumToStringMap<WifiType>::Map EnumToStringMap<WifiType>::kMap[] = { + {WifiType::kWifi24, "2.4GHz"}, + {WifiType::kWifi50, "5.0GHz"}, +}; + +template <> +const EnumToStringMap<PairingType>::Map EnumToStringMap<PairingType>::kMap[] = { + {PairingType::kPinCode, "pinCode"}, + {PairingType::kEmbeddedCode, "embeddedCode"}, + {PairingType::kUltrasound32, "ultrasound32"}, + {PairingType::kAudible32, "audible32"}, +}; + +template <> +const EnumToStringMap<CryptoType>::Map EnumToStringMap<CryptoType>::kMap[] = { + {CryptoType::kNone, "none"}, + {CryptoType::kSpake_p224, "p224_spake2"}, + {CryptoType::kSpake_p256, "p256_spake2"}, +}; + +template <> +const EnumToStringMap<AuthScope>::Map EnumToStringMap<AuthScope>::kMap[] = { + {AuthScope::kNone, "none"}, + {AuthScope::kViewer, "viewer"}, + {AuthScope::kUser, "user"}, + {AuthScope::kOwner, "owner"}, +}; + +struct { + const char* const reason; + int code; +} kReasonToCode[] = { + {errors::kInvalidClientCommitment, chromeos::http::status_code::Forbidden}, + {errors::kInvalidFormat, chromeos::http::status_code::BadRequest}, + {errors::kMissingAuthorization, chromeos::http::status_code::Denied}, + {errors::kInvalidAuthorization, chromeos::http::status_code::Denied}, + {errors::kInvalidAuthorizationScope, + chromeos::http::status_code::Forbidden}, + {errors::kAuthorizationExpired, chromeos::http::status_code::Forbidden}, + {errors::kCommitmentMismatch, chromeos::http::status_code::Forbidden}, + {errors::kUnknownSession, chromeos::http::status_code::NotFound}, + {errors::kInvalidAuthCode, chromeos::http::status_code::Forbidden}, + {errors::kInvalidAuthMode, chromeos::http::status_code::BadRequest}, + {errors::kInvalidRequestedScope, chromeos::http::status_code::BadRequest}, + {errors::kAccessDenied, chromeos::http::status_code::Forbidden}, + {errors::kInvalidParams, chromeos::http::status_code::BadRequest}, + {errors::kSetupUnavailable, chromeos::http::status_code::BadRequest}, + {errors::kDeviceBusy, chromeos::http::status_code::ServiceUnavailable}, + {errors::kInvalidState, chromeos::http::status_code::InternalServerError}, + {errors::kNotFound, chromeos::http::status_code::NotFound}, + {errors::kNotImplemented, chromeos::http::status_code::NotSupported}, +}; + +template <typename T> +std::string EnumToString(T id) { + return EnumToStringMap<T>::FindNameById(id); +} + +template <typename T> +bool StringToEnum(const std::string& name, T* id) { + return EnumToStringMap<T>::FindIdByName(name, id); +} + +AuthScope AuthScopeFromString(const std::string& scope, AuthScope auto_scope) { + if (scope == kAuthScopeAutoValue) + return auto_scope; + AuthScope scope_id = AuthScope::kNone; + StringToEnum(scope, &scope_id); + return scope_id; +} + +std::string GetAuthTokenFromAuthHeader(const std::string& auth_header) { + std::string name; + std::string value; + chromeos::string_utils::SplitAtFirst(auth_header, " ", &name, &value); + return value; +} + +std::unique_ptr<base::DictionaryValue> ErrorInfoToJson( + const chromeos::Error& error) { + std::unique_ptr<base::DictionaryValue> output{new base::DictionaryValue}; + output->SetString(kErrorMessageKey, error.GetMessage()); + output->SetString(kErrorCodeKey, error.GetCode()); + return output; +} + +// Creates JSON similar to GCD server error format. +std::unique_ptr<base::DictionaryValue> ErrorToJson( + const chromeos::Error& error) { + std::unique_ptr<base::DictionaryValue> output{ErrorInfoToJson(error)}; + + // Optional debug information. + std::unique_ptr<base::ListValue> errors{new base::ListValue}; + for (const chromeos::Error* it = &error; it; it = it->GetInnerError()) { + std::unique_ptr<base::DictionaryValue> inner{ErrorInfoToJson(*it)}; + tracked_objects::Location location{it->GetLocation().function_name.c_str(), + it->GetLocation().file_name.c_str(), + it->GetLocation().line_number, + nullptr}; + inner->SetString(kErrorDebugInfoKey, location.ToString()); + errors->Append(inner.release()); + } + output->Set(kErrorDebugInfoKey, errors.release()); + return output; +} + +template <class T> +void SetState(const T& state, base::DictionaryValue* parent) { + if (!state.error()) { + parent->SetString(kStatusKey, EnumToString(state.status())); + return; + } + parent->SetString(kStatusKey, kStatusErrorValue); + parent->Set(kErrorKey, ErrorToJson(*state.error()).release()); +} + +void ReturnError(const chromeos::Error& error, + const PrivetHandler::RequestCallback& callback) { + int code = chromeos::http::status_code::InternalServerError; + for (const auto& it : kReasonToCode) { + if (error.HasError(errors::kDomain, it.reason)) { + code = it.code; + break; + } + } + std::unique_ptr<base::DictionaryValue> output{new base::DictionaryValue}; + output->Set(kErrorKey, ErrorToJson(error).release()); + callback.Run(code, *output); +} + +void OnCommandRequestSucceeded(const PrivetHandler::RequestCallback& callback, + const base::DictionaryValue& output) { + callback.Run(chromeos::http::status_code::Ok, output); +} + +void OnCommandRequestFailed(const PrivetHandler::RequestCallback& callback, + chromeos::Error* error) { + if (error->HasError("gcd", "unknown_command")) { + chromeos::ErrorPtr new_error = error->Clone(); + chromeos::Error::AddTo(&new_error, FROM_HERE, errors::kDomain, + errors::kNotFound, "Unknown command ID"); + return ReturnError(*new_error, callback); + } + if (error->HasError("gcd", "access_denied")) { + chromeos::ErrorPtr new_error = error->Clone(); + chromeos::Error::AddTo(&new_error, FROM_HERE, errors::kDomain, + errors::kAccessDenied, error->GetMessage()); + return ReturnError(*new_error, callback); + } + return ReturnError(*error, callback); +} + +std::string GetDeviceKind(const std::string& manifest_id) { + CHECK_EQ(5u, manifest_id.size()); + std::string kind = manifest_id.substr(0, 2); + if (kind == "AC") + return "accessPoint"; + if (kind == "AK") + return "aggregator"; + if (kind == "AM") + return "camera"; + if (kind == "AB") + return "developmentBoard"; + if (kind == "AE") + return "printer"; + if (kind == "AF") + return "scanner"; + if (kind == "AD") + return "speaker"; + if (kind == "AL") + return "storage"; + if (kind == "AJ") + return "toy"; + if (kind == "AA") + return "vendor"; + if (kind == "AN") + return "video"; + LOG(FATAL) << "Invalid model id: " << manifest_id; + return std::string(); +} + +std::unique_ptr<base::DictionaryValue> CreateManifestSection( + const std::string& model_id, + const CloudDelegate& cloud) { + std::unique_ptr<base::DictionaryValue> manifest(new base::DictionaryValue()); + manifest->SetString(kInfoManifestUiDeviceKind, GetDeviceKind(model_id)); + manifest->SetString(kInfoManifestOemName, cloud.GetOemName()); + manifest->SetString(kInfoManifestModelName, cloud.GetModelName()); + return manifest; +} + +std::unique_ptr<base::DictionaryValue> CreateEndpointsSection( + const DeviceDelegate& device) { + std::unique_ptr<base::DictionaryValue> endpoints(new base::DictionaryValue()); + auto http_endpoint = device.GetHttpEnpoint(); + endpoints->SetInteger(kInfoEndpointsHttpPortKey, http_endpoint.first); + endpoints->SetInteger(kInfoEndpointsHttpUpdatePortKey, http_endpoint.second); + + auto https_endpoint = device.GetHttpsEnpoint(); + endpoints->SetInteger(kInfoEndpointsHttpsPortKey, https_endpoint.first); + endpoints->SetInteger(kInfoEndpointsHttpsUpdatePortKey, + https_endpoint.second); + + return endpoints; +} + +std::unique_ptr<base::DictionaryValue> CreateInfoAuthSection( + const SecurityDelegate& security, + AuthScope anonymous_max_scope) { + std::unique_ptr<base::DictionaryValue> auth(new base::DictionaryValue()); + + auth->SetString(kInfoAuthAnonymousMaxScopeKey, + EnumToString(anonymous_max_scope)); + + std::unique_ptr<base::ListValue> pairing_types(new base::ListValue()); + for (PairingType type : security.GetPairingTypes()) + pairing_types->AppendString(EnumToString(type)); + auth->Set(kPairingKey, pairing_types.release()); + + std::unique_ptr<base::ListValue> auth_types(new base::ListValue()); + auth_types->AppendString(kAuthTypeAnonymousValue); + auth_types->AppendString(kAuthTypePairingValue); + + // TODO(vitalybuka): Implement cloud auth. + // if (cloud.GetConnectionState().IsStatusEqual(ConnectionState::kOnline)) { + // auth_types->AppendString(kAuthTypeCloudValue); + // } + auth->Set(kAuthModeKey, auth_types.release()); + + std::unique_ptr<base::ListValue> crypto_types(new base::ListValue()); + for (CryptoType type : security.GetCryptoTypes()) + crypto_types->AppendString(EnumToString(type)); + auth->Set(kCryptoKey, crypto_types.release()); + + return auth; +} + +std::unique_ptr<base::DictionaryValue> CreateWifiSection( + const WifiDelegate& wifi) { + std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue()); + + std::unique_ptr<base::ListValue> capabilities(new base::ListValue()); + for (WifiType type : wifi.GetTypes()) + capabilities->AppendString(EnumToString(type)); + result->Set(kInfoWifiCapabilitiesKey, capabilities.release()); + + result->SetString(kInfoWifiSsidKey, wifi.GetCurrentlyConnectedSsid()); + + std::string hosted_ssid = wifi.GetHostedSsid(); + const ConnectionState& state = wifi.GetConnectionState(); + if (!hosted_ssid.empty()) { + DCHECK(!state.IsStatusEqual(ConnectionState::kDisabled)); + DCHECK(!state.IsStatusEqual(ConnectionState::kOnline)); + result->SetString(kInfoWifiHostedSsidKey, hosted_ssid); + } + SetState(state, result.get()); + return result; +} + +std::unique_ptr<base::DictionaryValue> CreateGcdSection( + const CloudDelegate& cloud) { + std::unique_ptr<base::DictionaryValue> gcd(new base::DictionaryValue()); + gcd->SetString(kInfoIdKey, cloud.GetCloudId()); + SetState(cloud.GetConnectionState(), gcd.get()); + return gcd; +} + +AuthScope GetAnonymousMaxScope(const CloudDelegate& cloud, + const WifiDelegate* wifi) { + if (wifi && !wifi->GetHostedSsid().empty()) + return AuthScope::kNone; + return cloud.GetAnonymousMaxScope(); +} + +} // namespace + +PrivetHandler::PrivetHandler(CloudDelegate* cloud, + DeviceDelegate* device, + SecurityDelegate* security, + WifiDelegate* wifi, + IdentityDelegate* identity) + : cloud_(cloud), + device_(device), + security_(security), + wifi_(wifi), + identity_(identity) { + CHECK(cloud_); + CHECK(device_); + CHECK(security_); + cloud_observer_.Add(cloud_); + + AddHandler("/privet/info", &PrivetHandler::HandleInfo, AuthScope::kNone); + AddHandler("/privet/v3/pairing/start", &PrivetHandler::HandlePairingStart, + AuthScope::kNone); + AddHandler("/privet/v3/pairing/confirm", &PrivetHandler::HandlePairingConfirm, + AuthScope::kNone); + AddHandler("/privet/v3/pairing/cancel", &PrivetHandler::HandlePairingCancel, + AuthScope::kNone); + AddHandler("/privet/v3/auth", &PrivetHandler::HandleAuth, AuthScope::kNone); + AddHandler("/privet/v3/setup/start", &PrivetHandler::HandleSetupStart, + AuthScope::kOwner); + AddHandler("/privet/v3/setup/status", &PrivetHandler::HandleSetupStatus, + AuthScope::kOwner); + AddHandler("/privet/v3/state", &PrivetHandler::HandleState, + AuthScope::kViewer); + AddHandler("/privet/v3/commandDefs", &PrivetHandler::HandleCommandDefs, + AuthScope::kViewer); + AddHandler("/privet/v3/commands/execute", + &PrivetHandler::HandleCommandsExecute, AuthScope::kViewer); + AddHandler("/privet/v3/commands/status", &PrivetHandler::HandleCommandsStatus, + AuthScope::kViewer); + AddHandler("/privet/v3/commands/cancel", &PrivetHandler::HandleCommandsCancel, + AuthScope::kViewer); + AddHandler("/privet/v3/commands/list", &PrivetHandler::HandleCommandsList, + AuthScope::kViewer); +} + +PrivetHandler::~PrivetHandler() { +} + +void PrivetHandler::OnCommandDefsChanged() { + ++command_defs_fingerprint_; +} + +void PrivetHandler::OnStateChanged() { + ++state_fingerprint_; +} + +void PrivetHandler::HandleRequest(const std::string& api, + const std::string& auth_header, + const base::DictionaryValue* input, + const RequestCallback& callback) { + chromeos::ErrorPtr error; + if (!input) { + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kInvalidFormat, "Malformed JSON"); + return ReturnError(*error, callback); + } + auto handler = handlers_.find(api); + if (handler == handlers_.end()) { + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kNotFound, "Path not found"); + return ReturnError(*error, callback); + } + if (auth_header.empty()) { + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kMissingAuthorization, + "Authorization header must not be empty"); + return ReturnError(*error, callback); + } + std::string token = GetAuthTokenFromAuthHeader(auth_header); + if (token.empty()) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidAuthorization, + "Invalid authorization header: %s", auth_header.c_str()); + return ReturnError(*error, callback); + } + UserInfo user_info; + if (token != kAuthTypeAnonymousValue) { + base::Time time; + user_info = security_->ParseAccessToken(token, &time); + if (user_info.scope() == AuthScope::kNone) { + chromeos::Error::AddToPrintf(&error, FROM_HERE, errors::kDomain, + errors::kInvalidAuthorization, + "Invalid access token: %s", token.c_str()); + return ReturnError(*error, callback); + } + time += base::TimeDelta::FromSeconds(kAccessTokenExpirationSeconds); + time += + base::TimeDelta::FromSeconds(kAccessTokenExpirationThresholdSeconds); + if (time < base::Time::Now()) { + chromeos::Error::AddToPrintf(&error, FROM_HERE, errors::kDomain, + errors::kAuthorizationExpired, + "Token expired: %s", token.c_str()); + return ReturnError(*error, callback); + } + } + + if (handler->second.first > user_info.scope()) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidAuthorizationScope, + "Scope '%s' does not allow '%s'", + EnumToString(user_info.scope()).c_str(), api.c_str()); + return ReturnError(*error, callback); + } + (this->*handler->second.second)(*input, user_info, callback); +} + +void PrivetHandler::AddHandler(const std::string& path, + ApiHandler handler, + AuthScope scope) { + CHECK(handlers_.emplace(path, std::make_pair(scope, handler)).second); +} + +void PrivetHandler::HandleInfo(const base::DictionaryValue&, + const UserInfo& user_info, + const RequestCallback& callback) { + base::DictionaryValue output; + + chromeos::ErrorPtr error; + + std::string name; + std::string model_id; + if (!cloud_->GetName(&name, &error) || + !cloud_->GetModelId(&model_id, &error)) { + return ReturnError(*error, callback); + } + + output.SetString(kInfoVersionKey, kInfoVersionValue); + output.SetString(kInfoIdKey, identity_->GetId()); + output.SetString(kNameKey, name); + + std::string description{cloud_->GetDescription()}; + if (!description.empty()) + output.SetString(kDescrptionKey, description); + + std::string location{cloud_->GetLocation()}; + if (!location.empty()) + output.SetString(kLocationKey, location); + + output.SetString(kInfoModelIdKey, model_id); + output.Set(kInfoModelManifestKey, + CreateManifestSection(model_id, *cloud_).release()); + output.Set(kInfoServicesKey, ToValue(cloud_->GetServices()).release()); + + output.Set(kInfoAuthenticationKey, + CreateInfoAuthSection( + *security_, GetAnonymousMaxScope(*cloud_, wifi_)).release()); + + output.Set(kInfoEndpointsKey, CreateEndpointsSection(*device_).release()); + + if (wifi_) + output.Set(kWifiKey, CreateWifiSection(*wifi_).release()); + + output.Set(kGcdKey, CreateGcdSection(*cloud_).release()); + + output.SetInteger(kInfoUptimeKey, device_->GetUptime().InSeconds()); + + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandlePairingStart(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + chromeos::ErrorPtr error; + + std::string pairing_str; + input.GetString(kPairingKey, &pairing_str); + + std::string crypto_str; + input.GetString(kCryptoKey, &crypto_str); + + PairingType pairing; + std::set<PairingType> modes = security_->GetPairingTypes(); + if (!StringToEnum(pairing_str, &pairing) || !ContainsKey(modes, pairing)) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidParams, + kInvalidParamValueFormat, kPairingKey, pairing_str.c_str()); + return ReturnError(*error, callback); + } + + CryptoType crypto; + std::set<CryptoType> cryptos = security_->GetCryptoTypes(); + if (!StringToEnum(crypto_str, &crypto) || !ContainsKey(cryptos, crypto)) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidParams, + kInvalidParamValueFormat, kCryptoKey, crypto_str.c_str()); + return ReturnError(*error, callback); + } + + std::string id; + std::string commitment; + if (!security_->StartPairing(pairing, crypto, &id, &commitment, &error)) + return ReturnError(*error, callback); + + base::DictionaryValue output; + output.SetString(kPairingSessionIdKey, id); + output.SetString(kPairingDeviceCommitmentKey, commitment); + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandlePairingConfirm(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + std::string id; + input.GetString(kPairingSessionIdKey, &id); + + std::string commitment; + input.GetString(kPairingClientCommitmentKey, &commitment); + + std::string fingerprint; + std::string signature; + chromeos::ErrorPtr error; + if (!security_->ConfirmPairing(id, commitment, &fingerprint, &signature, + &error)) { + return ReturnError(*error, callback); + } + + base::DictionaryValue output; + output.SetString(kPairingFingerprintKey, fingerprint); + output.SetString(kPairingSignatureKey, signature); + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandlePairingCancel(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + std::string id; + input.GetString(kPairingSessionIdKey, &id); + + chromeos::ErrorPtr error; + if (!security_->CancelPairing(id, &error)) + return ReturnError(*error, callback); + + base::DictionaryValue output; + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandleAuth(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + chromeos::ErrorPtr error; + + std::string auth_code_type; + input.GetString(kAuthModeKey, &auth_code_type); + + std::string auth_code; + input.GetString(kAuthCodeKey, &auth_code); + + AuthScope max_auth_scope = AuthScope::kNone; + if (auth_code_type == kAuthTypeAnonymousValue) { + max_auth_scope = GetAnonymousMaxScope(*cloud_, wifi_); + } else if (auth_code_type == kAuthTypePairingValue) { + if (!security_->IsValidPairingCode(auth_code)) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidAuthCode, + kInvalidParamValueFormat, kAuthCodeKey, auth_code.c_str()); + return ReturnError(*error, callback); + } + max_auth_scope = AuthScope::kOwner; + } else { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidAuthMode, + kInvalidParamValueFormat, kAuthModeKey, auth_code_type.c_str()); + return ReturnError(*error, callback); + } + + std::string requested_scope; + input.GetString(kAuthRequestedScopeKey, &requested_scope); + + AuthScope requested_auth_scope = + AuthScopeFromString(requested_scope, max_auth_scope); + if (requested_auth_scope == AuthScope::kNone) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidRequestedScope, + kInvalidParamValueFormat, kAuthRequestedScopeKey, + requested_scope.c_str()); + return ReturnError(*error, callback); + } + + if (requested_auth_scope > max_auth_scope) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kAccessDenied, + "Scope '%s' is not allowed for '%s'", + EnumToString(requested_auth_scope).c_str(), auth_code.c_str()); + return ReturnError(*error, callback); + } + + base::DictionaryValue output; + output.SetString( + kAuthAccessTokenKey, + security_->CreateAccessToken( + UserInfo{requested_auth_scope, ++last_user_id_}, base::Time::Now())); + output.SetString(kAuthTokenTypeKey, kAuthorizationHeaderPrefix); + output.SetInteger(kAuthExpiresInKey, kAccessTokenExpirationSeconds); + output.SetString(kAuthScopeKey, EnumToString(requested_auth_scope)); + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandleSetupStart(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + std::string name; + chromeos::ErrorPtr error; + if (!cloud_->GetName(&name, &error)) + return ReturnError(*error, callback); + input.GetString(kNameKey, &name); + + std::string description{cloud_->GetDescription()}; + input.GetString(kDescrptionKey, &description); + + std::string location{cloud_->GetLocation()}; + input.GetString(kLocationKey, &location); + + std::string ssid; + std::string passphrase; + std::string ticket; + std::string user; + + const base::DictionaryValue* wifi = nullptr; + if (input.GetDictionary(kWifiKey, &wifi)) { + if (!wifi_ || wifi_->GetTypes().empty()) { + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kSetupUnavailable, + "WiFi setup unavailible"); + return ReturnError(*error, callback); + } + wifi->GetString(kSetupStartSsidKey, &ssid); + if (ssid.empty()) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidParams, + kInvalidParamValueFormat, kSetupStartSsidKey, ""); + return ReturnError(*error, callback); + } + wifi->GetString(kSetupStartPassKey, &passphrase); + } + + const base::DictionaryValue* registration = nullptr; + if (input.GetDictionary(kGcdKey, ®istration)) { + registration->GetString(kSetupStartTicketIdKey, &ticket); + if (ticket.empty()) { + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidParams, + kInvalidParamValueFormat, kSetupStartTicketIdKey, ""); + return ReturnError(*error, callback); + } + registration->GetString(kSetupStartUserKey, &user); + } + + cloud_->UpdateDeviceInfo(name, description, location, + base::Bind(&PrivetHandler::OnUpdateDeviceInfoDone, + weak_ptr_factory_.GetWeakPtr(), ssid, + passphrase, ticket, user, callback), + base::Bind(&OnCommandRequestFailed, callback)); +} + +void PrivetHandler::OnUpdateDeviceInfoDone( + const std::string& ssid, + const std::string& passphrase, + const std::string& ticket, + const std::string& user, + const RequestCallback& callback) const { + chromeos::ErrorPtr error; + + if (!ssid.empty() && !wifi_->ConfigureCredentials(ssid, passphrase, &error)) + return ReturnError(*error, callback); + + if (!ticket.empty() && !cloud_->Setup(ticket, user, &error)) + return ReturnError(*error, callback); + + ReplyWithSetupStatus(callback); +} + +void PrivetHandler::HandleSetupStatus(const base::DictionaryValue&, + const UserInfo& user_info, + const RequestCallback& callback) { + ReplyWithSetupStatus(callback); +} + +void PrivetHandler::ReplyWithSetupStatus( + const RequestCallback& callback) const { + base::DictionaryValue output; + + const SetupState& state = cloud_->GetSetupState(); + if (!state.IsStatusEqual(SetupState::kNone)) { + base::DictionaryValue* gcd = new base::DictionaryValue; + output.Set(kGcdKey, gcd); + SetState(state, gcd); + if (state.IsStatusEqual(SetupState::kSuccess)) + gcd->SetString(kInfoIdKey, cloud_->GetCloudId()); + } + + if (wifi_) { + const SetupState& state = wifi_->GetSetupState(); + if (!state.IsStatusEqual(SetupState::kNone)) { + base::DictionaryValue* wifi = new base::DictionaryValue; + output.Set(kWifiKey, wifi); + SetState(state, wifi); + if (state.IsStatusEqual(SetupState::kSuccess)) + wifi->SetString(kInfoWifiSsidKey, wifi_->GetCurrentlyConnectedSsid()); + } + } + + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandleState(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + base::DictionaryValue output; + base::DictionaryValue* defs = cloud_->GetState().DeepCopy(); + output.Set(kStateKey, defs); + output.SetString(kFingerprintKey, base::IntToString(state_fingerprint_)); + + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandleCommandDefs(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + base::DictionaryValue output; + base::DictionaryValue* defs = cloud_->GetCommandDef().DeepCopy(); + output.Set(kCommandsKey, defs); + output.SetString(kFingerprintKey, + base::IntToString(command_defs_fingerprint_)); + + callback.Run(chromeos::http::status_code::Ok, output); +} + +void PrivetHandler::HandleCommandsExecute(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + cloud_->AddCommand(input, user_info, + base::Bind(&OnCommandRequestSucceeded, callback), + base::Bind(&OnCommandRequestFailed, callback)); +} + +void PrivetHandler::HandleCommandsStatus(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + std::string id; + if (!input.GetString(kCommandsIdKey, &id)) { + chromeos::ErrorPtr error; + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidParams, + kInvalidParamValueFormat, kCommandsIdKey, id.c_str()); + return ReturnError(*error, callback); + } + cloud_->GetCommand(id, user_info, + base::Bind(&OnCommandRequestSucceeded, callback), + base::Bind(&OnCommandRequestFailed, callback)); +} + +void PrivetHandler::HandleCommandsList(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + cloud_->ListCommands(user_info, + base::Bind(&OnCommandRequestSucceeded, callback), + base::Bind(&OnCommandRequestFailed, callback)); +} + +void PrivetHandler::HandleCommandsCancel(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback) { + std::string id; + if (!input.GetString(kCommandsIdKey, &id)) { + chromeos::ErrorPtr error; + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidParams, + kInvalidParamValueFormat, kCommandsIdKey, id.c_str()); + return ReturnError(*error, callback); + } + cloud_->CancelCommand(id, user_info, + base::Bind(&OnCommandRequestSucceeded, callback), + base::Bind(&OnCommandRequestFailed, callback)); +} + +bool StringToPairingType(const std::string& mode, PairingType* id) { + return StringToEnum(mode, id); +} + +std::string PairingTypeToString(PairingType id) { + return EnumToString(id); +} + +bool StringToAuthScope(const std::string& scope, AuthScope* id) { + return StringToEnum(scope, id); +} + +std::string AuthScopeToString(AuthScope id) { + return EnumToString(id); +} + +} // namespace privetd
diff --git a/buffet/privet/privet_handler.h b/buffet/privet/privet_handler.h new file mode 100644 index 0000000..bb86c0a --- /dev/null +++ b/buffet/privet/privet_handler.h
@@ -0,0 +1,139 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_PRIVET_HANDLER_H_ +#define BUFFET_PRIVET_PRIVET_HANDLER_H_ + +#include <map> +#include <string> +#include <utility> + +#include <base/macros.h> +#include <base/memory/weak_ptr.h> +#include <base/scoped_observer.h> + +#include "buffet/privet/cloud_delegate.h" + +namespace base { +class Value; +class DictionaryValue; +} // namespace base + +namespace privetd { + +class DeviceDelegate; +class IdentityDelegate; +class SecurityDelegate; +class WifiDelegate; + +enum class AuthScope; + +// Privet V3 HTTP/HTTPS requests handler. +// API details at https://developers.google.com/cloud-devices/ +class PrivetHandler : public CloudDelegate::Observer { + public: + // Callback to handle requests asynchronously. + // |status| is HTTP status code. + // |output| is result returned in HTTP response. Contains result of + // successfully request of information about error. + using RequestCallback = + base::Callback<void(int status, const base::DictionaryValue& output)>; + + PrivetHandler(CloudDelegate* cloud, + DeviceDelegate* device, + SecurityDelegate* pairing, + WifiDelegate* wifi, + IdentityDelegate* identity); + ~PrivetHandler() override; + + void OnCommandDefsChanged() override; + void OnStateChanged() override; + + // Handles HTTP/HTTPS Privet request. + // |api| is the path from the HTTP request, e.g /privet/info. + // |auth_header| is the Authentication header from HTTP request. + // |input| is the the POST data from HTTP request. If nullptr, data format is + // not valid JSON. + // |callback| will be called exactly once during or after |HandleRequest| + // call. + void HandleRequest(const std::string& api, + const std::string& auth_header, + const base::DictionaryValue* input, + const RequestCallback& callback); + + private: + using ApiHandler = void (PrivetHandler::*)(const base::DictionaryValue&, + const UserInfo&, + const RequestCallback&); + + void AddHandler(const std::string& path, ApiHandler handler, AuthScope scope); + + void HandleInfo(const base::DictionaryValue&, + const UserInfo& user_info, + const RequestCallback& callback); + void HandlePairingStart(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandlePairingConfirm(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandlePairingCancel(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleAuth(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleSetupStart(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleSetupStatus(const base::DictionaryValue&, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleState(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleCommandDefs(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleCommandsExecute(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleCommandsStatus(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleCommandsList(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + void HandleCommandsCancel(const base::DictionaryValue& input, + const UserInfo& user_info, + const RequestCallback& callback); + + void OnUpdateDeviceInfoDone(const std::string& ssid, + const std::string& passphrase, + const std::string& ticket, + const std::string& user, + const RequestCallback& callback) const; + void ReplyWithSetupStatus(const RequestCallback& callback) const; + + CloudDelegate* cloud_ = nullptr; + DeviceDelegate* device_ = nullptr; + SecurityDelegate* security_ = nullptr; + WifiDelegate* wifi_ = nullptr; + IdentityDelegate* identity_ = nullptr; + + std::map<std::string, std::pair<AuthScope, ApiHandler>> handlers_; + + uint64_t last_user_id_{0}; + int state_fingerprint_{0}; + int command_defs_fingerprint_{0}; + ScopedObserver<CloudDelegate, CloudDelegate::Observer> cloud_observer_{this}; + + base::WeakPtrFactory<PrivetHandler> weak_ptr_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(PrivetHandler); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_PRIVET_HANDLER_H_
diff --git a/buffet/privet/privet_handler_unittest.cc b/buffet/privet/privet_handler_unittest.cc new file mode 100644 index 0000000..e2dcfd3 --- /dev/null +++ b/buffet/privet/privet_handler_unittest.cc
@@ -0,0 +1,724 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/privet_handler.h" + +#include <set> +#include <string> +#include <utility> + +#include <base/bind.h> +#include <base/json/json_reader.h> +#include <base/json/json_writer.h> +#include <base/run_loop.h> +#include <base/strings/string_util.h> +#include <base/values.h> +#include <chromeos/http/http_request.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "buffet/privet/constants.h" +#include "buffet/privet/mock_delegates.h" + +using testing::_; +using testing::DoAll; +using testing::Invoke; +using testing::Return; +using testing::SetArgPointee; + +namespace privetd { + +namespace { + +void LoadTestJson(const std::string& test_json, + base::DictionaryValue* dictionary) { + std::string json = test_json; + base::ReplaceChars(json, "'", "\"", &json); + int error = 0; + std::string message; + std::unique_ptr<base::Value> value(base::JSONReader::ReadAndReturnError( + json, base::JSON_PARSE_RFC, &error, &message)); + EXPECT_TRUE(value.get()) << "\nError: " << message << "\n" << json; + base::DictionaryValue* dictionary_ptr = nullptr; + if (value->GetAsDictionary(&dictionary_ptr)) + dictionary->MergeDictionary(dictionary_ptr); +} + +bool IsEqualValue(const base::Value& val1, const base::Value& val2) { + return val1.Equals(&val2); +} + +struct CodeWithReason { + CodeWithReason(int code_in, const std::string& reason_in) + : code(code_in), reason(reason_in) {} + int code; + std::string reason; +}; + +std::ostream& operator<<(std::ostream& stream, const CodeWithReason& error) { + return stream << "{" << error.code << ", " << error.reason << "}"; +} + +bool IsEqualError(const CodeWithReason& expected, + const base::DictionaryValue& dictionary) { + std::string reason; + int code = 0; + return dictionary.GetInteger("error.http_status", &code) && + code == expected.code && dictionary.GetString("error.code", &reason) && + reason == expected.reason; +} + +bool IsEqualDictionary(const base::DictionaryValue& dictionary1, + const base::DictionaryValue& dictionary2) { + base::DictionaryValue::Iterator it1(dictionary1); + base::DictionaryValue::Iterator it2(dictionary2); + for (; !it1.IsAtEnd() && !it2.IsAtEnd(); it1.Advance(), it2.Advance()) { + // Output mismatched keys. + EXPECT_EQ(it1.key(), it2.key()); + if (it1.key() != it2.key()) + return false; + + if (it1.key() == "error") { + std::string code1; + std::string code2; + const char kCodeKey[] = "error.code"; + if (!dictionary1.GetString(kCodeKey, &code1) || + !dictionary2.GetString(kCodeKey, &code2) || code1 != code2) { + return false; + } + continue; + } + + const base::DictionaryValue* d1{nullptr}; + const base::DictionaryValue* d2{nullptr}; + if (it1.value().GetAsDictionary(&d1) && it2.value().GetAsDictionary(&d2)) { + if (!IsEqualDictionary(*d1, *d2)) + return false; + continue; + } + + // Output mismatched values. + EXPECT_PRED2(IsEqualValue, it1.value(), it2.value()); + if (!IsEqualValue(it1.value(), it2.value())) + return false; + } + + return it1.IsAtEnd() && it2.IsAtEnd(); +} + +bool IsEqualJson(const std::string& test_json, + const base::DictionaryValue& dictionary) { + base::DictionaryValue dictionary2; + LoadTestJson(test_json, &dictionary2); + return IsEqualDictionary(dictionary2, dictionary); +} + +} // namespace + +class PrivetHandlerTest : public testing::Test { + public: + PrivetHandlerTest() {} + + protected: + void SetUp() override { + auth_header_ = "Privet anonymous"; + handler_.reset( + new PrivetHandler(&cloud_, &device_, &security_, &wifi_, &identity_)); + } + + const base::DictionaryValue& HandleRequest( + const std::string& api, + const base::DictionaryValue* input) { + output_.Clear(); + handler_->HandleRequest(api, auth_header_, input, + base::Bind(&PrivetHandlerTest::HandlerCallback, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + return output_; + } + + const base::DictionaryValue& HandleRequest(const std::string& api, + const std::string& json_input) { + base::DictionaryValue dictionary; + LoadTestJson(json_input, &dictionary); + return HandleRequest(api, &dictionary); + } + + void HandleUnknownRequest(const std::string& api) { + output_.Clear(); + base::DictionaryValue dictionary; + handler_->HandleRequest(api, auth_header_, &dictionary, + base::Bind(&PrivetHandlerTest::HandlerNoFound)); + base::RunLoop().RunUntilIdle(); + } + + void SetNoWifiAndGcd() { + handler_.reset( + new PrivetHandler(&cloud_, &device_, &security_, nullptr, &identity_)); + EXPECT_CALL(cloud_, GetCloudId()).WillRepeatedly(Return("")); + EXPECT_CALL(cloud_, GetConnectionState()) + .WillRepeatedly(ReturnRef(gcd_disabled_state_)); + auto set_error = + [](const std::string&, const std::string&, chromeos::ErrorPtr* error) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + "setupUnavailable", ""); + }; + EXPECT_CALL(cloud_, Setup(_, _, _)) + .WillRepeatedly(DoAll(Invoke(set_error), Return(false))); + } + + testing::StrictMock<MockCloudDelegate> cloud_; + testing::StrictMock<MockDeviceDelegate> device_; + testing::StrictMock<MockSecurityDelegate> security_; + testing::StrictMock<MockWifiDelegate> wifi_; + testing::StrictMock<MockIdentityDelegate> identity_; + std::string auth_header_; + + private: + void HandlerCallback(int status, const base::DictionaryValue& output) { + output_.MergeDictionary(&output); + if (!output_.HasKey("error")) { + EXPECT_EQ(chromeos::http::status_code::Ok, status); + return; + } + EXPECT_NE(chromeos::http::status_code::Ok, status); + output_.SetInteger("error.http_status", status); + } + + static void HandlerNoFound(int status, const base::DictionaryValue&) { + EXPECT_EQ(status, 404); + } + + base::MessageLoop message_loop_; + std::unique_ptr<PrivetHandler> handler_; + base::DictionaryValue output_; + ConnectionState gcd_disabled_state_{ConnectionState::kDisabled}; +}; + +TEST_F(PrivetHandlerTest, UnknownApi) { + HandleUnknownRequest("/privet/foo"); +} + +TEST_F(PrivetHandlerTest, InvalidFormat) { + auth_header_ = ""; + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidFormat"), + HandleRequest("/privet/info", nullptr)); +} + +TEST_F(PrivetHandlerTest, MissingAuth) { + auth_header_ = ""; + EXPECT_PRED2(IsEqualError, CodeWithReason(401, "missingAuthorization"), + HandleRequest("/privet/info", "{}")); +} + +TEST_F(PrivetHandlerTest, InvalidAuth) { + auth_header_ = "foo"; + EXPECT_PRED2(IsEqualError, CodeWithReason(401, "invalidAuthorization"), + HandleRequest("/privet/info", "{}")); +} + +TEST_F(PrivetHandlerTest, ExpiredAuth) { + auth_header_ = "Privet 123"; + EXPECT_CALL(security_, ParseAccessToken(_, _)) + .WillRepeatedly(DoAll(SetArgPointee<1>(base::Time()), + Return(UserInfo{AuthScope::kOwner, 1}))); + EXPECT_PRED2(IsEqualError, CodeWithReason(403, "authorizationExpired"), + HandleRequest("/privet/info", "{}")); +} + +TEST_F(PrivetHandlerTest, InvalidAuthScope) { + EXPECT_PRED2(IsEqualError, CodeWithReason(403, "invalidAuthorizationScope"), + HandleRequest("/privet/v3/setup/start", "{}")); +} + +TEST_F(PrivetHandlerTest, InfoMinimal) { + SetNoWifiAndGcd(); + EXPECT_CALL(security_, GetPairingTypes()) + .WillRepeatedly(Return(std::set<PairingType>{})); + EXPECT_CALL(security_, GetCryptoTypes()) + .WillRepeatedly(Return(std::set<CryptoType>{})); + + const char kExpected[] = R"({ + 'version': '3.0', + 'id': 'TestId', + 'name': 'TestDevice', + 'services': [], + 'modelManifestId': "ABMID", + 'basicModelManifest': { + 'uiDeviceKind': 'developmentBoard', + 'oemName': 'Chromium', + 'modelName': 'Brillo' + }, + 'endpoints': { + 'httpPort': 0, + 'httpUpdatesPort': 0, + 'httpsPort': 0, + 'httpsUpdatesPort': 0 + }, + 'authentication': { + 'anonymousMaxScope': 'user', + 'mode': [ + 'anonymous', + 'pairing' + ], + 'pairing': [ + ], + 'crypto': [ + ] + }, + 'gcd': { + 'id': '', + 'status': 'disabled' + }, + 'uptime': 3600 + })"; + EXPECT_PRED2(IsEqualJson, kExpected, HandleRequest("/privet/info", "{}")); +} + +TEST_F(PrivetHandlerTest, Info) { + EXPECT_CALL(cloud_, GetDescription()) + .WillRepeatedly(Return("TestDescription")); + EXPECT_CALL(cloud_, GetLocation()).WillRepeatedly(Return("TestLocation")); + EXPECT_CALL(cloud_, GetServices()) + .WillRepeatedly(Return(std::set<std::string>{"service1", "service2"})); + EXPECT_CALL(device_, GetHttpEnpoint()) + .WillRepeatedly(Return(std::make_pair(80, 10080))); + EXPECT_CALL(device_, GetHttpsEnpoint()) + .WillRepeatedly(Return(std::make_pair(443, 10443))); + EXPECT_CALL(wifi_, GetHostedSsid()) + .WillRepeatedly(Return("Test_device.BBABCLAprv")); + + const char kExpected[] = R"({ + 'version': '3.0', + 'id': 'TestId', + 'name': 'TestDevice', + 'description': 'TestDescription', + 'location': 'TestLocation', + 'services': [ + "service1", + "service2" + ], + 'modelManifestId': "ABMID", + 'basicModelManifest': { + 'uiDeviceKind': 'developmentBoard', + 'oemName': 'Chromium', + 'modelName': 'Brillo' + }, + 'endpoints': { + 'httpPort': 80, + 'httpUpdatesPort': 10080, + 'httpsPort': 443, + 'httpsUpdatesPort': 10443 + }, + 'authentication': { + 'anonymousMaxScope': 'none', + 'mode': [ + 'anonymous', + 'pairing' + ], + 'pairing': [ + 'pinCode', + 'embeddedCode', + 'ultrasound32', + 'audible32' + ], + 'crypto': [ + 'p224_spake2', + 'p256_spake2' + ] + }, + 'wifi': { + 'capabilities': [ + '2.4GHz' + ], + 'ssid': 'TestSsid', + 'hostedSsid': 'Test_device.BBABCLAprv', + 'status': 'offline' + }, + 'gcd': { + 'id': 'TestCloudId', + 'status': 'online' + }, + 'uptime': 3600 + })"; + EXPECT_PRED2(IsEqualJson, kExpected, HandleRequest("/privet/info", "{}")); +} + +TEST_F(PrivetHandlerTest, PairingStartInvalidParams) { + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), + HandleRequest("/privet/v3/pairing/start", + "{'pairing':'embeddedCode','crypto':'crypto'}")); + + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), + HandleRequest("/privet/v3/pairing/start", + "{'pairing':'code','crypto':'p256_spake2'}")); +} + +TEST_F(PrivetHandlerTest, PairingStart) { + EXPECT_PRED2( + IsEqualJson, + "{'deviceCommitment': 'testCommitment', 'sessionId': 'testSession'}", + HandleRequest("/privet/v3/pairing/start", + "{'pairing': 'embeddedCode', 'crypto': 'p256_spake2'}")); +} + +TEST_F(PrivetHandlerTest, PairingConfirm) { + EXPECT_PRED2( + IsEqualJson, + "{'certFingerprint':'testFingerprint','certSignature':'testSignature'}", + HandleRequest( + "/privet/v3/pairing/confirm", + "{'sessionId':'testSession','clientCommitment':'testCommitment'}")); +} + +TEST_F(PrivetHandlerTest, PairingCancel) { + EXPECT_PRED2(IsEqualJson, "{}", + HandleRequest("/privet/v3/pairing/cancel", + "{'sessionId': 'testSession'}")); +} + +TEST_F(PrivetHandlerTest, AuthErrorNoType) { + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidAuthMode"), + HandleRequest("/privet/v3/auth", "{}")); +} + +TEST_F(PrivetHandlerTest, AuthErrorInvalidType) { + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidAuthMode"), + HandleRequest("/privet/v3/auth", "{'mode':'unknown'}")); +} + +TEST_F(PrivetHandlerTest, AuthErrorNoScope) { + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidRequestedScope"), + HandleRequest("/privet/v3/auth", "{'mode':'anonymous'}")); +} + +TEST_F(PrivetHandlerTest, AuthErrorInvalidScope) { + EXPECT_PRED2( + IsEqualError, CodeWithReason(400, "invalidRequestedScope"), + HandleRequest("/privet/v3/auth", + "{'mode':'anonymous','requestedScope':'unknown'}")); +} + +TEST_F(PrivetHandlerTest, AuthErrorAccessDenied) { + EXPECT_PRED2(IsEqualError, CodeWithReason(403, "accessDenied"), + HandleRequest("/privet/v3/auth", + "{'mode':'anonymous','requestedScope':'owner'}")); +} + +TEST_F(PrivetHandlerTest, AuthErrorInvalidAuthCode) { + EXPECT_CALL(security_, IsValidPairingCode("testToken")) + .WillRepeatedly(Return(false)); + const char kInput[] = R"({ + 'mode': 'pairing', + 'requestedScope': 'user', + 'authCode': 'testToken' + })"; + EXPECT_PRED2(IsEqualError, CodeWithReason(403, "invalidAuthCode"), + HandleRequest("/privet/v3/auth", kInput)); +} + +TEST_F(PrivetHandlerTest, AuthAnonymous) { + const char kExpected[] = R"({ + 'accessToken': 'GuestAccessToken', + 'expiresIn': 3600, + 'scope': 'user', + 'tokenType': 'Privet' + })"; + EXPECT_PRED2(IsEqualJson, kExpected, + HandleRequest("/privet/v3/auth", + "{'mode':'anonymous','requestedScope':'auto'}")); +} + +TEST_F(PrivetHandlerTest, AuthPairing) { + EXPECT_CALL(security_, IsValidPairingCode("testToken")) + .WillRepeatedly(Return(true)); + EXPECT_CALL(security_, CreateAccessToken(_, _)) + .WillRepeatedly(Return("OwnerAccessToken")); + const char kInput[] = R"({ + 'mode': 'pairing', + 'requestedScope': 'owner', + 'authCode': 'testToken' + })"; + const char kExpected[] = R"({ + 'accessToken': 'OwnerAccessToken', + 'expiresIn': 3600, + 'scope': 'owner', + 'tokenType': 'Privet' + })"; + EXPECT_PRED2( + IsEqualJson, kExpected, HandleRequest("/privet/v3/auth", kInput)); +} + +class PrivetHandlerSetupTest : public PrivetHandlerTest { + public: + void SetUp() override { + PrivetHandlerTest::SetUp(); + auth_header_ = "Privet 123"; + EXPECT_CALL(security_, ParseAccessToken(_, _)) + .WillRepeatedly(DoAll(SetArgPointee<1>(base::Time::Now()), + Return(UserInfo{AuthScope::kOwner, 1}))); + } +}; + +TEST_F(PrivetHandlerSetupTest, StatusEmpty) { + SetNoWifiAndGcd(); + EXPECT_PRED2( + IsEqualJson, "{}", HandleRequest("/privet/v3/setup/status", "{}")); +} + +TEST_F(PrivetHandlerSetupTest, StatusWifi) { + wifi_.setup_state_ = SetupState{SetupState::kSuccess}; + + const char kExpected[] = R"({ + 'wifi': { + 'ssid': 'TestSsid', + 'status': 'success' + } + })"; + EXPECT_PRED2( + IsEqualJson, kExpected, HandleRequest("/privet/v3/setup/status", "{}")); +} + +TEST_F(PrivetHandlerSetupTest, StatusWifiError) { + chromeos::ErrorPtr error; + chromeos::Error::AddTo(&error, FROM_HERE, "test", "invalidPassphrase", ""); + wifi_.setup_state_ = SetupState{std::move(error)}; + + const char kExpected[] = R"({ + 'wifi': { + 'status': 'error', + 'error': { + 'code': 'invalidPassphrase' + } + } + })"; + EXPECT_PRED2( + IsEqualJson, kExpected, HandleRequest("/privet/v3/setup/status", "{}")); +} + +TEST_F(PrivetHandlerSetupTest, StatusGcd) { + cloud_.setup_state_ = SetupState{SetupState::kSuccess}; + + const char kExpected[] = R"({ + 'gcd': { + 'id': 'TestCloudId', + 'status': 'success' + } + })"; + EXPECT_PRED2( + IsEqualJson, kExpected, HandleRequest("/privet/v3/setup/status", "{}")); +} + +TEST_F(PrivetHandlerSetupTest, StatusGcdError) { + chromeos::ErrorPtr error; + chromeos::Error::AddTo(&error, FROM_HERE, "test", "invalidTicket", ""); + cloud_.setup_state_ = SetupState{std::move(error)}; + + const char kExpected[] = R"({ + 'gcd': { + 'status': 'error', + 'error': { + 'code': 'invalidTicket' + } + } + })"; + EXPECT_PRED2( + IsEqualJson, kExpected, HandleRequest("/privet/v3/setup/status", "{}")); +} + +TEST_F(PrivetHandlerSetupTest, SetupNameDescriptionLocation) { + EXPECT_CALL(cloud_, UpdateDeviceInfo("testName", "testDescription", + "testLocation", _, _)).Times(1); + const char kInput[] = R"({ + 'name': 'testName', + 'description': 'testDescription', + 'location': 'testLocation' + })"; + EXPECT_PRED2(IsEqualJson, "{}", + HandleRequest("/privet/v3/setup/start", kInput)); +} + +TEST_F(PrivetHandlerSetupTest, InvalidParams) { + const char kInputWifi[] = R"({ + 'wifi': { + 'ssid': '' + } + })"; + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), + HandleRequest("/privet/v3/setup/start", kInputWifi)); + + const char kInputRegistration[] = R"({ + 'gcd': { + 'ticketId': '' + } + })"; + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), + HandleRequest("/privet/v3/setup/start", kInputRegistration)); +} + +TEST_F(PrivetHandlerSetupTest, WifiSetupUnavailable) { + SetNoWifiAndGcd(); + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "setupUnavailable"), + HandleRequest("/privet/v3/setup/start", "{'wifi': {}}")); +} + +TEST_F(PrivetHandlerSetupTest, WifiSetup) { + const char kInput[] = R"({ + 'wifi': { + 'ssid': 'testSsid', + 'passphrase': 'testPass' + } + })"; + auto set_error = [](const std::string&, const std::string&, + chromeos::ErrorPtr* error) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, "deviceBusy", ""); + }; + EXPECT_CALL(wifi_, ConfigureCredentials(_, _, _)) + .WillOnce(DoAll(Invoke(set_error), Return(false))); + EXPECT_PRED2(IsEqualError, CodeWithReason(503, "deviceBusy"), + HandleRequest("/privet/v3/setup/start", kInput)); + + const char kExpected[] = R"({ + 'wifi': { + 'status': 'inProgress' + } + })"; + wifi_.setup_state_ = SetupState{SetupState::kInProgress}; + EXPECT_CALL(wifi_, ConfigureCredentials("testSsid", "testPass", _)) + .WillOnce(Return(true)); + EXPECT_PRED2(IsEqualJson, kExpected, + HandleRequest("/privet/v3/setup/start", kInput)); +} + +TEST_F(PrivetHandlerSetupTest, GcdSetupUnavailable) { + SetNoWifiAndGcd(); + const char kInput[] = R"({ + 'gcd': { + 'ticketId': 'testTicket', + 'user': 'testUser' + } + })"; + + EXPECT_PRED2(IsEqualError, CodeWithReason(400, "setupUnavailable"), + HandleRequest("/privet/v3/setup/start", kInput)); +} + +TEST_F(PrivetHandlerSetupTest, GcdSetup) { + const char kInput[] = R"({ + 'gcd': { + 'ticketId': 'testTicket', + 'user': 'testUser' + } + })"; + + auto set_error = [](const std::string&, const std::string&, + chromeos::ErrorPtr* error) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, "deviceBusy", ""); + }; + EXPECT_CALL(cloud_, Setup(_, _, _)) + .WillOnce(DoAll(Invoke(set_error), Return(false))); + EXPECT_PRED2(IsEqualError, CodeWithReason(503, "deviceBusy"), + HandleRequest("/privet/v3/setup/start", kInput)); + + const char kExpected[] = R"({ + 'gcd': { + 'status': 'inProgress' + } + })"; + cloud_.setup_state_ = SetupState{SetupState::kInProgress}; + EXPECT_CALL(cloud_, Setup("testTicket", "testUser", _)) + .WillOnce(Return(true)); + EXPECT_PRED2(IsEqualJson, kExpected, + HandleRequest("/privet/v3/setup/start", kInput)); +} + +TEST_F(PrivetHandlerSetupTest, State) { + EXPECT_PRED2(IsEqualJson, "{'state': {'test': {}}, 'fingerprint': '0'}", + HandleRequest("/privet/v3/state", "{}")); + + cloud_.NotifyOnStateChanged(); + + EXPECT_PRED2(IsEqualJson, "{'state': {'test': {}}, 'fingerprint': '1'}", + HandleRequest("/privet/v3/state", "{}")); +} + +TEST_F(PrivetHandlerSetupTest, CommandsDefs) { + EXPECT_PRED2(IsEqualJson, "{'commands': {'test':{}}, 'fingerprint': '0'}", + HandleRequest("/privet/v3/commandDefs", "{}")); + + cloud_.NotifyOnCommandDefsChanged(); + + EXPECT_PRED2(IsEqualJson, "{'commands': {'test':{}}, 'fingerprint': '1'}", + HandleRequest("/privet/v3/commandDefs", "{}")); +} + +TEST_F(PrivetHandlerSetupTest, CommandsExecute) { + const char kInput[] = "{'name': 'test'}"; + base::DictionaryValue command; + LoadTestJson(kInput, &command); + LoadTestJson("{'id':'5'}", &command); + EXPECT_CALL(cloud_, AddCommand(_, _, _, _)) + .WillOnce(RunCallback<2, const base::DictionaryValue&>(command)); + + EXPECT_PRED2(IsEqualJson, "{'name':'test', 'id':'5'}", + HandleRequest("/privet/v3/commands/execute", kInput)); +} + +TEST_F(PrivetHandlerSetupTest, CommandsStatus) { + const char kInput[] = "{'id': '5'}"; + base::DictionaryValue command; + LoadTestJson(kInput, &command); + LoadTestJson("{'name':'test'}", &command); + EXPECT_CALL(cloud_, GetCommand(_, _, _, _)) + .WillOnce(RunCallback<2, const base::DictionaryValue&>(command)); + + EXPECT_PRED2(IsEqualJson, "{'name':'test', 'id':'5'}", + HandleRequest("/privet/v3/commands/status", kInput)); + + chromeos::ErrorPtr error; + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, "notFound", ""); + EXPECT_CALL(cloud_, GetCommand(_, _, _, _)) + .WillOnce(RunCallback<3>(error.get())); + + EXPECT_PRED2(IsEqualError, CodeWithReason(404, "notFound"), + HandleRequest("/privet/v3/commands/status", "{'id': '15'}")); +} + +TEST_F(PrivetHandlerSetupTest, CommandsCancel) { + const char kExpected[] = "{'id': '5', 'name':'test', 'state':'cancelled'}"; + base::DictionaryValue command; + LoadTestJson(kExpected, &command); + EXPECT_CALL(cloud_, CancelCommand(_, _, _, _)) + .WillOnce(RunCallback<2, const base::DictionaryValue&>(command)); + + EXPECT_PRED2(IsEqualJson, kExpected, + HandleRequest("/privet/v3/commands/cancel", "{'id': '8'}")); + + chromeos::ErrorPtr error; + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, "notFound", ""); + EXPECT_CALL(cloud_, CancelCommand(_, _, _, _)) + .WillOnce(RunCallback<3>(error.get())); + + EXPECT_PRED2(IsEqualError, CodeWithReason(404, "notFound"), + HandleRequest("/privet/v3/commands/cancel", "{'id': '11'}")); +} + +TEST_F(PrivetHandlerSetupTest, CommandsList) { + const char kExpected[] = R"({ + 'commands' : [ + {'id':'5', 'state':'cancelled'}, + {'id':'15', 'state':'inProgress'} + ]})"; + + base::DictionaryValue commands; + LoadTestJson(kExpected, &commands); + + EXPECT_CALL(cloud_, ListCommands(_, _, _)) + .WillOnce(RunCallback<1, const base::DictionaryValue&>(commands)); + + EXPECT_PRED2(IsEqualJson, kExpected, + HandleRequest("/privet/v3/commands/list", "{}")); +} + +} // namespace privetd
diff --git a/buffet/privet/privet_types.h b/buffet/privet/privet_types.h new file mode 100644 index 0000000..c1e2ba0 --- /dev/null +++ b/buffet/privet/privet_types.h
@@ -0,0 +1,98 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_PRIVET_TYPES_H_ +#define BUFFET_PRIVET_PRIVET_TYPES_H_ + +#include <string> + +#include <chromeos/errors/error.h> + +namespace privetd { + +// Scopes in order of increasing privileges. +enum class AuthScope { + kNone, + kViewer, + kUser, + kOwner, +}; + +class UserInfo { + public: + explicit UserInfo(AuthScope scope = AuthScope::kNone, uint64_t user_id = 0) + : scope_{scope}, user_id_{scope == AuthScope::kNone ? 0 : user_id} {} + AuthScope scope() const { return scope_; } + uint64_t user_id() const { return user_id_; } + + private: + AuthScope scope_; + uint64_t user_id_; +}; + +class ConnectionState final { + public: + enum Status { + kDisabled, + kUnconfigured, + kConnecting, + kOnline, + kOffline, + }; + + explicit ConnectionState(Status status) : status_(status) {} + explicit ConnectionState(chromeos::ErrorPtr error) + : status_(kOffline), error_(std::move(error)) {} + + Status status() const { + CHECK(!error_); + return status_; + } + + bool IsStatusEqual(Status status) const { + if (error_) + return false; + return status_ == status; + } + + const chromeos::Error* error() const { return error_.get(); } + + private: + Status status_; + chromeos::ErrorPtr error_; +}; + +class SetupState final { + public: + enum Status { + kNone, + kInProgress, + kSuccess, + }; + + explicit SetupState(Status status) : status_(status) {} + explicit SetupState(chromeos::ErrorPtr error) + : status_(kNone), error_(std::move(error)) {} + + Status status() const { + CHECK(!error_); + return status_; + } + + bool IsStatusEqual(Status status) const { + if (error_) + return false; + return status_ == status; + } + + const chromeos::Error* error() const { return error_.get(); } + + private: + Status status_; + chromeos::ErrorPtr error_; +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_PRIVET_TYPES_H_
diff --git a/buffet/privet/privetd_conf_parser.cc b/buffet/privet/privetd_conf_parser.cc new file mode 100644 index 0000000..6ebe155 --- /dev/null +++ b/buffet/privet/privetd_conf_parser.cc
@@ -0,0 +1,146 @@ +// Copyright 2015 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/privetd_conf_parser.h" + +#include <base/logging.h> +#include <base/strings/string_number_conversions.h> +#include <chromeos/strings/string_utils.h> + +#include "buffet/privet/security_delegate.h" + +namespace privetd { + +namespace { + +const char kWiFiBootstrapMode[] = "wifi_bootstrapping_mode"; +const char kGcdBootstrapMode[] = "gcd_bootstrapping_mode"; +const char kConnectTimeout[] = "connect_timeout_seconds"; +const char kBootstrapTimeout[] = "bootstrap_timeout_seconds"; +const char kMonitorTimeout[] = "monitor_timeout_seconds"; +const char kPairingModes[] = "pairing_modes"; +const char kEmbeddedCodePath[] = "embedded_code_path"; + +const char kBootstrapModeOff[] = "off"; +const char kBootstrapModeAutomatic[] = "automatic"; +const char kBootstrapModeManual[] = "manual"; + +} // namespace + +const char kWiFiBootstrapInterfaces[] = "automatic_mode_interfaces"; + +PrivetdConfigParser::PrivetdConfigParser() + : wifi_bootstrap_mode_{WiFiBootstrapMode::kDisabled}, + gcd_bootstrap_mode_{GcdBootstrapMode::kDisabled}, + connect_timeout_seconds_{60u}, + bootstrap_timeout_seconds_{600u}, + monitor_timeout_seconds_{120u}, + pairing_modes_{PairingType::kPinCode} { +} + +bool PrivetdConfigParser::Parse(const chromeos::KeyValueStore& config_store) { + std::string wifi_bootstrap_mode_str; + if (config_store.GetString(kWiFiBootstrapMode, &wifi_bootstrap_mode_str)) { + if (wifi_bootstrap_mode_str.compare(kBootstrapModeOff) == 0) { + wifi_bootstrap_mode_ = WiFiBootstrapMode::kDisabled; + } else if (wifi_bootstrap_mode_str.compare(kBootstrapModeAutomatic) == 0) { + wifi_bootstrap_mode_ = WiFiBootstrapMode::kAutomatic; + } else if (wifi_bootstrap_mode_str.compare(kBootstrapModeManual) == 0) { + LOG(ERROR) << "Manual WiFi bootstrapping mode is unsupported."; + return false; + } else { + LOG(ERROR) << "Unrecognized WiFi bootstrapping mode: " + << wifi_bootstrap_mode_str; + return false; + } + } + + std::string gcd_bootstrap_mode_str; + if (config_store.GetString(kGcdBootstrapMode, &gcd_bootstrap_mode_str)) { + if (gcd_bootstrap_mode_str.compare(kBootstrapModeOff) == 0) { + gcd_bootstrap_mode_ = GcdBootstrapMode::kDisabled; + } else if (gcd_bootstrap_mode_str.compare(kBootstrapModeAutomatic) == 0) { + gcd_bootstrap_mode_ = GcdBootstrapMode::kAutomatic; + } else if (gcd_bootstrap_mode_str.compare(kBootstrapModeManual) == 0) { + LOG(ERROR) << "Manual GCD bootstrapping mode is unsupported."; + return false; + } else { + LOG(ERROR) << "Unrecognized GCD bootstrapping mode: " + << gcd_bootstrap_mode_str; + return false; + } + } + + std::string wifi_interface_list_str; + if (config_store.GetString(kWiFiBootstrapInterfaces, + &wifi_interface_list_str)) { + auto interfaces = + chromeos::string_utils::Split(wifi_interface_list_str, ",", true, true); + automatic_wifi_interfaces_.insert(interfaces.begin(), interfaces.end()); + } + + auto parse_timeout = [](const std::string& input, uint32_t* parsed_value) { + uint32_t temp{0}; + // Tragically, this will set |temp| even for invalid parsings. + if (base::StringToUint(input, &temp)) { + *parsed_value = temp; // Parse worked, use that value. + return true; + } + return false; + }; + + std::string connect_timeout_seconds_str; + if (config_store.GetString(kConnectTimeout, &connect_timeout_seconds_str) && + !parse_timeout(connect_timeout_seconds_str, &connect_timeout_seconds_)) { + LOG(ERROR) << "Invalid string given for connect timeout: " + << connect_timeout_seconds_str; + return false; + } + + std::string bootstrap_timeout_seconds_str; + if (config_store.GetString(kBootstrapTimeout, + &bootstrap_timeout_seconds_str) && + !parse_timeout(bootstrap_timeout_seconds_str, + &bootstrap_timeout_seconds_)) { + LOG(ERROR) << "Invalid string given for bootstrap timeout: " + << bootstrap_timeout_seconds_str; + return false; + } + + std::string monitor_timeout_seconds_str; + if (config_store.GetString(kMonitorTimeout, &monitor_timeout_seconds_str) && + !parse_timeout(monitor_timeout_seconds_str, &monitor_timeout_seconds_)) { + LOG(ERROR) << "Invalid string given for monitor timeout: " + << monitor_timeout_seconds_str; + return false; + } + + std::set<PairingType> pairing_modes; + std::string embedded_code_path; + if (config_store.GetString(kEmbeddedCodePath, &embedded_code_path)) { + embedded_code_path_ = base::FilePath(embedded_code_path); + if (!embedded_code_path_.empty()) + pairing_modes.insert(PairingType::kEmbeddedCode); + } + + std::string modes_str; + if (config_store.GetString(kPairingModes, &modes_str)) { + for (const std::string& mode : + chromeos::string_utils::Split(modes_str, ",", true, true)) { + PairingType pairing_mode; + if (!StringToPairingType(mode, &pairing_mode)) { + LOG(ERROR) << "Invalid pairing mode : " << mode; + return false; + } + pairing_modes.insert(pairing_mode); + } + } + + if (!pairing_modes.empty()) + pairing_modes_ = std::move(pairing_modes); + + return true; +} + +} // namespace privetd
diff --git a/buffet/privet/privetd_conf_parser.h b/buffet/privet/privetd_conf_parser.h new file mode 100644 index 0000000..846342f --- /dev/null +++ b/buffet/privet/privetd_conf_parser.h
@@ -0,0 +1,65 @@ +// Copyright 2015 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_PRIVETD_CONF_PARSER_H_ +#define BUFFET_PRIVET_PRIVETD_CONF_PARSER_H_ + +#include <set> +#include <string> + +#include <chromeos/key_value_store.h> + +namespace privetd { + +extern const char kWiFiBootstrapInterfaces[]; + +enum class WiFiBootstrapMode { + kDisabled, + kManual, + kAutomatic, +}; + +enum class GcdBootstrapMode { + kDisabled, + kManual, + kAutomatic, +}; + +enum class PairingType; + +class PrivetdConfigParser final { + public: + PrivetdConfigParser(); + + bool Parse(const chromeos::KeyValueStore& config_store); + + WiFiBootstrapMode wifi_bootstrap_mode() const { return wifi_bootstrap_mode_; } + GcdBootstrapMode gcd_bootstrap_mode() const { return gcd_bootstrap_mode_; } + const std::set<std::string>& automatic_wifi_interfaces() const { + return automatic_wifi_interfaces_; + } + uint32_t connect_timeout_seconds() const { return connect_timeout_seconds_; } + uint32_t bootstrap_timeout_seconds() const { + return bootstrap_timeout_seconds_; + } + uint32_t monitor_timeout_seconds() const { return monitor_timeout_seconds_; } + const std::set<PairingType>& pairing_modes() { return pairing_modes_; } + const base::FilePath& embedded_code_path() const { + return embedded_code_path_; + } + + private: + WiFiBootstrapMode wifi_bootstrap_mode_; + GcdBootstrapMode gcd_bootstrap_mode_; + std::set<std::string> automatic_wifi_interfaces_; + uint32_t connect_timeout_seconds_; + uint32_t bootstrap_timeout_seconds_; + uint32_t monitor_timeout_seconds_; + std::set<PairingType> pairing_modes_; + base::FilePath embedded_code_path_; +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_PRIVETD_CONF_PARSER_H_
diff --git a/buffet/privet/privetd_conf_parser_unittest.cc b/buffet/privet/privetd_conf_parser_unittest.cc new file mode 100644 index 0000000..8060dd7 --- /dev/null +++ b/buffet/privet/privetd_conf_parser_unittest.cc
@@ -0,0 +1,138 @@ +// Copyright 2015 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/privetd_conf_parser.h" + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include <base/files/file_util.h> +#include <base/files/scoped_temp_dir.h> +#include <base/logging.h> +#include <chromeos/strings/string_utils.h> +#include <gtest/gtest.h> + +#include "buffet/privet/security_delegate.h" + +using chromeos::string_utils::Join; +using chromeos::KeyValueStore; +using std::map; +using std::string; + +namespace privetd { + +namespace { + +const char kWiFiBootstrapMode[] = "wifi_bootstrapping_mode"; +const char kGcdBootstrapMode[] = "gcd_bootstrapping_mode"; +const char kConnectTimeout[] = "connect_timeout_seconds"; +const char kBootstrapTimeout[] = "bootstrap_timeout_seconds"; +const char kMonitorTimeout[] = "monitor_timeout_seconds"; +const char kPairingModes[] = "pairing_modes"; +const char kEmbeddedCodePath[] = "embedded_code_path"; + +} // namespace + +class PrivetdConfParserTest : public testing::Test { + public: + using ConfDict = map<string, string>; + + void SetUp() override { + CHECK(temp_dir_.CreateUniqueTempDir()); + temp_file_ = temp_dir_.path().Append("temp.conf"); + } + + bool IsValid(const ConfDict& conf_dict) { + KeyValueStore store; + FillKeyValueStore(conf_dict, &store); + PrivetdConfigParser config; + return config.Parse(store); + } + + void FillKeyValueStore(const ConfDict& conf_dict, KeyValueStore* store) { + std::vector<string> file_pieces; + for (const auto& it : conf_dict) { + file_pieces.push_back(Join("=", it.first, it.second)); + } + string blob{Join("\n", file_pieces)}; + int expected_len = blob.length(); + CHECK(expected_len == base::WriteFile(temp_file_, + blob.c_str(), + expected_len)); + CHECK(store->Load(temp_file_)); + } + + private: + base::FilePath temp_file_; + base::ScopedTempDir temp_dir_; +}; + +TEST_F(PrivetdConfParserTest, ShouldRejectInvalidTimeouts) { + EXPECT_FALSE(IsValid({{kConnectTimeout, "-1"}})); + EXPECT_FALSE(IsValid({{kConnectTimeout, "a"}})); + EXPECT_FALSE(IsValid({{kConnectTimeout, ""}})); + EXPECT_FALSE(IsValid({{kConnectTimeout, "30 430"}})); +} + +TEST_F(PrivetdConfParserTest, ShouldRejectInvalidWiFiBootstrapModes) { + EXPECT_FALSE(IsValid({{kWiFiBootstrapMode, ""}})); + EXPECT_FALSE(IsValid({{kWiFiBootstrapMode, "clown_shoes"}})); + EXPECT_FALSE(IsValid({{kWiFiBootstrapMode, "off is invalid"}})); + EXPECT_FALSE(IsValid({{kWiFiBootstrapMode, "30"}})); +} + +TEST_F(PrivetdConfParserTest, ShouldRejectInvalidGcdBootstrapModes) { + EXPECT_FALSE(IsValid({{kGcdBootstrapMode, ""}})); + EXPECT_FALSE(IsValid({{kGcdBootstrapMode, "clown_shoes"}})); + EXPECT_FALSE(IsValid({{kGcdBootstrapMode, "off is invalid"}})); + EXPECT_FALSE(IsValid({{kGcdBootstrapMode, "30"}})); +} + +TEST_F(PrivetdConfParserTest, ShouldParseSettings) { + const std::set<std::string> kExpectedWiFiInterfaces{"eth1", "clown shoes"}; + const uint32_t kExpectedConnectTimeout{1}; + const uint32_t kExpectedBootstrapTimeout{2}; + const uint32_t kExpectedMonitorTimeout{3}; + const std::set<PairingType> kExpectedPairingModes{PairingType::kEmbeddedCode, + PairingType::kPinCode}; + static const char kExpectedEmbeddedCodePath[]{"123ABC"}; + const ConfDict conf_dict{ + {kWiFiBootstrapMode, "automatic"}, + {kGcdBootstrapMode, "automatic"}, + {kWiFiBootstrapInterfaces, Join(",", kExpectedWiFiInterfaces)}, + {kConnectTimeout, std::to_string(kExpectedConnectTimeout)}, + {kBootstrapTimeout, std::to_string(kExpectedBootstrapTimeout)}, + {kMonitorTimeout, std::to_string(kExpectedMonitorTimeout)}, + {kPairingModes, "pinCode"}, + {kEmbeddedCodePath, kExpectedEmbeddedCodePath}, + }; + KeyValueStore store; + FillKeyValueStore(conf_dict, &store); + PrivetdConfigParser parser; + EXPECT_TRUE(parser.Parse(store)); + EXPECT_EQ(WiFiBootstrapMode::kAutomatic, parser.wifi_bootstrap_mode()); + EXPECT_EQ(GcdBootstrapMode::kAutomatic, parser.gcd_bootstrap_mode()); + EXPECT_EQ(kExpectedWiFiInterfaces, parser.automatic_wifi_interfaces()); + EXPECT_EQ(kExpectedConnectTimeout, parser.connect_timeout_seconds()); + EXPECT_EQ(kExpectedBootstrapTimeout, parser.bootstrap_timeout_seconds()); + EXPECT_EQ(kExpectedMonitorTimeout, parser.monitor_timeout_seconds()); + EXPECT_EQ(kExpectedPairingModes, parser.pairing_modes()); + EXPECT_EQ(kExpectedEmbeddedCodePath, parser.embedded_code_path().value()); +} + +TEST_F(PrivetdConfParserTest, CriticalDefaults) { + PrivetdConfigParser parser; + EXPECT_EQ(WiFiBootstrapMode::kDisabled, parser.wifi_bootstrap_mode()); + EXPECT_EQ(GcdBootstrapMode::kDisabled, parser.gcd_bootstrap_mode()); + EXPECT_GT(parser.connect_timeout_seconds(), 0); + EXPECT_GT(parser.bootstrap_timeout_seconds(), 0); + EXPECT_GT(parser.monitor_timeout_seconds(), 0); + EXPECT_EQ(std::set<PairingType>{PairingType::kPinCode}, + parser.pairing_modes()); + EXPECT_TRUE(parser.embedded_code_path().empty()); +} + +} // namespace privetd
diff --git a/buffet/privet/security_delegate.h b/buffet/privet/security_delegate.h new file mode 100644 index 0000000..f69ffb2 --- /dev/null +++ b/buffet/privet/security_delegate.h
@@ -0,0 +1,79 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_SECURITY_DELEGATE_H_ +#define BUFFET_PRIVET_SECURITY_DELEGATE_H_ + +#include <memory> +#include <set> +#include <string> + +#include <base/time/time.h> +#include <chromeos/secure_blob.h> + +#include "buffet/privet/privet_types.h" + +namespace privetd { + +enum class PairingType { + kPinCode, + kEmbeddedCode, + kUltrasound32, + kAudible32, +}; + +enum class CryptoType { + kNone, + kSpake_p224, + kSpake_p256, +}; + +// Interface to provide Security related logic for |PrivetHandler|. +class SecurityDelegate { + public: + virtual ~SecurityDelegate() = default; + + // Creates access token for the given scope, user id and |time|. + virtual std::string CreateAccessToken(const UserInfo& user_info, + const base::Time& time) = 0; + + // Validates |token| and returns scope and user id parsed from that. + virtual UserInfo ParseAccessToken(const std::string& token, + base::Time* time) const = 0; + + // Returns list of pairing methods by device. + virtual std::set<PairingType> GetPairingTypes() const = 0; + + // Returns list of crypto methods supported by devices. + virtual std::set<CryptoType> GetCryptoTypes() const = 0; + + // Returns true if |auth_code| provided by client is valid. Client should + // obtain |auth_code| during pairing process. + virtual bool IsValidPairingCode(const std::string& auth_code) const = 0; + + virtual bool StartPairing(PairingType mode, + CryptoType crypto, + std::string* session_id, + std::string* device_commitment, + chromeos::ErrorPtr* error) = 0; + + virtual bool ConfirmPairing(const std::string& session_id, + const std::string& client_commitment, + std::string* fingerprint, + std::string* signature, + chromeos::ErrorPtr* error) = 0; + + virtual bool CancelPairing(const std::string& session_id, + chromeos::ErrorPtr* error) = 0; +}; + +bool StringToPairingType(const std::string& mode, PairingType* id); +std::string PairingTypeToString(PairingType id); + +bool StringToAuthScope(const std::string& scope, AuthScope* id); +std::string AuthScopeToString(AuthScope id); + +} // namespace privetd + +#endif // BUFFET_PRIVET_SECURITY_DELEGATE_H_
diff --git a/buffet/privet/security_manager.cc b/buffet/privet/security_manager.cc new file mode 100644 index 0000000..5dc3b0c --- /dev/null +++ b/buffet/privet/security_manager.cc
@@ -0,0 +1,415 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/security_manager.h" + +#include <algorithm> +#include <limits> +#include <memory> +#include <set> + +#include <base/bind.h> +#include <base/guid.h> +#include <base/logging.h> +#include <base/message_loop/message_loop.h> +#include <base/rand_util.h> +#include <base/stl_util.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/stringprintf.h> +#include <base/time/time.h> +#include <chromeos/data_encoding.h> +#include <chromeos/key_value_store.h> +#include <chromeos/strings/string_utils.h> +#include <crypto/p224_spake.h> + +#include "buffet/privet/constants.h" +#include "buffet/privet/openssl_utils.h" + +namespace privetd { + +namespace { + +const char kTokenDelimeter[] = ":"; +const int kSessionExpirationTimeMinutes = 5; +const int kPairingExpirationTimeMinutes = 5; +const int kMaxAllowedPairingAttemts = 3; +const int kPairingBlockingTimeMinutes = 1; + +const char kEmbeddedCode[] = "embedded_code"; + +// Returns "scope:id:time". +std::string CreateTokenData(const UserInfo& user_info, const base::Time& time) { + return base::IntToString(static_cast<int>(user_info.scope())) + + kTokenDelimeter + base::Uint64ToString(user_info.user_id()) + + kTokenDelimeter + base::Int64ToString(time.ToTimeT()); +} + +// Splits string of "scope:id:time" format. +UserInfo SplitTokenData(const std::string& token, base::Time* time) { + const UserInfo kNone; + auto parts = chromeos::string_utils::Split(token, kTokenDelimeter); + if (parts.size() != 3) + return kNone; + int scope = 0; + if (!base::StringToInt(parts[0], &scope) || + scope < static_cast<int>(AuthScope::kNone) || + scope > static_cast<int>(AuthScope::kOwner)) { + return kNone; + } + + uint64_t id{0}; + if (!base::StringToUint64(parts[1], &id)) + return kNone; + + int64_t timestamp{0}; + if (!base::StringToInt64(parts[2], ×tamp)) + return kNone; + *time = base::Time::FromTimeT(timestamp); + return UserInfo{static_cast<AuthScope>(scope), id}; +} + +std::string LoadEmbeddedCode(const base::FilePath& path) { + std::string code; + chromeos::KeyValueStore store; + if (store.Load(path)) + store.GetString(kEmbeddedCode, &code); + return code; +} + +class Spakep224Exchanger : public SecurityManager::KeyExchanger { + public: + explicit Spakep224Exchanger(const std::string& password) + : spake_(crypto::P224EncryptedKeyExchange::kPeerTypeServer, password) {} + ~Spakep224Exchanger() override = default; + + // SecurityManager::KeyExchanger methods. + const std::string& GetMessage() override { return spake_.GetNextMessage(); } + + bool ProcessMessage(const std::string& message, + chromeos::ErrorPtr* error) override { + switch (spake_.ProcessMessage(message)) { + case crypto::P224EncryptedKeyExchange::kResultPending: + return true; + case crypto::P224EncryptedKeyExchange::kResultFailed: + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kInvalidClientCommitment, + spake_.error()); + return false; + default: + LOG(FATAL) << "SecurityManager uses only one round trip"; + } + return false; + } + + const std::string& GetKey() const override { + return spake_.GetUnverifiedKey(); + } + + private: + crypto::P224EncryptedKeyExchange spake_; +}; + +class UnsecureKeyExchanger : public SecurityManager::KeyExchanger { + public: + explicit UnsecureKeyExchanger(const std::string& password) + : password_(password) {} + ~UnsecureKeyExchanger() override = default; + + // SecurityManager::KeyExchanger methods. + const std::string& GetMessage() override { return password_; } + + bool ProcessMessage(const std::string& message, + chromeos::ErrorPtr* error) override { + return true; + } + + const std::string& GetKey() const override { return password_; } + + private: + std::string password_; +}; + +} // namespace + +SecurityManager::SecurityManager(const std::set<PairingType>& pairing_modes, + const base::FilePath& embedded_code_path, + bool disable_security) + : is_security_disabled_(disable_security), + pairing_modes_(pairing_modes), + embedded_code_path_(embedded_code_path), + secret_(kSha256OutputSize) { + base::RandBytes(secret_.data(), kSha256OutputSize); + + CHECK_EQ(embedded_code_path_.empty(), + std::find(pairing_modes_.begin(), pairing_modes_.end(), + PairingType::kEmbeddedCode) == pairing_modes_.end()); +} + +SecurityManager::~SecurityManager() { + while (!pending_sessions_.empty()) + ClosePendingSession(pending_sessions_.begin()->first); +} + +// Returns "base64([hmac]scope:id:time)". +std::string SecurityManager::CreateAccessToken(const UserInfo& user_info, + const base::Time& time) { + chromeos::SecureBlob data(CreateTokenData(user_info, time)); + chromeos::Blob hash(HmacSha256(secret_, data)); + return chromeos::data_encoding::Base64Encode(chromeos::SecureBlob::Combine( + chromeos::SecureBlob(hash.begin(), hash.end()), data)); +} + +// Parses "base64([hmac]scope:id:time)". +UserInfo SecurityManager::ParseAccessToken(const std::string& token, + base::Time* time) const { + chromeos::Blob decoded; + if (!chromeos::data_encoding::Base64Decode(token, &decoded) || + decoded.size() <= kSha256OutputSize) { + return UserInfo{}; + } + chromeos::SecureBlob data(decoded.begin() + kSha256OutputSize, decoded.end()); + decoded.resize(kSha256OutputSize); + if (decoded != HmacSha256(secret_, data)) + return UserInfo{}; + return SplitTokenData(data.to_string(), time); +} + +std::set<PairingType> SecurityManager::GetPairingTypes() const { + return pairing_modes_; +} + +std::set<CryptoType> SecurityManager::GetCryptoTypes() const { + std::set<CryptoType> result{CryptoType::kSpake_p224}; + if (is_security_disabled_) + result.insert(CryptoType::kNone); + return result; +} + +bool SecurityManager::IsValidPairingCode(const std::string& auth_code) const { + if (is_security_disabled_) + return true; + chromeos::Blob auth_decoded; + if (!chromeos::data_encoding::Base64Decode(auth_code, &auth_decoded)) + return false; + for (const auto& session : confirmed_sessions_) { + if (auth_decoded == + HmacSha256(chromeos::SecureBlob{session.second->GetKey()}, + chromeos::SecureBlob{session.first})) { + pairing_attemts_ = 0; + block_pairing_until_ = base::Time{}; + return true; + } + } + LOG(ERROR) << "Attempt to authenticate with invalide code."; + return false; +} + +bool SecurityManager::StartPairing(PairingType mode, + CryptoType crypto, + std::string* session_id, + std::string* device_commitment, + chromeos::ErrorPtr* error) { + if (!CheckIfPairingAllowed(error)) + return false; + + if (std::find(pairing_modes_.begin(), pairing_modes_.end(), mode) == + pairing_modes_.end()) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kInvalidParams, + "Pairing mode is not enabled"); + return false; + } + + std::string code; + switch (mode) { + case PairingType::kEmbeddedCode: + CHECK(!embedded_code_path_.empty()); + + if (embedded_code_.empty()) + embedded_code_ = LoadEmbeddedCode(embedded_code_path_); + + if (embedded_code_.empty()) { // File is not created yet. + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kDeviceBusy, + "Embedded code is not ready"); + return false; + } + + code = embedded_code_; + break; + case PairingType::kUltrasound32: + case PairingType::kAudible32: { + code = base::RandBytesAsString(4); + break; + } + case PairingType::kPinCode: + code = base::StringPrintf("%04i", base::RandInt(0, 9999)); + break; + default: + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kInvalidParams, + "Unsupported pairing mode"); + return false; + } + + std::unique_ptr<KeyExchanger> spake; + switch (crypto) { + case CryptoType::kSpake_p224: + spake.reset(new Spakep224Exchanger(code)); + break; + case CryptoType::kNone: + if (is_security_disabled_) { + spake.reset(new UnsecureKeyExchanger(code)); + break; + } + // Fall through... + default: + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kInvalidParams, "Unsupported crypto"); + return false; + } + + // Allow only a single session at a time for now. + while (!pending_sessions_.empty()) + ClosePendingSession(pending_sessions_.begin()->first); + + std::string session; + do { + session = base::GenerateGUID(); + } while (confirmed_sessions_.find(session) != confirmed_sessions_.end() || + pending_sessions_.find(session) != pending_sessions_.end()); + std::string commitment = spake->GetMessage(); + pending_sessions_.emplace(session, std::move(spake)); + + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(base::IgnoreResult(&SecurityManager::ClosePendingSession), + weak_ptr_factory_.GetWeakPtr(), session), + base::TimeDelta::FromMinutes(kPairingExpirationTimeMinutes)); + + *session_id = session; + *device_commitment = chromeos::data_encoding::Base64Encode(commitment); + LOG(INFO) << "Pairing code for session " << *session_id << " is " << code; + // TODO(vitalybuka): Handle case when device can't start multiple pairing + // simultaneously and implement throttling to avoid brute force attack. + if (!on_start_.is_null()) { + on_start_.Run(session, mode, + chromeos::string_utils::GetStringAsBytes(code)); + } + + return true; +} + +bool SecurityManager::ConfirmPairing(const std::string& session_id, + const std::string& client_commitment, + std::string* fingerprint, + std::string* signature, + chromeos::ErrorPtr* error) { + auto session = pending_sessions_.find(session_id); + if (session == pending_sessions_.end()) { + chromeos::Error::AddToPrintf( + error, FROM_HERE, errors::kDomain, errors::kUnknownSession, + "Unknown session id: '%s'", session_id.c_str()); + return false; + } + CHECK(!certificate_fingerprint_.empty()); + + chromeos::Blob commitment; + if (!chromeos::data_encoding::Base64Decode(client_commitment, &commitment)) { + ClosePendingSession(session_id); + chromeos::Error::AddToPrintf( + error, FROM_HERE, errors::kDomain, errors::kInvalidFormat, + "Invalid commitment string: '%s'", client_commitment.c_str()); + return false; + } + + if (!session->second->ProcessMessage( + std::string(commitment.begin(), commitment.end()), error)) { + ClosePendingSession(session_id); + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kCommitmentMismatch, + "Pairing code or crypto implementation mismatch"); + return false; + } + + std::string key = session->second->GetKey(); + VLOG(3) << "KEY " << base::HexEncode(key.data(), key.size()); + + *fingerprint = + chromeos::data_encoding::Base64Encode(certificate_fingerprint_); + chromeos::Blob cert_hmac = + HmacSha256(chromeos::SecureBlob(session->second->GetKey()), + certificate_fingerprint_); + *signature = chromeos::data_encoding::Base64Encode(cert_hmac); + confirmed_sessions_.emplace(session->first, std::move(session->second)); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(base::IgnoreResult(&SecurityManager::CloseConfirmedSession), + weak_ptr_factory_.GetWeakPtr(), session_id), + base::TimeDelta::FromMinutes(kSessionExpirationTimeMinutes)); + ClosePendingSession(session_id); + return true; +} + +bool SecurityManager::CancelPairing(const std::string& session_id, + chromeos::ErrorPtr* error) { + bool confirmed = CloseConfirmedSession(session_id); + bool pending = ClosePendingSession(session_id); + if (pending) { + CHECK_GE(pairing_attemts_, 1); + --pairing_attemts_; + } + CHECK(!confirmed || !pending); + if (confirmed || pending) + return true; + chromeos::Error::AddToPrintf(error, FROM_HERE, errors::kDomain, + errors::kUnknownSession, + "Unknown session id: '%s'", session_id.c_str()); + return false; +} + +void SecurityManager::RegisterPairingListeners( + const PairingStartListener& on_start, + const PairingEndListener& on_end) { + CHECK(on_start_.is_null() && on_end_.is_null()); + on_start_ = on_start; + on_end_ = on_end; +} + +bool SecurityManager::CheckIfPairingAllowed(chromeos::ErrorPtr* error) { + if (is_security_disabled_) + return true; + + if (block_pairing_until_ > base::Time::Now()) { + chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain, + errors::kDeviceBusy, "Too many pairing attempts"); + return false; + } + + if (++pairing_attemts_ >= kMaxAllowedPairingAttemts) { + LOG(INFO) << "Pairing blocked for" << kPairingBlockingTimeMinutes + << "minutes."; + block_pairing_until_ = base::Time::Now(); + block_pairing_until_ += + base::TimeDelta::FromMinutes(kPairingBlockingTimeMinutes); + } + + return true; +} + +bool SecurityManager::ClosePendingSession(const std::string& session_id) { + // The most common source of these session_id values is the map containing + // the sessions, which we're about to clear out. Make a local copy. + const std::string safe_session_id{session_id}; + const size_t num_erased = pending_sessions_.erase(safe_session_id); + if (num_erased > 0 && !on_end_.is_null()) + on_end_.Run(safe_session_id); + return num_erased != 0; +} + +bool SecurityManager::CloseConfirmedSession(const std::string& session_id) { + return confirmed_sessions_.erase(session_id) != 0; +} + +} // namespace privetd
diff --git a/buffet/privet/security_manager.h b/buffet/privet/security_manager.h new file mode 100644 index 0000000..04a942e --- /dev/null +++ b/buffet/privet/security_manager.h
@@ -0,0 +1,110 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_SECURITY_MANAGER_H_ +#define BUFFET_PRIVET_SECURITY_MANAGER_H_ + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <vector> + +#include <base/callback.h> +#include <base/files/file_path.h> +#include <base/memory/weak_ptr.h> +#include <chromeos/errors/error.h> +#include <chromeos/secure_blob.h> + +#include "buffet/privet/security_delegate.h" + +namespace crypto { +class P224EncryptedKeyExchange; +} // namespace crypto + +namespace privetd { + +class SecurityManager : public SecurityDelegate { + public: + using PairingStartListener = + base::Callback<void(const std::string& session_id, + PairingType pairing_type, + const std::vector<uint8_t>& code)>; + using PairingEndListener = + base::Callback<void(const std::string& session_id)>; + + class KeyExchanger { + public: + virtual ~KeyExchanger() = default; + + virtual const std::string& GetMessage() = 0; + virtual bool ProcessMessage(const std::string& message, + chromeos::ErrorPtr* error) = 0; + virtual const std::string& GetKey() const = 0; + }; + + SecurityManager(const std::set<PairingType>& pairing_modes, + const base::FilePath& embedded_code_path, + bool disable_security = false); + ~SecurityManager() override; + + // SecurityDelegate methods + std::string CreateAccessToken(const UserInfo& user_info, + const base::Time& time) override; + UserInfo ParseAccessToken(const std::string& token, + base::Time* time) const override; + std::set<PairingType> GetPairingTypes() const override; + std::set<CryptoType> GetCryptoTypes() const override; + bool IsValidPairingCode(const std::string& auth_code) const override; + + bool StartPairing(PairingType mode, + CryptoType crypto, + std::string* session_id, + std::string* device_commitment, + chromeos::ErrorPtr* error) override; + + bool ConfirmPairing(const std::string& session_id, + const std::string& client_commitment, + std::string* fingerprint, + std::string* signature, + chromeos::ErrorPtr* error) override; + bool CancelPairing(const std::string& session_id, + chromeos::ErrorPtr* error) override; + + void RegisterPairingListeners(const PairingStartListener& on_start, + const PairingEndListener& on_end); + + void SetCertificateFingerprint(const chromeos::Blob& fingerprint) { + certificate_fingerprint_ = fingerprint; + } + + private: + FRIEND_TEST_ALL_PREFIXES(SecurityManagerTest, ThrottlePairing); + // Allows limited number of new sessions without successful authorization. + bool CheckIfPairingAllowed(chromeos::ErrorPtr* error); + bool ClosePendingSession(const std::string& session_id); + bool CloseConfirmedSession(const std::string& session_id); + + // If true allows unencrypted pairing and accepts any access code. + bool is_security_disabled_{false}; + std::set<PairingType> pairing_modes_; + const base::FilePath embedded_code_path_; + std::string embedded_code_; + std::map<std::string, std::unique_ptr<KeyExchanger>> pending_sessions_; + std::map<std::string, std::unique_ptr<KeyExchanger>> confirmed_sessions_; + mutable int pairing_attemts_{0}; + mutable base::Time block_pairing_until_; + chromeos::SecureBlob secret_; + chromeos::Blob certificate_fingerprint_; + PairingStartListener on_start_; + PairingEndListener on_end_; + + base::WeakPtrFactory<SecurityManager> weak_ptr_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(SecurityManager); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_SECURITY_MANAGER_H_
diff --git a/buffet/privet/security_manager_unittest.cc b/buffet/privet/security_manager_unittest.cc new file mode 100644 index 0000000..32b7463 --- /dev/null +++ b/buffet/privet/security_manager_unittest.cc
@@ -0,0 +1,316 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/security_manager.h" + +#include <algorithm> +#include <cctype> +#include <functional> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include <base/bind.h> +#include <base/files/file_util.h> +#include <base/logging.h> +#include <base/message_loop/message_loop.h> +#include <base/rand_util.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/string_util.h> +#include <chromeos/data_encoding.h> +#include <chromeos/key_value_store.h> +#include <chromeos/strings/string_utils.h> +#include <crypto/p224_spake.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "buffet/privet/openssl_utils.h" + +using testing::Eq; +using testing::_; + +namespace privetd { + +namespace { + +bool IsBase64Char(char c) { + return isalnum(c) || (c == '+') || (c == '/') || (c == '='); +} + +bool IsBase64(const std::string& text) { + return !text.empty() && + !std::any_of(text.begin(), text.end(), + std::not1(std::ref(IsBase64Char))); +} + +class MockPairingCallbacks { + public: + MOCK_METHOD3(OnPairingStart, + void(const std::string& session_id, + PairingType pairing_type, + const std::vector<uint8_t>& code)); + MOCK_METHOD1(OnPairingEnd, void(const std::string& session_id)); +}; + +base::FilePath GetTempFilePath() { + base::FilePath file_path; + EXPECT_TRUE(base::CreateTemporaryFile(&file_path)); + return file_path; +} + +} // namespace + +class SecurityManagerTest : public testing::Test { + public: + void SetUp() override { + chromeos::Blob fingerprint; + fingerprint.resize(256 / 8); + base::RandBytes(fingerprint.data(), fingerprint.size()); + security_.SetCertificateFingerprint(fingerprint); + + chromeos::KeyValueStore store; + store.SetString("embedded_code", "1234"); + EXPECT_TRUE(store.Save(embedded_code_path_)); + } + + void TearDown() override { base::DeleteFile(embedded_code_path_, false); } + + protected: + void PairAndAuthenticate(std::string* fingerprint, std::string* signature) { + std::string session_id; + std::string device_commitment_base64; + + EXPECT_TRUE(security_.StartPairing(PairingType::kEmbeddedCode, + CryptoType::kSpake_p224, &session_id, + &device_commitment_base64, nullptr)); + EXPECT_FALSE(session_id.empty()); + EXPECT_FALSE(device_commitment_base64.empty()); + + crypto::P224EncryptedKeyExchange spake{ + crypto::P224EncryptedKeyExchange::kPeerTypeClient, "1234"}; + + std::string client_commitment_base64{ + chromeos::data_encoding::Base64Encode(spake.GetNextMessage())}; + + EXPECT_TRUE(security_.ConfirmPairing(session_id, client_commitment_base64, + fingerprint, signature, nullptr)); + EXPECT_TRUE(IsBase64(*fingerprint)); + EXPECT_TRUE(IsBase64(*signature)); + + chromeos::Blob device_commitment; + ASSERT_TRUE(chromeos::data_encoding::Base64Decode(device_commitment_base64, + &device_commitment)); + spake.ProcessMessage( + chromeos::string_utils::GetBytesAsString(device_commitment)); + + chromeos::Blob auth_code{ + HmacSha256(chromeos::SecureBlob{spake.GetUnverifiedKey()}, + chromeos::SecureBlob{session_id})}; + + std::string auth_code_base64{ + chromeos::data_encoding::Base64Encode(auth_code)}; + + EXPECT_TRUE(security_.IsValidPairingCode(auth_code_base64)); + } + + const base::Time time_ = base::Time::FromTimeT(1410000000); + base::MessageLoop message_loop_; + base::FilePath embedded_code_path_{GetTempFilePath()}; + SecurityManager security_{{PairingType::kEmbeddedCode}, embedded_code_path_}; +}; + +TEST_F(SecurityManagerTest, IsBase64) { + EXPECT_TRUE(IsBase64( + security_.CreateAccessToken(UserInfo{AuthScope::kUser, 7}, time_))); +} + +TEST_F(SecurityManagerTest, CreateSameToken) { + EXPECT_EQ( + security_.CreateAccessToken(UserInfo{AuthScope::kViewer, 555}, time_), + security_.CreateAccessToken(UserInfo{AuthScope::kViewer, 555}, time_)); +} + +TEST_F(SecurityManagerTest, CreateTokenDifferentScope) { + EXPECT_NE( + security_.CreateAccessToken(UserInfo{AuthScope::kViewer, 456}, time_), + security_.CreateAccessToken(UserInfo{AuthScope::kOwner, 456}, time_)); +} + +TEST_F(SecurityManagerTest, CreateTokenDifferentUser) { + EXPECT_NE( + security_.CreateAccessToken(UserInfo{AuthScope::kOwner, 456}, time_), + security_.CreateAccessToken(UserInfo{AuthScope::kOwner, 789}, time_)); +} + +TEST_F(SecurityManagerTest, CreateTokenDifferentTime) { + EXPECT_NE( + security_.CreateAccessToken(UserInfo{AuthScope::kOwner, 567}, time_), + security_.CreateAccessToken(UserInfo{AuthScope::kOwner, 567}, + base::Time::FromTimeT(1400000000))); +} + +TEST_F(SecurityManagerTest, CreateTokenDifferentInstance) { + EXPECT_NE(security_.CreateAccessToken(UserInfo{AuthScope::kUser, 123}, time_), + SecurityManager({}, base::FilePath{}) + .CreateAccessToken(UserInfo{AuthScope::kUser, 123}, time_)); +} + +TEST_F(SecurityManagerTest, ParseAccessToken) { + // Multiple attempts with random secrets. + for (size_t i = 0; i < 1000; ++i) { + SecurityManager security{{}, base::FilePath{}}; + + std::string token = + security.CreateAccessToken(UserInfo{AuthScope::kUser, 5}, time_); + base::Time time2; + EXPECT_EQ(AuthScope::kUser, + security.ParseAccessToken(token, &time2).scope()); + EXPECT_EQ(5u, security.ParseAccessToken(token, &time2).user_id()); + // Token timestamp resolution is one second. + EXPECT_GE(1, std::abs((time_ - time2).InSeconds())); + } +} + +TEST_F(SecurityManagerTest, PairingNoSession) { + std::string fingerprint; + std::string signature; + chromeos::ErrorPtr error; + ASSERT_FALSE( + security_.ConfirmPairing("123", "345", &fingerprint, &signature, &error)); + EXPECT_EQ("unknownSession", error->GetCode()); +} + +TEST_F(SecurityManagerTest, Pairing) { + std::vector<std::pair<std::string, std::string> > fingerprints(2); + for (auto& it : fingerprints) { + PairAndAuthenticate(&it.first, &it.second); + } + + // Same certificate. + EXPECT_EQ(fingerprints.front().first, fingerprints.back().first); + + // Signed with different secret. + EXPECT_NE(fingerprints.front().second, fingerprints.back().second); +} + +TEST_F(SecurityManagerTest, NotifiesListenersOfSessionStartAndEnd) { + testing::StrictMock<MockPairingCallbacks> callbacks; + security_.RegisterPairingListeners( + base::Bind(&MockPairingCallbacks::OnPairingStart, + base::Unretained(&callbacks)), + base::Bind(&MockPairingCallbacks::OnPairingEnd, + base::Unretained(&callbacks))); + for (auto commitment_suffix : + std::vector<std::string>{"", "invalid_commitment"}) { + // StartPairing should notify us that a new session has begun. + std::string session_id; + std::string device_commitment; + EXPECT_CALL(callbacks, OnPairingStart(_, PairingType::kEmbeddedCode, _)); + EXPECT_TRUE(security_.StartPairing(PairingType::kEmbeddedCode, + CryptoType::kSpake_p224, &session_id, + &device_commitment, nullptr)); + EXPECT_FALSE(session_id.empty()); + EXPECT_FALSE(device_commitment.empty()); + testing::Mock::VerifyAndClearExpectations(&callbacks); + + // ConfirmPairing should notify us that the session has ended. + EXPECT_CALL(callbacks, OnPairingEnd(Eq(session_id))); + crypto::P224EncryptedKeyExchange spake{ + crypto::P224EncryptedKeyExchange::kPeerTypeServer, "1234"}; + std::string client_commitment = + chromeos::data_encoding::Base64Encode(spake.GetNextMessage()); + std::string fingerprint, signature; + // Regardless of whether the commitment is valid or not, we should get a + // callback indicating that the pairing session is gone. + security_.ConfirmPairing(session_id, client_commitment + commitment_suffix, + &fingerprint, &signature, nullptr); + testing::Mock::VerifyAndClearExpectations(&callbacks); + } +} + +TEST_F(SecurityManagerTest, CancelPairing) { + testing::StrictMock<MockPairingCallbacks> callbacks; + security_.RegisterPairingListeners( + base::Bind(&MockPairingCallbacks::OnPairingStart, + base::Unretained(&callbacks)), + base::Bind(&MockPairingCallbacks::OnPairingEnd, + base::Unretained(&callbacks))); + std::string session_id; + std::string device_commitment; + EXPECT_CALL(callbacks, OnPairingStart(_, PairingType::kEmbeddedCode, _)); + EXPECT_TRUE(security_.StartPairing(PairingType::kEmbeddedCode, + CryptoType::kSpake_p224, &session_id, + &device_commitment, nullptr)); + EXPECT_CALL(callbacks, OnPairingEnd(Eq(session_id))); + EXPECT_TRUE(security_.CancelPairing(session_id, nullptr)); +} + +TEST_F(SecurityManagerTest, ThrottlePairing) { + auto pair = [this]() { + std::string session_id; + std::string device_commitment; + chromeos::ErrorPtr error; + bool result = security_.StartPairing(PairingType::kEmbeddedCode, + CryptoType::kSpake_p224, &session_id, + &device_commitment, &error); + EXPECT_TRUE(result || error->GetCode() == "deviceBusy"); + return result; + }; + + EXPECT_TRUE(pair()); + EXPECT_TRUE(pair()); + EXPECT_TRUE(pair()); + EXPECT_FALSE(pair()); + EXPECT_GT(security_.block_pairing_until_, base::Time::Now()); + EXPECT_LE(security_.block_pairing_until_, + base::Time::Now() + base::TimeDelta::FromMinutes(15)); + + // Wait timeout. + security_.block_pairing_until_ = + base::Time::Now() - base::TimeDelta::FromMinutes(1); + + // Allow exactly one attempt. + EXPECT_TRUE(pair()); + EXPECT_FALSE(pair()); + + // Wait timeout. + security_.block_pairing_until_ = + base::Time::Now() - base::TimeDelta::FromMinutes(1); + + // Completely unblock by successfully pairing. + std::string fingerprint; + std::string signature; + PairAndAuthenticate(&fingerprint, &signature); + + // Now we have 3 attempts again. + EXPECT_TRUE(pair()); + EXPECT_TRUE(pair()); + EXPECT_TRUE(pair()); + EXPECT_FALSE(pair()); +} + +TEST_F(SecurityManagerTest, DontBlockForCanceledSessions) { + for (int i = 0; i < 20; ++i) { + std::string session_id; + std::string device_commitment; + EXPECT_TRUE(security_.StartPairing(PairingType::kEmbeddedCode, + CryptoType::kSpake_p224, &session_id, + &device_commitment, nullptr)); + EXPECT_TRUE(security_.CancelPairing(session_id, nullptr)); + } +} + +TEST_F(SecurityManagerTest, EmbeddedCodeNotReady) { + std::string session_id; + std::string device_commitment; + base::DeleteFile(embedded_code_path_, false); + chromeos::ErrorPtr error; + ASSERT_FALSE(security_.StartPairing(PairingType::kEmbeddedCode, + CryptoType::kSpake_p224, &session_id, + &device_commitment, &error)); + EXPECT_EQ("deviceBusy", error->GetCode()); +} + +} // namespace privetd
diff --git a/buffet/privet/shill_client.cc b/buffet/privet/shill_client.cc new file mode 100644 index 0000000..335d568 --- /dev/null +++ b/buffet/privet/shill_client.cc
@@ -0,0 +1,515 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/shill_client.h" + +#include <set> + +#include <base/message_loop/message_loop.h> +#include <base/stl_util.h> +#include <chromeos/any.h> +#include <chromeos/dbus/service_constants.h> +#include <chromeos/errors/error.h> +#include <chromeos/errors/error_codes.h> + +using chromeos::Any; +using chromeos::VariantDictionary; +using dbus::ObjectPath; +using org::chromium::flimflam::DeviceProxy; +using org::chromium::flimflam::ServiceProxy; +using std::map; +using std::set; +using std::string; +using std::vector; + +namespace privetd { + +namespace { + +void IgnoreDetachEvent() { } + +bool GetStateForService(ServiceProxy* service, string* state) { + CHECK(service) << "|service| was nullptr in GetStateForService()"; + VariantDictionary properties; + if (!service->GetProperties(&properties, nullptr)) { + LOG(WARNING) << "Failed to read properties from service."; + return false; + } + auto property_it = properties.find(shill::kStateProperty); + if (property_it == properties.end()) { + LOG(WARNING) << "No state found in service properties."; + return false; + } + string new_state = property_it->second.TryGet<string>(); + if (new_state.empty()) { + LOG(WARNING) << "Invalid state value."; + return false; + } + *state = new_state; + return true; +} + +ServiceState ShillServiceStateToServiceState(const string& state) { + // TODO(wiley) What does "unconfigured" mean in a world with multiple sets + // of WiFi credentials? + // TODO(wiley) Detect disabled devices, update state appropriately. + if ((state.compare(shill::kStateReady) == 0) || + (state.compare(shill::kStatePortal) == 0) || + (state.compare(shill::kStateOnline) == 0)) { + return ServiceState::kConnected; + } + if ((state.compare(shill::kStateAssociation) == 0) || + (state.compare(shill::kStateConfiguration) == 0)) { + return ServiceState::kConnecting; + } + if ((state.compare(shill::kStateFailure) == 0) || + (state.compare(shill::kStateActivationFailure) == 0)) { + // TODO(wiley) Get error information off the service object. + return ServiceState::kFailure; + } + if ((state.compare(shill::kStateIdle) == 0) || + (state.compare(shill::kStateOffline) == 0) || + (state.compare(shill::kStateDisconnect) == 0)) { + return ServiceState::kOffline; + } + LOG(WARNING) << "Unknown state found: '" << state << "'"; + return ServiceState::kOffline; +} + +} // namespace + +std::string ServiceStateToString(ServiceState state) { + switch (state) { + case ServiceState::kOffline: + return "offline"; + case ServiceState::kFailure: + return "failure"; + case ServiceState::kConnecting: + return "connecting"; + case ServiceState::kConnected: + return "connected"; + } + LOG(ERROR) << "Unknown ServiceState value!"; + return "unknown"; +} + +ShillClient::ShillClient(const scoped_refptr<dbus::Bus>& bus, + const set<string>& device_whitelist) + : bus_{bus}, + manager_proxy_{bus_, ObjectPath{"/"}}, + device_whitelist_{device_whitelist} { + manager_proxy_.RegisterPropertyChangedSignalHandler( + base::Bind(&ShillClient::OnManagerPropertyChange, + weak_factory_.GetWeakPtr()), + base::Bind(&ShillClient::OnManagerPropertyChangeRegistration, + weak_factory_.GetWeakPtr())); + auto owner_changed_cb = base::Bind(&ShillClient::OnShillServiceOwnerChange, + weak_factory_.GetWeakPtr()); + bus_->GetObjectProxy( + shill::kFlimflamServiceName, + ObjectPath{"/"})->SetNameOwnerChangedCallback(owner_changed_cb); +} + +void ShillClient::Init() { + VLOG(2) << "ShillClient::Init();"; + CleanupConnectingService(false); + devices_.clear(); + connectivity_state_ = ServiceState::kOffline; + VariantDictionary properties; + if (!manager_proxy_.GetProperties(&properties, nullptr)) { + LOG(ERROR) << "Unable to get properties from Manager, waiting for " + "Manager to come back online."; + return; + } + auto it = properties.find(shill::kDevicesProperty); + CHECK(it != properties.end()) << "shill should always publish a device list."; + OnManagerPropertyChange(shill::kDevicesProperty, it->second); +} + +bool ShillClient::ConnectToService(const string& ssid, + const string& passphrase, + const base::Closure& on_success, + chromeos::ErrorPtr* error) { + CleanupConnectingService(false); + VariantDictionary service_properties; + service_properties[shill::kTypeProperty] = Any{string{shill::kTypeWifi}}; + service_properties[shill::kSSIDProperty] = Any{ssid}; + service_properties[shill::kPassphraseProperty] = Any{passphrase}; + service_properties[shill::kSecurityProperty] = Any{ + string{passphrase.empty() ? shill::kSecurityNone : shill::kSecurityPsk}}; + service_properties[shill::kSaveCredentialsProperty] = Any{true}; + service_properties[shill::kAutoConnectProperty] = Any{true}; + ObjectPath service_path; + if (!manager_proxy_.ConfigureService(service_properties, + &service_path, + error)) { + return false; + } + if (!manager_proxy_.RequestScan(shill::kTypeWifi, error)) { + return false; + } + connecting_service_.reset(new ServiceProxy{bus_, service_path}); + on_connect_success_.Reset(on_success); + connecting_service_->RegisterPropertyChangedSignalHandler( + base::Bind(&ShillClient::OnServicePropertyChange, + weak_factory_.GetWeakPtr(), + service_path), + base::Bind(&ShillClient::OnServicePropertyChangeRegistration, + weak_factory_.GetWeakPtr(), + service_path)); + return true; +} + +ServiceState ShillClient::GetConnectionState() const { + return connectivity_state_; +} + +bool ShillClient::AmOnline() const { + return connectivity_state_ == ServiceState::kConnected; +} + +void ShillClient::RegisterConnectivityListener( + const ConnectivityListener& listener) { + connectivity_listeners_.push_back(listener); +} + +bool ShillClient::IsMonitoredDevice(DeviceProxy* device) { + if (device_whitelist_.empty()) { + return true; + } + VariantDictionary device_properties; + if (!device->GetProperties(&device_properties, nullptr)) { + LOG(ERROR) << "Devices without properties aren't whitelisted."; + return false; + } + auto it = device_properties.find(shill::kInterfaceProperty); + if (it == device_properties.end()) { + LOG(ERROR) << "Failed to find interface property in device properties."; + return false; + } + return ContainsKey(device_whitelist_, it->second.TryGet<string>()); +} + +void ShillClient::OnShillServiceOwnerChange(const string& old_owner, + const string& new_owner) { + VLOG(1) << "Shill service owner name changed to '" << new_owner << "'"; + if (new_owner.empty()) { + CleanupConnectingService(false); + devices_.clear(); + connectivity_state_ = ServiceState::kOffline; + } else { + Init(); // New service owner means shill reset! + } +} + +void ShillClient::OnManagerPropertyChangeRegistration(const string& interface, + const string& signal_name, + bool success) { + VLOG(3) << "Registered ManagerPropertyChange handler."; + CHECK(success) << "privetd requires Manager signals."; + VariantDictionary properties; + if (!manager_proxy_.GetProperties(&properties, nullptr)) { + LOG(ERROR) << "Unable to get properties from Manager, waiting for " + "Manager to come back online."; + return; + } + auto it = properties.find(shill::kDevicesProperty); + CHECK(it != properties.end()) << "Shill should always publish a device list."; + OnManagerPropertyChange(shill::kDevicesProperty, it->second); +} + +void ShillClient::OnManagerPropertyChange(const string& property_name, + const Any& property_value) { + if (property_name != shill::kDevicesProperty) { + return; + } + VLOG(3) << "Manager's device list has changed."; + // We're going to remove every device we haven't seen in the update. + set<ObjectPath> device_paths_to_remove; + for (const auto& kv : devices_) { + device_paths_to_remove.insert(kv.first); + } + for (const auto& device_path : property_value.TryGet<vector<ObjectPath>>()) { + if (!device_path.IsValid()) { + LOG(ERROR) << "Ignoring invalid device path in Manager's device list."; + return; + } + auto it = devices_.find(device_path); + if (it != devices_.end()) { + // Found an existing proxy. Since the whitelist never changes, + // this still a valid device. + device_paths_to_remove.erase(device_path); + continue; + } + std::unique_ptr<DeviceProxy> device{new DeviceProxy{bus_, device_path}}; + if (!IsMonitoredDevice(device.get())) { + continue; + } + device->RegisterPropertyChangedSignalHandler( + base::Bind(&ShillClient::OnDevicePropertyChange, + weak_factory_.GetWeakPtr(), + device_path), + base::Bind(&ShillClient::OnDevicePropertyChangeRegistration, + weak_factory_.GetWeakPtr(), + device_path)); + VLOG(3) << "Creating device proxy at " << device_path.value(); + devices_[device_path].device = std::move(device); + } + // Clean up devices/services related to removed devices. + if (!device_paths_to_remove.empty()) { + for (const ObjectPath& device_path : device_paths_to_remove) { + devices_.erase(device_path); + } + UpdateConnectivityState(); + } +} + +void ShillClient::OnDevicePropertyChangeRegistration( + const ObjectPath& device_path, + const string& interface, + const string& signal_name, + bool success) { + VLOG(3) << "Registered DevicePropertyChange handler."; + auto it = devices_.find(device_path); + if (it == devices_.end()) { + return; + } + CHECK(success) << "Failed to subscribe to Device property changes."; + DeviceProxy* device = it->second.device.get(); + VariantDictionary properties; + if (!device->GetProperties(&properties, nullptr)) { + LOG(WARNING) << "Failed to get device properties?"; + return; + } + auto prop_it = properties.find(shill::kSelectedServiceProperty); + if (prop_it == properties.end()) { + LOG(WARNING) << "Failed to get device's selected service?"; + return; + } + OnDevicePropertyChange(device_path, + shill::kSelectedServiceProperty, + prop_it->second); +} + +void ShillClient::OnDevicePropertyChange(const ObjectPath& device_path, + const string& property_name, + const Any& property_value) { + // We only care about selected services anyway. + if (property_name != shill::kSelectedServiceProperty) { + return; + } + // If the device isn't our list of whitelisted devices, ignore it. + auto it = devices_.find(device_path); + if (it == devices_.end()) { + return; + } + DeviceState& device_state = it->second; + ObjectPath service_path{property_value.TryGet<ObjectPath>()}; + if (!service_path.IsValid()) { + LOG(ERROR) << "Device at " << device_path.value() + << " selected invalid service path."; + return; + } + VLOG(3) << "Device at " << it->first.value() + << " has selected service at " << service_path.value(); + bool removed_old_service{false}; + if (device_state.selected_service) { + if (device_state.selected_service->GetObjectPath() == service_path) { + return; // Spurious update? + } + device_state.selected_service.reset(); + device_state.service_state = ServiceState::kOffline; + removed_old_service = true; + } + std::shared_ptr<ServiceProxy> new_service; + const bool reuse_connecting_service = + service_path.value() != "/" && + connecting_service_ && + connecting_service_->GetObjectPath() == service_path; + if (reuse_connecting_service) { + new_service = connecting_service_; + // When we reuse the connecting service, we need to make sure that our + // cached state is correct. Normally, we do this by relying reading the + // state when our signal handlers finish registering, but this may have + // happened long in the past for the connecting service. + string state; + if (GetStateForService(new_service.get(), &state)) { + device_state.service_state = ShillServiceStateToServiceState(state); + } else { + LOG(WARNING) << "Failed to read properties from existing service " + "on selection."; + } + } else if (service_path.value() != "/") { + // The device has selected a new service we haven't see before. + new_service.reset(new ServiceProxy{bus_, service_path}); + new_service->RegisterPropertyChangedSignalHandler( + base::Bind(&ShillClient::OnServicePropertyChange, + weak_factory_.GetWeakPtr(), + service_path), + base::Bind(&ShillClient::OnServicePropertyChangeRegistration, + weak_factory_.GetWeakPtr(), + service_path)); + } + device_state.selected_service = new_service; + if (reuse_connecting_service || removed_old_service) { + UpdateConnectivityState(); + } +} + +void ShillClient::OnServicePropertyChangeRegistration(const ObjectPath& path, + const string& interface, + const string& signal_name, + bool success) { + VLOG(3) << "OnServicePropertyChangeRegistration(" << path.value() << ");"; + ServiceProxy* service{nullptr}; + if (connecting_service_ && connecting_service_->GetObjectPath() == path) { + // Note that the connecting service might also be a selected service. + service = connecting_service_.get(); + if (!success) { + CleanupConnectingService(false); + } + } else { + for (const auto& kv : devices_) { + if (kv.second.selected_service && + kv.second.selected_service->GetObjectPath() == path) { + service = kv.second.selected_service.get(); + break; + } + } + } + if (service == nullptr || !success) { + return; // A failure or success for a proxy we no longer care about. + } + VariantDictionary properties; + if (!service->GetProperties(&properties, nullptr)) { + return; + } + // Give ourselves property changed signals for the initial property + // values. + auto it = properties.find(shill::kStateProperty); + if (it != properties.end()) { + OnServicePropertyChange(path, shill::kStateProperty, + it->second); + } + it = properties.find(shill::kSignalStrengthProperty); + if (it != properties.end()) { + OnServicePropertyChange(path, shill::kSignalStrengthProperty, + it->second); + } +} + +void ShillClient::OnServicePropertyChange(const ObjectPath& service_path, + const string& property_name, + const Any& property_value) { + VLOG(3) << "ServicePropertyChange(" << service_path.value() << ", " + << property_name << ", ...);"; + if (property_name == shill::kStateProperty) { + const string state{property_value.TryGet<string>()}; + if (state.empty()) { + VLOG(3) << "Invalid service state update."; + return; + } + VLOG(3) << "New service state=" << state; + OnStateChangeForSelectedService(service_path, state); + OnStateChangeForConnectingService(service_path, state); + } else if (property_name == shill::kSignalStrengthProperty) { + OnStrengthChangeForConnectingService( + service_path, property_value.TryGet<uint8_t>()); + } +} + +void ShillClient::OnStateChangeForConnectingService( + const ObjectPath& service_path, + const string& state) { + if (!connecting_service_ || + connecting_service_->GetObjectPath() != service_path || + ShillServiceStateToServiceState(state) != ServiceState::kConnected) { + return; + } + connecting_service_reset_pending_ = true; + on_connect_success_.callback().Run(); + CleanupConnectingService(true); +} + +void ShillClient::OnStrengthChangeForConnectingService( + const ObjectPath& service_path, + uint8_t signal_strength) { + if (!connecting_service_ || + connecting_service_->GetObjectPath() != service_path || + signal_strength <= 0 || + have_called_connect_) { + return; + } + VLOG(1) << "Connecting service has signal. Calling Connect()."; + have_called_connect_ = true; + // Failures here indicate that we've already connected, + // or are connecting, or some other very unexciting thing. + // Ignore all that, and rely on state changes to detect + // connectivity. + connecting_service_->Connect(nullptr); +} + +void ShillClient::OnStateChangeForSelectedService( + const ObjectPath& service_path, + const string& state) { + // Find the device/service pair responsible for this update + VLOG(3) << "State for potentially selected service " << service_path.value() + << " have changed to " << state; + for (auto& kv : devices_) { + if (kv.second.selected_service && + kv.second.selected_service->GetObjectPath() == service_path) { + VLOG(3) << "Updated cached connection state for selected service."; + kv.second.service_state = ShillServiceStateToServiceState(state); + UpdateConnectivityState(); + return; + } + } +} + +void ShillClient::UpdateConnectivityState() { + // Update the connectivity state of the device by picking the + // state of the currently most connected selected service. + ServiceState new_connectivity_state{ServiceState::kOffline}; + for (const auto& kv : devices_) { + if (kv.second.service_state > new_connectivity_state) { + new_connectivity_state = kv.second.service_state; + } + } + VLOG(3) << "New connectivity state is " + << ServiceStateToString(new_connectivity_state); + if (new_connectivity_state != connectivity_state_) { + connectivity_state_ = new_connectivity_state; + // We may call UpdateConnectivityState whenever we mutate a data structure + // such that our connectivity status could change. However, we don't want + // to allow people to call into ShillClient while some other operation is + // underway. Therefore, call our callbacks later, when we're in a good + // state. + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ShillClient::NotifyConnectivityListeners, + weak_factory_.GetWeakPtr(), AmOnline())); + } +} + +void ShillClient::NotifyConnectivityListeners(bool am_online) { + VLOG(3) << "Notifying connectivity listeners that online=" << am_online; + for (const auto& listener : connectivity_listeners_) { + listener.Run(am_online); + } +} + +void ShillClient::CleanupConnectingService(bool check_for_reset_pending) { + if (check_for_reset_pending && !connecting_service_reset_pending_) { + return; // Must have called connect before we got here. + } + if (connecting_service_) { + connecting_service_->ReleaseObjectProxy(base::Bind(&IgnoreDetachEvent)); + connecting_service_.reset(); + } + on_connect_success_.Cancel(); + have_called_connect_ = false; + connecting_service_reset_pending_ = false; +} + +} // namespace privetd
diff --git a/buffet/privet/shill_client.h b/buffet/privet/shill_client.h new file mode 100644 index 0000000..d8e6959 --- /dev/null +++ b/buffet/privet/shill_client.h
@@ -0,0 +1,134 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_SHILL_CLIENT_H_ +#define BUFFET_PRIVET_SHILL_CLIENT_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include <base/callback.h> +#include <base/cancelable_callback.h> +#include <base/macros.h> +#include <base/memory/ref_counted.h> +#include <base/memory/weak_ptr.h> +#include <dbus/bus.h> + +#include "shill/dbus-proxies.h" + +namespace privetd { + +enum class ServiceState { + kOffline = 0, + kFailure, + kConnecting, + kConnected, +}; + +std::string ServiceStateToString(ServiceState state); + +class ShillClient final { + public: + // A callback that interested parties can register to be notified of + // transitions from online to offline and vice versa. The boolean + // parameter will be true if we're online, and false if we're offline. + using ConnectivityListener = base::Callback<void(bool)>; + + ShillClient(const scoped_refptr<dbus::Bus>& bus, + const std::set<std::string>& device_whitelist); + ~ShillClient() = default; + + + void Init(); + void RegisterConnectivityListener(const ConnectivityListener& listener); + // Causes shill to attempt to connect to the given network with the given + // passphrase. This is accomplished by: + // 1) Configuring a service through the Manager with the SSID and passphrase. + // 2) Calling Connect() on the service. + // 2) Monitoring the returned Service object until we reach an online state, + // an error state, or another call to ConnectToService() occurs. + // Returns false on immediate failures with some descriptive codes in |error|. + bool ConnectToService(const std::string& ssid, + const std::string& passphrase, + const base::Closure& on_success, + chromeos::ErrorPtr* error); + ServiceState GetConnectionState() const; + bool AmOnline() const; + + private: + struct DeviceState { + std::unique_ptr<org::chromium::flimflam::DeviceProxy> device; + // ServiceProxy objects are shared because the connecting service will + // also be the selected service for a device, but is not always the selected + // service (for instance, in the period between configuring a WiFi service + // with credentials, and when Connect() is called.) + std::shared_ptr<org::chromium::flimflam::ServiceProxy> selected_service; + ServiceState service_state{ServiceState::kOffline}; + }; + + bool IsMonitoredDevice(org::chromium::flimflam::DeviceProxy* device); + void OnShillServiceOwnerChange(const std::string& old_owner, + const std::string& new_owner); + void OnManagerPropertyChangeRegistration(const std::string& interface, + const std::string& signal_name, + bool success); + void OnManagerPropertyChange(const std::string& property_name, + const chromeos::Any& property_value); + void OnDevicePropertyChangeRegistration(const dbus::ObjectPath& device_path, + const std::string& interface, + const std::string& signal_name, + bool success); + void OnDevicePropertyChange(const dbus::ObjectPath& device_path, + const std::string& property_name, + const chromeos::Any& property_value); + void OnServicePropertyChangeRegistration(const dbus::ObjectPath& path, + const std::string& interface, + const std::string& signal_name, + bool success); + void OnServicePropertyChange(const dbus::ObjectPath& service_path, + const std::string& property_name, + const chromeos::Any& property_value); + + void OnStateChangeForConnectingService(const dbus::ObjectPath& service_path, + const std::string& state); + void OnStrengthChangeForConnectingService( + const dbus::ObjectPath& service_path, + uint8_t signal_strength); + void OnStateChangeForSelectedService(const dbus::ObjectPath& service_path, + const std::string& state); + void UpdateConnectivityState(); + void NotifyConnectivityListeners(bool am_online); + // Clean up state related to a connecting service. If + // |check_for_reset_pending| is set, then we'll check to see if we've called + // ConnectToService() in the time since a task to call this function was + // posted. + void CleanupConnectingService(bool check_for_reset_pending); + + const scoped_refptr<dbus::Bus> bus_; + org::chromium::flimflam::ManagerProxy manager_proxy_; + // There is logic that assumes we will never change this device list + // in OnManagerPropertyChange. Do not be tempted to remove this const. + const std::set<std::string> device_whitelist_; + std::vector<ConnectivityListener> connectivity_listeners_; + + // State for tracking where we are in our attempts to connect to a service. + bool connecting_service_reset_pending_{false}; + bool have_called_connect_{false}; + std::shared_ptr<org::chromium::flimflam::ServiceProxy> connecting_service_; + base::CancelableClosure on_connect_success_; + + // State for tracking our online connectivity. + std::map<dbus::ObjectPath, DeviceState> devices_; + ServiceState connectivity_state_{ServiceState::kOffline}; + + base::WeakPtrFactory<ShillClient> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(ShillClient); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_SHILL_CLIENT_H_
diff --git a/buffet/privet/wifi_bootstrap_manager.cc b/buffet/privet/wifi_bootstrap_manager.cc new file mode 100644 index 0000000..6c2bc4f --- /dev/null +++ b/buffet/privet/wifi_bootstrap_manager.cc
@@ -0,0 +1,293 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/wifi_bootstrap_manager.h" + +#include <base/logging.h> +#include <base/memory/weak_ptr.h> +#include <base/message_loop/message_loop.h> +#include <chromeos/bind_lambda.h> +#include <chromeos/key_value_store.h> + +#include "buffet/privet/ap_manager_client.h" +#include "buffet/privet/constants.h" +#include "buffet/privet/shill_client.h" + +namespace privetd { + +WifiBootstrapManager::WifiBootstrapManager(DaemonState* state_store, + ShillClient* shill_client, + ApManagerClient* ap_manager_client, + CloudDelegate* gcd, + uint32_t connect_timeout_seconds, + uint32_t bootstrap_timeout_seconds, + uint32_t monitor_timeout_seconds) + : state_store_(state_store), + shill_client_(shill_client), + ap_manager_client_(ap_manager_client), + ssid_generator_(gcd, this), + connect_timeout_seconds_(connect_timeout_seconds), + bootstrap_timeout_seconds_(bootstrap_timeout_seconds), + monitor_timeout_seconds_(monitor_timeout_seconds) { + cloud_observer_.Add(gcd); +} + +void WifiBootstrapManager::Init() { + CHECK(!is_initialized_); + std::string ssid = ssid_generator_.GenerateSsid(); + if (ssid.empty()) + return; // Delay initialization until ssid_generator_ is ready. + chromeos::KeyValueStore state_store; + if (!state_store_->GetBoolean(state_key::kWifiHasBeenBootstrapped, + &have_ever_been_bootstrapped_) || + !state_store_->GetString(state_key::kWifiLastConfiguredSSID, + &last_configured_ssid_)) { + have_ever_been_bootstrapped_ = false; + } + UpdateConnectionState(); + shill_client_->RegisterConnectivityListener( + base::Bind(&WifiBootstrapManager::OnConnectivityChange, + lifetime_weak_factory_.GetWeakPtr())); + if (!have_ever_been_bootstrapped_) { + StartBootstrapping(); + } else { + StartMonitoring(); + } + is_initialized_ = true; +} + +void WifiBootstrapManager::RegisterStateListener( + const StateListener& listener) { + // Notify about current state. + listener.Run(state_); + state_listeners_.push_back(listener); +} + +void WifiBootstrapManager::StartBootstrapping() { + if (shill_client_->AmOnline()) { + // If one of the devices we monitor for connectivity is online, we need not + // start an AP. For most devices, this is a situation which happens in + // testing when we have an ethernet connection. If you need to always + // start an AP to bootstrap WiFi credentials, then add your WiFi interface + // to the device whitelist. + StartMonitoring(); + return; + } + + UpdateState(kBootstrapping); + if (have_ever_been_bootstrapped_) { + // If we have been configured before, we'd like to periodically take down + // our AP and find out if we can connect again. Many kinds of failures are + // transient, and having an AP up prohibits us from connecting as a client. + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&WifiBootstrapManager::OnBootstrapTimeout, + tasks_weak_factory_.GetWeakPtr()), + base::TimeDelta::FromSeconds(bootstrap_timeout_seconds_)); + } + // TODO(vitalybuka): Add SSID probing. + std::string ssid = ssid_generator_.GenerateSsid(); + CHECK(!ssid.empty()); + ap_manager_client_->Start(ssid); +} + +void WifiBootstrapManager::EndBootstrapping() { + ap_manager_client_->Stop(); +} + +void WifiBootstrapManager::StartConnecting(const std::string& ssid, + const std::string& passphrase) { + VLOG(1) << "WiFi is attempting to connect. (ssid=" << ssid + << ", pass=" << passphrase << ")."; + UpdateState(kConnecting); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&WifiBootstrapManager::OnConnectTimeout, + tasks_weak_factory_.GetWeakPtr()), + base::TimeDelta::FromSeconds(connect_timeout_seconds_)); + shill_client_->ConnectToService( + ssid, passphrase, base::Bind(&WifiBootstrapManager::OnConnectSuccess, + tasks_weak_factory_.GetWeakPtr(), ssid), + nullptr); +} + +void WifiBootstrapManager::EndConnecting() { +} + +void WifiBootstrapManager::StartMonitoring() { + VLOG(1) << "Monitoring connectivity."; + // We already have a callback in place with |shill_client_| to update our + // connectivity state. See OnConnectivityChange(). + UpdateState(kMonitoring); +} + +void WifiBootstrapManager::EndMonitoring() { +} + +void WifiBootstrapManager::UpdateState(State new_state) { + VLOG(3) << "Switching state from " << state_ << " to " << new_state; + // Abort irrelevant tasks. + tasks_weak_factory_.InvalidateWeakPtrs(); + + switch (state_) { + case kDisabled: + break; + case kBootstrapping: + EndBootstrapping(); + break; + case kMonitoring: + EndMonitoring(); + break; + case kConnecting: + EndConnecting(); + break; + } + + if (new_state != state_) { + state_ = new_state; + // Post with weak ptr to avoid notification after this object destroyed. + base::MessageLoop::current()->PostTask( + FROM_HERE, base::Bind(&WifiBootstrapManager::NotifyStateListeners, + lifetime_weak_factory_.GetWeakPtr(), new_state)); + } else { + VLOG(3) << "Not notifying listeners of state change, " + << "because the states are the same."; + } +} + +void WifiBootstrapManager::NotifyStateListeners(State new_state) const { + for (const StateListener& listener : state_listeners_) + listener.Run(new_state); +} + +const ConnectionState& WifiBootstrapManager::GetConnectionState() const { + return connection_state_; +} + +const SetupState& WifiBootstrapManager::GetSetupState() const { + return setup_state_; +} + +bool WifiBootstrapManager::ConfigureCredentials(const std::string& ssid, + const std::string& passphrase, + chromeos::ErrorPtr* error) { + setup_state_ = SetupState{SetupState::kInProgress}; + // TODO(vitalybuka): Find more reliable way to finish request or move delay + // into PrivetHandler as it's very HTTP specific. + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&WifiBootstrapManager::StartConnecting, + tasks_weak_factory_.GetWeakPtr(), ssid, passphrase), + base::TimeDelta::FromSeconds(kSetupDelaySeconds)); + return true; +} + +std::string WifiBootstrapManager::GetCurrentlyConnectedSsid() const { + // TODO(vitalybuka): Get from shill, if possible. + return last_configured_ssid_; +} + +std::string WifiBootstrapManager::GetHostedSsid() const { + return ap_manager_client_->GetSsid(); +} + +std::set<WifiType> WifiBootstrapManager::GetTypes() const { + // TODO(wiley) This should do some system work to figure this out. + return {WifiType::kWifi24}; +} + +void WifiBootstrapManager::OnDeviceInfoChanged() { + // Initialization was delayed until buffet is ready. + if (!is_initialized_) + Init(); +} + +void WifiBootstrapManager::OnConnectSuccess(const std::string& ssid) { + VLOG(1) << "Wifi was connected successfully"; + have_ever_been_bootstrapped_ = true; + last_configured_ssid_ = ssid; + state_store_->SetBoolean(state_key::kWifiHasBeenBootstrapped, + have_ever_been_bootstrapped_); + state_store_->SetString(state_key::kWifiLastConfiguredSSID, + last_configured_ssid_); + state_store_->Save(); + setup_state_ = SetupState{SetupState::kSuccess}; + StartMonitoring(); +} + +void WifiBootstrapManager::OnBootstrapTimeout() { + VLOG(1) << "Bootstrapping has timed out."; + StartMonitoring(); +} + +void WifiBootstrapManager::OnConnectTimeout() { + VLOG(1) << "Wifi timed out while connecting"; + chromeos::ErrorPtr error; + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kInvalidState, + "Failed to connect to provided network"); + setup_state_ = SetupState{std::move(error)}; + StartBootstrapping(); +} + +void WifiBootstrapManager::OnConnectivityChange(bool is_connected) { + VLOG(3) << "ConnectivityChanged: " << is_connected; + UpdateConnectionState(); + + if (state_ == kBootstrapping) { + StartMonitoring(); + return; + } + if (state_ == kMonitoring) { + if (is_connected) { + tasks_weak_factory_.InvalidateWeakPtrs(); + } else { + // Tasks queue may have more than one OnMonitorTimeout enqueued. The + // first one could be executed as it would change the state and abort the + // rest. + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&WifiBootstrapManager::OnMonitorTimeout, + tasks_weak_factory_.GetWeakPtr()), + base::TimeDelta::FromSeconds(monitor_timeout_seconds_)); + } + } +} + +void WifiBootstrapManager::OnMonitorTimeout() { + VLOG(1) << "Spent too long offline. Entering bootstrap mode."; + // TODO(wiley) Retrieve relevant errors from shill. + StartBootstrapping(); +} + +void WifiBootstrapManager::UpdateConnectionState() { + connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; + if (!have_ever_been_bootstrapped_) { + return; + } + ServiceState service_state{shill_client_->GetConnectionState()}; + switch (service_state) { + case ServiceState::kOffline: + connection_state_ = ConnectionState{ConnectionState::kOffline}; + return; + case ServiceState::kFailure: { + // TODO(wiley) Pull error information from somewhere. + chromeos::ErrorPtr error; + chromeos::Error::AddTo(&error, FROM_HERE, errors::kDomain, + errors::kInvalidState, "Unknown WiFi error"); + connection_state_ = ConnectionState{std::move(error)}; + return; + } + case ServiceState::kConnecting: + connection_state_ = ConnectionState{ConnectionState::kConnecting}; + return; + case ServiceState::kConnected: + connection_state_ = ConnectionState{ConnectionState::kOnline}; + return; + } + chromeos::ErrorPtr error; + chromeos::Error::AddToPrintf( + &error, FROM_HERE, errors::kDomain, errors::kInvalidState, + "Unknown state returned from ShillClient: %s", + ServiceStateToString(service_state).c_str()); + connection_state_ = ConnectionState{std::move(error)}; +} + +} // namespace privetd
diff --git a/buffet/privet/wifi_bootstrap_manager.h b/buffet/privet/wifi_bootstrap_manager.h new file mode 100644 index 0000000..bb7d3ea --- /dev/null +++ b/buffet/privet/wifi_bootstrap_manager.h
@@ -0,0 +1,130 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_WIFI_BOOTSTRAP_MANAGER_H_ +#define BUFFET_PRIVET_WIFI_BOOTSTRAP_MANAGER_H_ + +#include <set> +#include <string> +#include <vector> + +#include <base/callback.h> +#include <base/files/file_path.h> +#include <base/macros.h> +#include <base/memory/weak_ptr.h> +#include <base/scoped_observer.h> + +#include "buffet/privet/cloud_delegate.h" +#include "buffet/privet/daemon_state.h" +#include "buffet/privet/privet_types.h" +#include "buffet/privet/wifi_delegate.h" +#include "buffet/privet/wifi_ssid_generator.h" + +namespace privetd { + +class ApManagerClient; +class CloudDelegate; +class DeviceDelegate; +class ShillClient; + +class WifiBootstrapManager : public WifiDelegate, + public CloudDelegate::Observer { + public: + enum State { + kDisabled, + kBootstrapping, + kMonitoring, + kConnecting, + }; + + using StateListener = base::Callback<void(State)>; + + WifiBootstrapManager(DaemonState* state_store, + ShillClient* shill_client, + ApManagerClient* ap_manager_client, + CloudDelegate* gcd, + uint32_t connect_timeout_seconds, + uint32_t bootstrap_timeout_seconds, + uint32_t monitor_timeout_seconds); + ~WifiBootstrapManager() override = default; + virtual void Init(); + void RegisterStateListener(const StateListener& listener); + + // Overrides from WifiDelegate. + const ConnectionState& GetConnectionState() const override; + const SetupState& GetSetupState() const override; + bool ConfigureCredentials(const std::string& ssid, + const std::string& passphrase, + chromeos::ErrorPtr* error) override; + std::string GetCurrentlyConnectedSsid() const override; + std::string GetHostedSsid() const override; + std::set<WifiType> GetTypes() const override; + + // Overrides from CloudDelegate::Observer. + void OnDeviceInfoChanged() override; + + private: + // These Start* tasks: + // 1) Do state appropriate work for entering the indicated state. + // 2) Update the state variable to reflect that we're in a new state + // 3) Call StateListeners to notify that we've transitioned. + // These End* tasks perform cleanup on leaving indicated state. + void StartBootstrapping(); + void EndBootstrapping(); + + void StartConnecting(const std::string& ssid, const std::string& passphrase); + void EndConnecting(); + + void StartMonitoring(); + void EndMonitoring(); + + // Update the current state, post tasks to notify listeners accordingly to + // the MessageLoop. + void UpdateState(State new_state); + void NotifyStateListeners(State new_state) const; + + // If we've been bootstrapped successfully before, and we're bootstrapping + // again because we slipped offline for a sufficiently longtime, we want + // to return to monitoring mode periodically in case our connectivity issues + // were temporary. + void OnBootstrapTimeout(); + void OnConnectTimeout(); + void OnConnectSuccess(const std::string& ssid); + void OnConnectivityChange(bool is_connected); + void OnMonitorTimeout(); + void UpdateConnectionState(); + + // Initialization could be delayed if ssid_generator_ is not ready. + bool is_initialized_{false}; + State state_{kDisabled}; + // Setup state is the temporal state of the most recent bootstrapping attempt. + // It is not persisted to disk. + SetupState setup_state_{SetupState::kNone}; + ConnectionState connection_state_{ConnectionState::kDisabled}; + DaemonState* state_store_; + ShillClient* shill_client_; + ApManagerClient* ap_manager_client_; + WifiSsidGenerator ssid_generator_; + + uint32_t connect_timeout_seconds_; + uint32_t bootstrap_timeout_seconds_; + uint32_t monitor_timeout_seconds_; + std::vector<StateListener> state_listeners_; + bool have_ever_been_bootstrapped_{false}; + bool currently_online_{false}; + std::string last_configured_ssid_; + + ScopedObserver<CloudDelegate, CloudDelegate::Observer> cloud_observer_{this}; + + // Helps to reset irrelevant tasks switching state. + base::WeakPtrFactory<WifiBootstrapManager> tasks_weak_factory_{this}; + + base::WeakPtrFactory<WifiBootstrapManager> lifetime_weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(WifiBootstrapManager); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_WIFI_BOOTSTRAP_MANAGER_H_
diff --git a/buffet/privet/wifi_delegate.h b/buffet/privet/wifi_delegate.h new file mode 100644 index 0000000..f350652 --- /dev/null +++ b/buffet/privet/wifi_delegate.h
@@ -0,0 +1,55 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_WIFI_DELEGATE_H_ +#define BUFFET_PRIVET_WIFI_DELEGATE_H_ + +#include <memory> +#include <set> +#include <string> + +#include "buffet/privet/privet_types.h" + +namespace privetd { + +enum class WifiType { + kWifi24, + kWifi50, +}; + +// Interface to provide WiFi functionality for PrivetHandler. +class WifiDelegate { + public: + WifiDelegate() = default; + virtual ~WifiDelegate() = default; + + // Returns status of the WiFi connection. + virtual const ConnectionState& GetConnectionState() const = 0; + + // Returns status of the last WiFi setup. + virtual const SetupState& GetSetupState() const = 0; + + // Starts WiFi setup. Device should try to connect to provided SSID and + // password and store them on success. Result of setup should be available + // using GetSetupState(). + // Final setup state can be retrieved with GetSetupState(). + virtual bool ConfigureCredentials(const std::string& ssid, + const std::string& password, + chromeos::ErrorPtr* error) = 0; + + // Returns SSID of the currently configured WiFi network. Empty string, if + // WiFi has not been configured yet. + virtual std::string GetCurrentlyConnectedSsid() const = 0; + + // Returns SSID of the WiFi network hosted by this device. Empty if device is + // not in setup or P2P modes. + virtual std::string GetHostedSsid() const = 0; + + // Returns list of supported WiFi types. Currently it's just frequencies. + virtual std::set<WifiType> GetTypes() const = 0; +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_WIFI_DELEGATE_H_
diff --git a/buffet/privet/wifi_ssid_generator.cc b/buffet/privet/wifi_ssid_generator.cc new file mode 100644 index 0000000..0fa0c7c --- /dev/null +++ b/buffet/privet/wifi_ssid_generator.cc
@@ -0,0 +1,92 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/wifi_ssid_generator.h" + +#include <bitset> + +#include <base/bind.h> +#include <base/rand_util.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/stringprintf.h> + +#include "buffet/privet/cloud_delegate.h" +#include "buffet/privet/device_delegate.h" +#include "buffet/privet/wifi_delegate.h" + +namespace privetd { + +namespace { + +const int kDeviceNameSize = 20; +// [DeviceName+Idx <= 20].[modelID == 5][flags == 2]prv +const char kSsidFormat[] = "%s %s.%5.5s%2.2sprv"; + +const char base64chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +bool IsSetupNeeded(const ConnectionState& state) { + if (state.error()) + return true; + switch (state.status()) { + case ConnectionState::kUnconfigured: + return true; + case ConnectionState::kDisabled: + case ConnectionState::kConnecting: + case ConnectionState::kOnline: + case ConnectionState::kOffline: + return false; + } + CHECK(false); + return false; +} + +} // namespace + +WifiSsidGenerator::WifiSsidGenerator(const CloudDelegate* cloud, + const WifiDelegate* wifi) + : gcd_(cloud), wifi_(wifi), get_random_(base::Bind(&base::RandInt, 0, 99)) { + CHECK(gcd_); +} + +std::string WifiSsidGenerator::GenerateFlags() const { + std::bitset<6> flags1; + // Device needs WiFi configuration. + flags1[0] = wifi_ && IsSetupNeeded(wifi_->GetConnectionState()); + // Device needs GCD registration. + flags1[1] = IsSetupNeeded(gcd_->GetConnectionState()); + + std::bitset<6> flags2; + // Device is discoverable over WiFi. + flags2[0] = true; + + std::string result{2, base64chars[0]}; + result[0] = base64chars[flags1.to_ulong()]; + result[1] = base64chars[flags2.to_ulong()]; + return result; +} + +std::string WifiSsidGenerator::GenerateSsid() const { + std::string name; + std::string model_id; + if (!gcd_ || !gcd_->GetName(&name, nullptr) || + !gcd_->GetModelId(&model_id, nullptr)) { + return std::string(); + } + std::string idx{base::IntToString(get_random_.Run())}; + name = name.substr(0, kDeviceNameSize - idx.size() - 1); + CHECK_EQ(5u, model_id.size()); + + std::string result = + base::StringPrintf(kSsidFormat, name.c_str(), idx.c_str(), + model_id.c_str(), GenerateFlags().c_str()); + CHECK_EQ(result[result.size() - 11], '.'); + return result; +} + +void WifiSsidGenerator::SetRandomForTests(int n) { + get_random_ = base::Bind(&base::RandInt, n, n); +} + +} // namespace privetd
diff --git a/buffet/privet/wifi_ssid_generator.h b/buffet/privet/wifi_ssid_generator.h new file mode 100644 index 0000000..33e9628 --- /dev/null +++ b/buffet/privet/wifi_ssid_generator.h
@@ -0,0 +1,43 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_PRIVET_WIFI_SSID_GENERATOR_H_ +#define BUFFET_PRIVET_WIFI_SSID_GENERATOR_H_ + +#include <string> + +#include <base/callback.h> + +namespace privetd { + +class CloudDelegate; +class WifiDelegate; + +class WifiSsidGenerator final { + public: + WifiSsidGenerator(const CloudDelegate* gcd, const WifiDelegate* wifi); + ~WifiSsidGenerator() = default; + + std::string GenerateFlags() const; + + // Can return empty string if CloudDelegate is not ready. + std::string GenerateSsid() const; + + private: + friend class WifiSsidGeneratorTest; + + // Sets object to use |n| instead of random number for SSID generation. + void SetRandomForTests(int n); + + const CloudDelegate* gcd_{nullptr}; + const WifiDelegate* wifi_{nullptr}; + + base::Callback<int(void)> get_random_; + + DISALLOW_COPY_AND_ASSIGN(WifiSsidGenerator); +}; + +} // namespace privetd + +#endif // BUFFET_PRIVET_WIFI_SSID_GENERATOR_H_
diff --git a/buffet/privet/wifi_ssid_generator_unittest.cc b/buffet/privet/wifi_ssid_generator_unittest.cc new file mode 100644 index 0000000..16c4475 --- /dev/null +++ b/buffet/privet/wifi_ssid_generator_unittest.cc
@@ -0,0 +1,68 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/privet/wifi_ssid_generator.h" + +#include <base/strings/utf_string_conversions.h> +#include <gtest/gtest.h> + +#include "buffet/privet/mock_delegates.h" +#include "buffet/privet/openssl_utils.h" + +namespace privetd { + +class WifiSsidGeneratorTest : public testing::Test { + protected: + void SetRandomForTests(int n) { ssid_generator_.SetRandomForTests(n); } + + testing::StrictMock<MockCloudDelegate> gcd_; + testing::StrictMock<MockWifiDelegate> wifi_; + + WifiSsidGenerator ssid_generator_{&gcd_, &wifi_}; +}; + +TEST_F(WifiSsidGeneratorTest, GenerateFlags) { + EXPECT_EQ(ssid_generator_.GenerateFlags().size(), 2); + + wifi_.connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; + gcd_.connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; + EXPECT_EQ("DB", ssid_generator_.GenerateFlags()); + + wifi_.connection_state_ = ConnectionState{ConnectionState::kOnline}; + EXPECT_EQ("CB", ssid_generator_.GenerateFlags()); + + gcd_.connection_state_ = ConnectionState{ConnectionState::kOffline}; + EXPECT_EQ("AB", ssid_generator_.GenerateFlags()); + + wifi_.connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; + EXPECT_EQ("BB", ssid_generator_.GenerateFlags()); +} + +TEST_F(WifiSsidGeneratorTest, GenerateSsid31orLess) { + EXPECT_LE(ssid_generator_.GenerateSsid().size(), 31); +} + +TEST_F(WifiSsidGeneratorTest, GenerateSsidValue) { + SetRandomForTests(47); + EXPECT_EQ("TestDevice 47.ABMIDABprv", ssid_generator_.GenerateSsid()); + + SetRandomForTests(9); + EXPECT_EQ("TestDevice 9.ABMIDABprv", ssid_generator_.GenerateSsid()); +} + +TEST_F(WifiSsidGeneratorTest, GenerateSsidLongName) { + SetRandomForTests(99); + EXPECT_CALL(gcd_, GetName(_, _)) + .WillRepeatedly( + DoAll(SetArgPointee<0>("Very Long Device Name"), Return(true))); + EXPECT_EQ("Very Long Device 99.ABMIDABprv", ssid_generator_.GenerateSsid()); +} + +TEST_F(WifiSsidGeneratorTest, GenerateSsidNoName) { + SetRandomForTests(99); + EXPECT_CALL(gcd_, GetName(_, _)).WillRepeatedly(Return(false)); + EXPECT_EQ("", ssid_generator_.GenerateSsid()); +} + +} // namespace privetd