Merge branch 'merge-libchromeos-rewrite' into merge-libchromeos BUG=chromium:370258 Change-Id: Ie4ff2b1976ac7bacf77d13542fea17b3e8efdabf
diff --git a/buffet/HACKING b/buffet/HACKING new file mode 100644 index 0000000..5bab79e --- /dev/null +++ b/buffet/HACKING
@@ -0,0 +1,24 @@ +Some common workflows for developing with buffet: + +# Tell portage that you'd like to make local changes to Buffet: +cros_workon start --board=${BOARD} platform2 + +# Edit files in platform2/buffet/ +vim ... + +# Compile and install those changes into the chroot: +USE=buffet emerge-<board> platform2 + +# Compile and run buffet unittests +USE=buffet P2_TEST_FILTER="buffet::*" FEATURES=test emerge-<board> platform2 + +# Deploy the most recently built version of buffet to a DUT: +cros deploy --board=${BOARD} <remote host> platform2 + +#To enable additional debug logging in buffet daemon, run it as: +# buffet --v=<level>, where <level> is verbosity level of debug info: +# 1 - enable additional tracing of internal object construction and destruction +# 2 - add tracing of request and response data sent over HTTP (beware of +# privacy concerns). +# 3 - enable low-level CURL tracing for HTTP communication. +buffet --v=2
diff --git a/buffet/README b/buffet/README new file mode 100644 index 0000000..9dc1e87 --- /dev/null +++ b/buffet/README
@@ -0,0 +1,6 @@ +// 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. + +This directory contains the a Brillo service for registering a device and +sending/receiving remote commands.
diff --git a/buffet/async_event_sequencer.cc b/buffet/async_event_sequencer.cc new file mode 100644 index 0000000..1dca62c --- /dev/null +++ b/buffet/async_event_sequencer.cc
@@ -0,0 +1,112 @@ +// 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/async_event_sequencer.h" + +namespace buffet { + +namespace dbus_utils { + +AsyncEventSequencer::AsyncEventSequencer() { } +AsyncEventSequencer::~AsyncEventSequencer() { } + +AsyncEventSequencer::Handler AsyncEventSequencer::GetHandler( + const std::string& descriptive_message, bool failure_is_fatal) { + CHECK(!started_) << "Cannot create handlers after OnAllTasksCompletedCall()"; + int unique_registration_id = ++registration_counter_; + outstanding_registrations_.insert(unique_registration_id); + return base::Bind(&AsyncEventSequencer::HandleFinish, this, + unique_registration_id, descriptive_message, + failure_is_fatal); +} + +AsyncEventSequencer::ExportHandler AsyncEventSequencer::GetExportHandler( + const std::string& interface_name, const std::string& method_name, + const std::string& descriptive_message, bool failure_is_fatal) { + auto finish_handler = GetHandler(descriptive_message, failure_is_fatal); + return base::Bind(&AsyncEventSequencer::HandleDBusMethodExported, this, + finish_handler, + interface_name, + method_name); +} + +void AsyncEventSequencer::OnAllTasksCompletedCall( + std::vector<CompletionAction> actions) { + CHECK(!started_) << "OnAllTasksCompletedCall called twice!"; + started_ = true; + completion_actions_.assign(actions.begin(), actions.end()); + // All of our callbacks might have been called already. + PossiblyRunCompletionActions(); +} + +namespace { +void IgnoreSuccess(const AsyncEventSequencer::CompletionTask& task, + bool /*success*/) { task.Run(); } +} // namespace + +AsyncEventSequencer::CompletionAction AsyncEventSequencer::WrapCompletionTask( + const CompletionTask& task) { + return base::Bind(&IgnoreSuccess, task); +} + +void AsyncEventSequencer::HandleFinish(int registration_number, + const std::string& error_message, + bool failure_is_fatal, bool success) { + RetireRegistration(registration_number); + CheckForFailure(failure_is_fatal, success, error_message); + PossiblyRunCompletionActions(); +} + +void AsyncEventSequencer::HandleDBusMethodExported( + const AsyncEventSequencer::Handler& finish_handler, + const std::string& expected_interface_name, + const std::string& expected_method_name, + const std::string& actual_interface_name, + const std::string& actual_method_name, bool success) { + CHECK_EQ(expected_method_name, actual_method_name) + << "Exported DBus method '" << actual_method_name << "' " + << "but expected '" << expected_method_name << "'"; + CHECK_EQ(expected_interface_name, actual_interface_name) + << "Exported method DBus interface '" << actual_interface_name << "' " + << "but expected '" << expected_interface_name << "'"; + finish_handler.Run(success); +} + + +void AsyncEventSequencer::RetireRegistration(int registration_number) { + const size_t handlers_retired = outstanding_registrations_.erase( + registration_number); + CHECK_EQ(1, handlers_retired) + << "Tried to retire invalid handler " << registration_number << ")"; +} + +void AsyncEventSequencer::CheckForFailure(bool failure_is_fatal, bool success, + const std::string& error_message) { + if (failure_is_fatal) { + CHECK(success) << error_message; + } + if (!success) { + LOG(ERROR) << error_message; + had_failures_ = true; + } +} + +void AsyncEventSequencer::PossiblyRunCompletionActions() { + if (!started_ || !outstanding_registrations_.empty()) { + // Don't run completion actions if we have any outstanding + // Handlers outstanding or if any more handlers might + // be scheduled in the future. + return; + } + for (auto&& completion_action : completion_actions_) { + // Should this be put on the message loop or run directly? + completion_action.Run(!had_failures_); + } + // Discard our references to those actions. + completion_actions_.clear(); +} + +} // namespace dbus_utils + +} // namespace buffet
diff --git a/buffet/async_event_sequencer.h b/buffet/async_event_sequencer.h new file mode 100644 index 0000000..96adad7 --- /dev/null +++ b/buffet/async_event_sequencer.h
@@ -0,0 +1,105 @@ +// 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_ASYNC_EVENT_SEQUENCER_H_ +#define BUFFET_ASYNC_EVENT_SEQUENCER_H_ + +#include <set> +#include <string> +#include <vector> + +#include <base/bind.h> +#include <base/basictypes.h> +#include <base/memory/ref_counted.h> + +namespace buffet { + +namespace dbus_utils { + +// A helper class for coordinating the multiple async tasks. A consumer +// may grab any number of callbacks via Get*Handler() and schedule a list +// of completion actions to take. When all handlers obtained via Get*Handler() +// have been called, the AsyncEventSequencer will call its CompletionActions. +// +// Usage: +// +// void Init(const base::Callback<void(bool success)> cb) { +// scoped_refptr<AsyncEventSequencer> sequencer( +// new AsyncEventSequencer()); +// one_delegate_needing_init_.Init(sequencer->GetHandler( +// "my delegate failed to init", false)); +// dbus_init_delegate_.Init(sequencer->GetExportHandler( +// "org.test.Interface", "ExposedMethodName", +// "another delegate is flaky", false)); +// sequencer->OnAllTasksCompletedCall({cb}); +// } +class AsyncEventSequencer : public base::RefCounted<AsyncEventSequencer> { + public: + typedef base::Callback<void(bool success)> Handler; + typedef base::Callback<void (const std::string& interface_name, + const std::string& method_name, + bool success)> ExportHandler; + typedef base::Callback<void(bool all_succeeded)> CompletionAction; + typedef base::Callback<void(void)> CompletionTask; + + AsyncEventSequencer(); + + // Get a Finished handler callback. Each callback is "unique" in the sense + // that subsequent calls to GetHandler() will create new handlers + // which will need to be called before completion actions are run. + Handler GetHandler(const std::string& descriptive_message, + bool failure_is_fatal); + + // Like GetHandler except with a signature tailored to + // ExportedObject's ExportMethod callback requirements. Will also assert + // that the passed interface/method names from ExportedObject are correct. + ExportHandler GetExportHandler( + const std::string& interface_name, const std::string& method_name, + const std::string& descriptive_message, bool failure_is_fatal); + + // Once all handlers obtained via GetHandler have run, + // we'll run each CompletionAction, then discard our references. + // No more handlers may be obtained after this call. + void OnAllTasksCompletedCall(std::vector<CompletionAction> actions); + + // Wrap a CompletionTask with a function that discards the result. + // This CompletionTask retains no references to the AsyncEventSequencer. + CompletionAction WrapCompletionTask(const CompletionTask& task); + + private: + // We'll partially bind this function before giving it back via + // GetHandler. Note that the returned callbacks have + // references to *this, which gives us the neat property that we'll + // destroy *this only when all our callbacks have been destroyed. + void HandleFinish(int registration_number, const std::string& error_message, + bool failure_is_fatal, bool success); + // Similar to HandleFinish. + void HandleDBusMethodExported( + const Handler& finish_handler, + const std::string& expected_interface_name, + const std::string& expected_method_name, + const std::string& actual_interface_name, + const std::string& actual_method_name, + bool success); + void RetireRegistration(int registration_number); + void CheckForFailure(bool failure_is_fatal, bool success, + const std::string& error_message); + void PossiblyRunCompletionActions(); + + bool started_{false}; // NOLINT - initializer list + int registration_counter_{0}; // NOLINT - initializer list + std::set<int> outstanding_registrations_; + std::vector<CompletionAction> completion_actions_; + bool had_failures_{false}; // NOLINT - initializer list + // Ref counted objects have private destructors. + ~AsyncEventSequencer(); + friend class base::RefCounted<AsyncEventSequencer>; + DISALLOW_COPY_AND_ASSIGN(AsyncEventSequencer); +}; + +} // namespace dbus_utils + +} // namespace buffet + +#endif // BUFFET_ASYNC_EVENT_SEQUENCER_H_
diff --git a/buffet/async_event_sequencer_unittest.cc b/buffet/async_event_sequencer_unittest.cc new file mode 100644 index 0000000..d95ff56 --- /dev/null +++ b/buffet/async_event_sequencer_unittest.cc
@@ -0,0 +1,96 @@ +// 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/async_event_sequencer.h" + +#include <base/bind_helpers.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +namespace buffet { + +namespace dbus_utils { + +namespace { + +const char kTestInterface[] = "org.test.if"; +const char kTestMethod1[] = "TestMethod1"; +const char kTestMethod2[] = "TestMethod2"; + +} // namespace + +class AsyncEventSequencerTest : public ::testing::Test { + public: + MOCK_METHOD1(HandleCompletion, void(bool all_succeeded)); + + void SetUp() { + aec_ = new AsyncEventSequencer(); + cb_ = base::Bind(&AsyncEventSequencerTest::HandleCompletion, + base::Unretained(this)); + } + + scoped_refptr<AsyncEventSequencer> aec_; + AsyncEventSequencer::CompletionAction cb_; +}; + +TEST_F(AsyncEventSequencerTest, WaitForCompletionActions) { + auto finished_handler = aec_->GetHandler("handler failed", false); + finished_handler.Run(true); + EXPECT_CALL(*this, HandleCompletion(true)).Times(1); + aec_->OnAllTasksCompletedCall({cb_}); +} + +TEST_F(AsyncEventSequencerTest, MultiInitActionsSucceed) { + auto finished_handler1 = aec_->GetHandler("handler failed", false); + auto finished_handler2 = aec_->GetHandler("handler failed", false); + aec_->OnAllTasksCompletedCall({cb_}); + finished_handler1.Run(true); + EXPECT_CALL(*this, HandleCompletion(true)).Times(1); + finished_handler2.Run(true); +} + +TEST_F(AsyncEventSequencerTest, SomeInitActionsFail) { + auto finished_handler1 = aec_->GetHandler("handler failed", false); + auto finished_handler2 = aec_->GetHandler("handler failed", false); + aec_->OnAllTasksCompletedCall({cb_}); + finished_handler1.Run(false); + EXPECT_CALL(*this, HandleCompletion(false)).Times(1); + finished_handler2.Run(true); +} + +TEST_F(AsyncEventSequencerTest, MultiDBusActionsSucceed) { + auto handler1 = aec_->GetExportHandler( + kTestInterface, kTestMethod1, "method export failed", false); + auto handler2 = aec_->GetExportHandler( + kTestInterface, kTestMethod2, "method export failed", false); + aec_->OnAllTasksCompletedCall({cb_}); + handler1.Run(kTestInterface, kTestMethod1, true); + EXPECT_CALL(*this, HandleCompletion(true)).Times(1); + handler2.Run(kTestInterface, kTestMethod2, true); +} + +TEST_F(AsyncEventSequencerTest, SomeDBusActionsFail) { + auto handler1 = aec_->GetExportHandler( + kTestInterface, kTestMethod1, "method export failed", false); + auto handler2 = aec_->GetExportHandler( + kTestInterface, kTestMethod2, "method export failed", false); + aec_->OnAllTasksCompletedCall({cb_}); + handler1.Run(kTestInterface, kTestMethod1, true); + EXPECT_CALL(*this, HandleCompletion(false)).Times(1); + handler2.Run(kTestInterface, kTestMethod2, false); +} + +TEST_F(AsyncEventSequencerTest, MixedActions) { + auto handler1 = aec_->GetExportHandler( + kTestInterface, kTestMethod1, "method export failed", false); + auto handler2 = aec_->GetHandler("handler failed", false); + aec_->OnAllTasksCompletedCall({cb_}); + handler1.Run(kTestInterface, kTestMethod1, true); + EXPECT_CALL(*this, HandleCompletion(true)).Times(1); + handler2.Run(true); +} + +} // namespace dbus_utils + +} // namespace buffet
diff --git a/buffet/bind_lambda.h b/buffet/bind_lambda.h new file mode 100644 index 0000000..0172ab3 --- /dev/null +++ b/buffet/bind_lambda.h
@@ -0,0 +1,65 @@ +// 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_BIND_LAMBDA_H_ +#define BUFFET_BIND_LAMBDA_H_ + +#include <base/bind.h> + +//////////////////////////////////////////////////////////////////////////////// +// This file is an extension to base/bind_internal.h and adds a RunnableAdapter +// class specialization that wraps a functor (including lambda objects), so +// they can be used in base::Callback/base::Bind constructs. +// By including this file you will gain the ability to write expressions like: +// base::Callback<int(int)> callback = base::Bind([](int value) { +// return value * value; +// }); +//////////////////////////////////////////////////////////////////////////////// +namespace base { +namespace internal { + +// LambdaAdapter is a helper class that specializes on different function call +// signatures and provides the RunType and Run() method required by +// RunnableAdapter<> class. +template <typename Lambda, typename Sig> +class LambdaAdapter; + +// R(...) +template <typename Lambda, typename R, typename... Args> +class LambdaAdapter<Lambda, R(Lambda::*)(Args... args)> { + public: + typedef R(RunType)(Args...); + LambdaAdapter(Lambda lambda) : lambda_(lambda) {} + R Run(Args... args) { return lambda_(args...); } + + private: + Lambda lambda_; +}; + +// R(...) const +template <typename Lambda, typename R, typename... Args> +class LambdaAdapter<Lambda, R(Lambda::*)(Args... args) const> { + public: + typedef R(RunType)(Args...); + LambdaAdapter(Lambda lambda) : lambda_(lambda) {} + R Run(Args... args) { return lambda_(args...); } + + private: + Lambda lambda_; +}; + +template <typename Lambda> +class RunnableAdapter : public LambdaAdapter<Lambda, + decltype(&Lambda::operator())> { + public: + explicit RunnableAdapter(Lambda lambda) : + LambdaAdapter<Lambda, decltype(&Lambda::operator())>(lambda) { + } +}; + + +} // namespace internal +} // namespace base + +#endif // BUFFET_BIND_LAMBDA_H_
diff --git a/buffet/buffet.conf b/buffet/buffet.conf new file mode 100644 index 0000000..66cd9ed --- /dev/null +++ b/buffet/buffet.conf
@@ -0,0 +1,16 @@ +# 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. + +description "Brillo Buffet Service" +author "chromium-os-dev@chromium.org" + +start on starting system-services +stop on stopping system-services +respawn + +pre-start script + mkdir -p /var/lib/buffet +end script + +exec buffet
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp new file mode 100644 index 0000000..7e5c71a --- /dev/null +++ b/buffet/buffet.gyp
@@ -0,0 +1,94 @@ +{ + 'target_defaults': { + 'dependencies': [ + '<(platform_root)/metrics/libmetrics-<(libbase_ver).gyp:libmetrics-<(libbase_ver)', + ], + 'variables': { + 'deps': [ + 'dbus-1', + 'libchrome-<(libbase_ver)', + 'libchrome-test-<(libbase_ver)', + 'libcurl', + ], + }, + # TODO(sosa): Remove gflags: crbug.com/356745. + 'link_settings': { + 'libraries': [ + '-lgflags', + '-lbase-dbus_test_support-<(libbase_ver)', + ], + }, + 'cflags_cc': [ + '-std=gnu++11', + ], + }, + 'targets': [ + { + 'target_name': 'buffet_common', + 'type': 'static_library', + 'sources': [ + 'async_event_sequencer.cc', + 'data_encoding.cc', + 'dbus_constants.cc', + 'dbus_utils.cc', + 'device_registration_info.cc', + 'error.cc', + 'exported_object_manager.cc', + 'exported_property_set.cc', + 'http_request.cc', + 'http_connection_curl.cc', + 'http_transport_curl.cc', + 'http_utils.cc', + 'manager.cc', + 'mime_utils.cc', + 'storage_impls.cc', + 'string_utils.cc', + 'url_utils.cc' + ], + }, + { + 'target_name': 'buffet', + 'type': 'executable', + 'sources': [ + 'main.cc', + ], + 'dependencies': [ + 'buffet_common', + ], + }, + { + 'target_name': 'buffet_client', + 'type': 'executable', + 'sources': [ + 'buffet_client.cc', + 'dbus_constants.cc', + ], + 'dependencies': [ + 'buffet_common', + ], + }, + { + 'target_name': 'buffet_testrunner', + 'type': 'executable', + 'dependencies': [ + 'buffet_common', + ], + 'includes': ['../common-mk/common_test.gypi'], + 'sources': [ + 'async_event_sequencer_unittest.cc', + 'buffet_testrunner.cc', + 'data_encoding_unittest.cc', + 'device_registration_info_unittest.cc', + 'error_unittest.cc', + 'exported_object_manager_unittest.cc', + 'exported_property_set_unittest.cc', + 'http_connection_fake.cc', + 'http_transport_fake.cc', + 'http_utils_unittest.cc', + 'mime_utils_unittest.cc', + 'string_utils_unittest.cc', + 'url_utils_unittest.cc' + ], + }, + ], +}
diff --git a/buffet/buffet_client.cc b/buffet/buffet_client.cc new file mode 100644 index 0000000..a763e42 --- /dev/null +++ b/buffet/buffet_client.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 <iostream> // NOLINT(readability/streams) +#include <string> +#include <sysexits.h> + +#include <base/command_line.h> +#include <base/logging.h> +#include <base/memory/ref_counted.h> +#include <base/memory/scoped_ptr.h> +#include <base/values.h> +#include <dbus/bus.h> +#include <dbus/message.h> +#include <dbus/object_proxy.h> +#include <dbus/object_manager.h> +#include <dbus/values_util.h> + +#include "buffet/dbus_constants.h" +#include "buffet/data_encoding.h" + +using namespace buffet::dbus_constants; // NOLINT(build/namespaces) + +namespace { +static const int default_timeout_ms = 1000; + +void usage() { + std::cerr << "Possible commands:" << std::endl; + std::cerr << " " << kManagerTestMethod << std::endl; + std::cerr << " " << kManagerCheckDeviceRegistered << std::endl; + std::cerr << " " << kManagerGetDeviceInfo << std::endl; + std::cerr << " " << kManagerStartRegisterDevice + << " param1 = val1¶m2 = val2..." << std::endl; + std::cerr << " " << kManagerFinishRegisterDevice + << " device_id" << std::endl; + std::cerr << " " << kManagerUpdateStateMethod << std::endl; + std::cerr << " " << dbus::kObjectManagerGetManagedObjects << std::endl; +} + +class BuffetHelperProxy { + public: + int Init() { + dbus::Bus::Options options; + options.bus_type = dbus::Bus::SYSTEM; + bus_ = new dbus::Bus(options); + manager_proxy_ = bus_->GetObjectProxy( + kServiceName, + dbus::ObjectPath(kManagerServicePath)); + root_proxy_ = bus_->GetObjectProxy( + kServiceName, + dbus::ObjectPath(kRootServicePath)); + return EX_OK; + } + + int CallTestMethod(const CommandLine::StringVector& args) { + dbus::MethodCall method_call(kManagerInterface, kManagerTestMethod); + scoped_ptr<dbus::Response> response( + manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); + if (!response) { + std::cout << "Failed to receive a response." << std::endl; + return EX_UNAVAILABLE; + } + std::cout << "Received a response." << std::endl; + return EX_OK; + } + + int CallManagerCheckDeviceRegistered(const CommandLine::StringVector& args) { + if (!args.empty()) { + std::cerr << "Invalid number of arguments for " + << "Manager." << kManagerCheckDeviceRegistered << std::endl; + usage(); + return EX_USAGE; + } + dbus::MethodCall method_call( + kManagerInterface, kManagerCheckDeviceRegistered); + + scoped_ptr<dbus::Response> response( + manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); + if (!response) { + std::cout << "Failed to receive a response." << std::endl; + return EX_UNAVAILABLE; + } + + dbus::MessageReader reader(response.get()); + std::string device_id; + if (!reader.PopString(&device_id)) { + std::cout << "No device ID in response." << std::endl; + return EX_SOFTWARE; + } + + std::cout << "Device ID: " + << (device_id.empty() ? std::string("<unregistered>") : device_id) + << std::endl; + return EX_OK; + } + + int CallManagerGetDeviceInfo(const CommandLine::StringVector& args) { + if (!args.empty()) { + std::cerr << "Invalid number of arguments for " + << "Manager." << kManagerGetDeviceInfo << std::endl; + usage(); + return EX_USAGE; + } + dbus::MethodCall method_call( + kManagerInterface, kManagerGetDeviceInfo); + + scoped_ptr<dbus::Response> response( + manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); + if (!response) { + std::cout << "Failed to receive a response." << std::endl; + return EX_UNAVAILABLE; + } + + dbus::MessageReader reader(response.get()); + std::string device_info; + if (!reader.PopString(&device_info)) { + std::cout << "No device info in response." << std::endl; + return EX_SOFTWARE; + } + + std::cout << "Device Info: " + << (device_info.empty() ? std::string("<unregistered>") : device_info) + << std::endl; + return EX_OK; + } + + int CallManagerStartRegisterDevice(const CommandLine::StringVector& args) { + if (args.size() > 1) { + std::cerr << "Invalid number of arguments for " + << "Manager." << kManagerStartRegisterDevice << std::endl; + usage(); + return EX_USAGE; + } + std::map<std::string, std::shared_ptr<base::Value>> params; + + if (!args.empty()) { + auto key_values = buffet::data_encoding::WebParamsDecode(args.front()); + for (auto&& pair : key_values) { + params.insert(std::make_pair( + pair.first, std::shared_ptr<base::Value>( + base::Value::CreateStringValue(pair.second)))); + } + } + + dbus::MethodCall method_call( + kManagerInterface, kManagerStartRegisterDevice); + dbus::MessageWriter writer(&method_call); + dbus::MessageWriter dict_writer(nullptr); + writer.OpenArray("{sv}", &dict_writer); + for (auto&& pair : params) { + dbus::MessageWriter dict_entry_writer(nullptr); + dict_writer.OpenDictEntry(&dict_entry_writer); + dict_entry_writer.AppendString(pair.first); + dbus::AppendBasicTypeValueDataAsVariant(&dict_entry_writer, + *pair.second.get()); + dict_writer.CloseContainer(&dict_entry_writer); + } + writer.CloseContainer(&dict_writer); + + static const int timeout_ms = 3000; + scoped_ptr<dbus::Response> response( + manager_proxy_->CallMethodAndBlock(&method_call, timeout_ms)); + if (!response) { + std::cout << "Failed to receive a response." << std::endl; + return EX_UNAVAILABLE; + } + + dbus::MessageReader reader(response.get()); + std::string info; + if (!reader.PopString(&info)) { + std::cout << "No valid response." << std::endl; + return EX_SOFTWARE; + } + + std::cout << "Registration started: " << info << std::endl; + return EX_OK; + } + + int CallManagerFinishRegisterDevice(const CommandLine::StringVector& args) { + if (args.size() > 1) { + std::cerr << "Invalid number of arguments for " + << "Manager." << kManagerFinishRegisterDevice << std::endl; + usage(); + return EX_USAGE; + } + dbus::MethodCall method_call( + kManagerInterface, kManagerFinishRegisterDevice); + dbus::MessageWriter writer(&method_call); + std::string user_auth_code; + if (!args.empty()) { user_auth_code = args.front(); } + writer.AppendString(user_auth_code); + static const int timeout_ms = 10000; + scoped_ptr<dbus::Response> response( + manager_proxy_->CallMethodAndBlock(&method_call, timeout_ms)); + if (!response) { + std::cout << "Failed to receive a response." << std::endl; + return EX_UNAVAILABLE; + } + + dbus::MessageReader reader(response.get()); + std::string device_id; + if (!reader.PopString(&device_id)) { + std::cout << "No device ID in response." << std::endl; + return EX_SOFTWARE; + } + + std::cout << "Device ID is " + << (device_id.empty() ? std::string("<unregistered>") : device_id) + << std::endl; + return EX_OK; + } + + int CallManagerUpdateState(const CommandLine::StringVector& args) { + if (args.size() != 1) { + std::cerr << "Invalid number of arguments for " + << "Manager." << kManagerUpdateStateMethod << std::endl; + usage(); + return EX_USAGE; + } + dbus::MethodCall method_call( + kManagerInterface, kManagerUpdateStateMethod); + dbus::MessageWriter writer(&method_call); + writer.AppendString(args.front()); + scoped_ptr<dbus::Response> response( + manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); + if (!response) { + std::cout << "Failed to receive a response." << std::endl; + return EX_UNAVAILABLE; + } + return EX_OK; + } + + int CallRootGetManagedObjects(const CommandLine::StringVector& args) { + if (!args.empty()) { + std::cerr << "Invalid number of arguments for " + << dbus::kObjectManagerGetManagedObjects << std::endl; + usage(); + return EX_USAGE; + } + dbus::MethodCall method_call( + dbus::kObjectManagerInterface, dbus::kObjectManagerGetManagedObjects); + scoped_ptr<dbus::Response> response( + root_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms)); + if (!response) { + std::cout << "Failed to receive a response." << std::endl; + return EX_UNAVAILABLE; + } + std::cout << response->ToString() << std::endl; + return EX_OK; + } + + private: + scoped_refptr<dbus::Bus> bus_; + dbus::ObjectProxy* manager_proxy_{nullptr}; // NOLINT - initializer list + dbus::ObjectProxy* root_proxy_{nullptr}; // NOLINT - initializer list +}; + +} // namespace + +int main(int argc, char** argv) { + CommandLine::Init(argc, argv); + CommandLine* cl = CommandLine::ForCurrentProcess(); + CommandLine::StringVector args = cl->GetArgs(); + if (args.empty()) { + usage(); + return EX_USAGE; + } + + // Pop the command off of the args list. + std::string command = args.front(); + args.erase(args.begin()); + int err = EX_USAGE; + BuffetHelperProxy helper; + err = helper.Init(); + if (err) { + std::cerr << "Error initializing proxies." << std::endl; + return err; + } + + if (command.compare(kManagerTestMethod) == 0) { + err = helper.CallTestMethod(args); + } else if (command.compare(kManagerCheckDeviceRegistered) == 0 || + command.compare("cr") == 0) { + err = helper.CallManagerCheckDeviceRegistered(args); + } else if (command.compare(kManagerGetDeviceInfo) == 0 || + command.compare("di") == 0) { + err = helper.CallManagerGetDeviceInfo(args); + } else if (command.compare(kManagerStartRegisterDevice) == 0 || + command.compare("sr") == 0) { + err = helper.CallManagerStartRegisterDevice(args); + } else if (command.compare(kManagerFinishRegisterDevice) == 0 || + command.compare("fr") == 0) { + err = helper.CallManagerFinishRegisterDevice(args); + } else if (command.compare(kManagerUpdateStateMethod) == 0 || + command.compare("us") == 0) { + err = helper.CallManagerUpdateState(args); + } else if (command.compare(dbus::kObjectManagerGetManagedObjects) == 0) { + err = helper.CallRootGetManagedObjects(args); + } else { + std::cerr << "Unknown command: " << command << std::endl; + usage(); + } + + if (err) { + std::cerr << "Done, with errors." << std::endl; + } else { + std::cout << "Done." << std::endl; + } + return err; +}
diff --git a/buffet/buffet_testrunner.cc b/buffet/buffet_testrunner.cc new file mode 100644 index 0000000..575f952 --- /dev/null +++ b/buffet/buffet_testrunner.cc
@@ -0,0 +1,12 @@ +// Copyright (c) 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 <base/at_exit.h> +#include <gtest/gtest.h> + +int main(int argc, char **argv) { + base::AtExitManager exit_manager; + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}
diff --git a/buffet/data_encoding.cc b/buffet/data_encoding.cc new file mode 100644 index 0000000..9f351de --- /dev/null +++ b/buffet/data_encoding.cc
@@ -0,0 +1,101 @@ +// 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/data_encoding.h" + +#include <base/strings/stringprintf.h> +#include <string.h> + +#include "buffet/string_utils.h" + +namespace { + +inline int HexToDec(int hex) { + int dec = -1; + if (hex >= '0' && hex <= '9') { + dec = hex - '0'; + } else if (hex >= 'A' && hex <= 'F') { + dec = hex - 'A' + 10; + } else if (hex >= 'a' && hex <= 'f') { + dec = hex - 'a' + 10; + } + return dec; +} + +} // namespace + +///////////////////////////////////////////////////////////////////////// +namespace buffet { +namespace data_encoding { + +std::string UrlEncode(const char* data, bool encodeSpaceAsPlus) { + std::string result; + + while (*data) { + char c = *data++; + // According to RFC3986 (http://www.faqs.org/rfcs/rfc3986.html), + // section 2.3. - Unreserved Characters + if ((c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + c == '-' || c == '.' || c == '_' || c == '~') { + result += c; + } else if (c == ' ' && encodeSpaceAsPlus) { + // For historical reasons, some URLs have spaces encoded as '+', + // this also applies to form data encoded as + // 'application/x-www-form-urlencoded' + result += '+'; + } else { + base::StringAppendF(&result, "%%%02X", + static_cast<unsigned char>(c)); // Encode as %NN + } + } + return result; +} + +std::string UrlDecode(const char* data) { + std::string result; + while (*data) { + char c = *data++; + int part1 = 0, part2 = 0; + // HexToDec would return -1 even for character 0 (end of string), + // so it is safe to access data[0] and data[1] without overrunning the buf. + if (c == '%' && + (part1 = HexToDec(data[0])) >= 0 && (part2 = HexToDec(data[1])) >= 0) { + c = static_cast<char>((part1 << 4) | part2); + data += 2; + } else if (c == '+') { + c = ' '; + } + result += c; + } + return result; +} + +std::string WebParamsEncode(const WebParamList& params, + bool encodeSpaceAsPlus) { + std::vector<std::string> pairs; + pairs.reserve(params.size()); + for (auto&& p : params) { + std::string key = UrlEncode(p.first.c_str(), encodeSpaceAsPlus); + std::string value = UrlEncode(p.second.c_str(), encodeSpaceAsPlus); + pairs.push_back(string_utils::Join('=', key, value)); + } + + return string_utils::Join('&', pairs); +} + +WebParamList WebParamsDecode(const std::string& data) { + WebParamList result; + std::vector<std::string> params = string_utils::Split(data, '&'); + for (auto p : params) { + auto pair = string_utils::SplitAtFirst(p, '='); + result.emplace_back(UrlDecode(pair.first.c_str()), + UrlDecode(pair.second.c_str())); + } + return result; +} + +} // namespace data_encoding +} // namespace buffet
diff --git a/buffet/data_encoding.h b/buffet/data_encoding.h new file mode 100644 index 0000000..e493e36 --- /dev/null +++ b/buffet/data_encoding.h
@@ -0,0 +1,46 @@ +// 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_DATA_ENCODING_H_ +#define BUFFET_DATA_ENCODING_H_ + +#include <string> +#include <utility> +#include <vector> + +namespace buffet { +namespace data_encoding { + +typedef std::vector<std::pair<std::string, std::string>> WebParamList; + +// Encode/escape string to be used in the query portion of a URL. +// If |encodeSpaceAsPlus| is set to true, spaces are encoded as '+' instead +// of "%20" +std::string UrlEncode(const char* data, bool encodeSpaceAsPlus); + +inline std::string UrlEncode(const char* data) { + return UrlEncode(data, true); +} + +// Decodes/unescapes a URL. Replaces all %XX sequences with actual characters. +// Also replaces '+' with spaces. +std::string UrlDecode(const char* data); + +// Converts a list of key-value pairs into a string compatible with +// 'application/x-www-form-urlencoded' content encoding. +std::string WebParamsEncode(const WebParamList& params, bool encodeSpaceAsPlus); + +inline std::string WebParamsEncode(const WebParamList& params) { + return WebParamsEncode(params, true); +} + +// Parses a string of '&'-delimited key-value pairs (separated by '=') and +// encoded in a way compatible with 'application/x-www-form-urlencoded' +// content encoding. +WebParamList WebParamsDecode(const std::string& data); + +} // namespace data_encoding +} // namespace buffet + +#endif // BUFFET_DATA_ENCODING_H_
diff --git a/buffet/data_encoding_unittest.cc b/buffet/data_encoding_unittest.cc new file mode 100644 index 0000000..ed6295c --- /dev/null +++ b/buffet/data_encoding_unittest.cc
@@ -0,0 +1,39 @@ +// Copyright (c) 2011 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/data_encoding.h" + +#include <gtest/gtest.h> + +using namespace buffet::data_encoding; // NOLINT(build/namespaces) + +TEST(data_encoding, UrlEncoding) { + std::string test = "\"http://sample/path/0014.html \""; + std::string encoded = UrlEncode(test.c_str()); + EXPECT_EQ("%22http%3A%2F%2Fsample%2Fpath%2F0014.html+%22", + encoded); + EXPECT_EQ(test, UrlDecode(encoded.c_str())); + + test = "\"http://sample/path/0014.html \""; + encoded = UrlEncode(test.c_str(), false); + EXPECT_EQ("%22http%3A%2F%2Fsample%2Fpath%2F0014.html%20%22", + encoded); + EXPECT_EQ(test, UrlDecode(encoded.c_str())); +} + +TEST(data_encoding, WebParamsEncoding) { + std::string encoded = WebParamsEncode({{"q", "test"}, + {"path", "/usr/bin"}, + {"#", "%"}}); + EXPECT_EQ("q=test&path=%2Fusr%2Fbin&%23=%25", encoded); + + auto params = WebParamsDecode(encoded); + EXPECT_EQ(3, params.size()); + EXPECT_EQ("q", params[0].first); + EXPECT_EQ("test", params[0].second); + EXPECT_EQ("path", params[1].first); + EXPECT_EQ("/usr/bin", params[1].second); + EXPECT_EQ("#", params[2].first); + EXPECT_EQ("%", params[2].second); +}
diff --git a/buffet/dbus/org.chromium.Buffet.conf b/buffet/dbus/org.chromium.Buffet.conf new file mode 100644 index 0000000..0f6ea4d --- /dev/null +++ b/buffet/dbus/org.chromium.Buffet.conf
@@ -0,0 +1,13 @@ +<!DOCTYPE busconfig PUBLIC + "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <policy user="root"> + <allow own="org.chromium.Buffet" /> + <allow send_destination="org.chromium.Buffet" /> + </policy> + + <policy context="default"> + <allow send_destination="org.chromium.Buffet" /> + </policy> +</busconfig> \ No newline at end of file
diff --git a/buffet/dbus_constants.cc b/buffet/dbus_constants.cc new file mode 100644 index 0000000..d8c70ba --- /dev/null +++ b/buffet/dbus_constants.cc
@@ -0,0 +1,27 @@ +// 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/dbus_constants.h" + +namespace buffet { + +namespace dbus_constants { + +const char kServiceName[] = "org.chromium.Buffet"; + +const char kRootServicePath[] = "/org/chromium/Buffet"; + +const char kManagerInterface[] = "org.chromium.Buffet.Manager"; +const char kManagerServicePath[] = "/org/chromium/Buffet/Manager"; + +const char kManagerCheckDeviceRegistered[] = "CheckDeviceRegistered"; +const char kManagerGetDeviceInfo[] = "GetDeviceInfo"; +const char kManagerStartRegisterDevice[] = "StartRegisterDevice"; +const char kManagerFinishRegisterDevice[] = "FinishRegisterDevice"; +const char kManagerUpdateStateMethod[] = "UpdateState"; +const char kManagerTestMethod[] = "TestMethod"; + +} // namespace dbus_constants + +} // namespace buffet
diff --git a/buffet/dbus_constants.h b/buffet/dbus_constants.h new file mode 100644 index 0000000..b7e9239 --- /dev/null +++ b/buffet/dbus_constants.h
@@ -0,0 +1,34 @@ +// 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_DBUS_CONSTANTS_H_ +#define BUFFET_DBUS_CONSTANTS_H_ + +namespace buffet { + +namespace dbus_constants { + +// The service name claimed by the Buffet daemon. +extern const char kServiceName[]; + +// The object at this path implements the ObjectManager interface. +extern const char kRootServicePath[]; + +// Interface implemented by the object at kManagerServicePath. +extern const char kManagerInterface[]; +extern const char kManagerServicePath[]; + +// Methods exposed as part of kManagerInterface. +extern const char kManagerCheckDeviceRegistered[]; +extern const char kManagerGetDeviceInfo[]; +extern const char kManagerStartRegisterDevice[]; +extern const char kManagerFinishRegisterDevice[]; +extern const char kManagerUpdateStateMethod[]; +extern const char kManagerTestMethod[]; + +} // namespace dbus_constants + +} // namespace buffet + +#endif // BUFFET_DBUS_CONSTANTS_H_
diff --git a/buffet/dbus_utils.cc b/buffet/dbus_utils.cc new file mode 100644 index 0000000..58f0b26 --- /dev/null +++ b/buffet/dbus_utils.cc
@@ -0,0 +1,63 @@ +// 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/dbus_utils.h" + +#include <base/logging.h> +#include <base/bind.h> + +namespace buffet { + +namespace dbus_utils { + +namespace { + +// Passes |method_call| to |handler| and passes the response to +// |response_sender|. If |handler| returns NULL, an empty response is created +// and sent. +void HandleSynchronousDBusMethodCall( + base::Callback<scoped_ptr<dbus::Response>(dbus::MethodCall*)> handler, + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) { + auto response = handler.Run(method_call); + if (!response) + response = dbus::Response::FromMethodCall(method_call); + + response_sender.Run(response.Pass()); +} + +} // namespace + +scoped_ptr<dbus::Response> GetBadArgsError(dbus::MethodCall* method_call, + const std::string& message) { + LOG(ERROR) << "Error while handling DBus call: " << message; + scoped_ptr<dbus::ErrorResponse> resp(dbus::ErrorResponse::FromMethodCall( + method_call, "org.freedesktop.DBus.Error.InvalidArgs", message)); + return scoped_ptr<dbus::Response>(resp.release()); +} + +scoped_ptr<dbus::Response> GetDBusError(dbus::MethodCall* method_call, + const Error* error) { + std::string message; + while (error) { + // Format error string as "domain/code:message". + if (!message.empty()) + message += ';'; + message += error->GetDomain() + '/' + error->GetCode() + ':' + + error->GetMessage(); + error = error->GetInnerError(); + } + scoped_ptr<dbus::ErrorResponse> resp(dbus::ErrorResponse::FromMethodCall( + method_call, "org.freedesktop.DBus.Error.Failed", message)); + return scoped_ptr<dbus::Response>(resp.release()); +} + +dbus::ExportedObject::MethodCallCallback GetExportableDBusMethod( + base::Callback<scoped_ptr<dbus::Response>(dbus::MethodCall*)> handler) { + return base::Bind(&HandleSynchronousDBusMethodCall, handler); +} + +} // namespace dbus_utils + +} // namespace buffet
diff --git a/buffet/dbus_utils.h b/buffet/dbus_utils.h new file mode 100644 index 0000000..6314531 --- /dev/null +++ b/buffet/dbus_utils.h
@@ -0,0 +1,35 @@ +// 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_DBUS_UTILS_H_ +#define BUFFET_DBUS_UTILS_H_ + +#include <string> + +#include <base/memory/scoped_ptr.h> +#include <dbus/exported_object.h> +#include <dbus/message.h> + +#include "buffet/error.h" + +namespace buffet { + +namespace dbus_utils { + +scoped_ptr<dbus::Response> GetBadArgsError(dbus::MethodCall* method_call, + const std::string& message); + +scoped_ptr<dbus::Response> GetDBusError(dbus::MethodCall* method_call, + const Error* error); + + +dbus::ExportedObject::MethodCallCallback GetExportableDBusMethod( + base::Callback<scoped_ptr<dbus::Response>(dbus::MethodCall*)> handler); + +} // namespace dbus_utils + +} // namespace buffet + +#endif // BUFFET_DBUS_UTILS_H_ +
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc new file mode 100644 index 0000000..4aab715 --- /dev/null +++ b/buffet/device_registration_info.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/device_registration_info.h" + +#include <memory> +#include <utility> +#include <vector> + +#include <base/json/json_writer.h> +#include <base/values.h> + +#include "buffet/data_encoding.h" +#include "buffet/device_registration_storage_keys.h" +#include "buffet/storage_impls.h" +#include "buffet/http_transport_curl.h" +#include "buffet/http_utils.h" +#include "buffet/mime_utils.h" +#include "buffet/string_utils.h" +#include "buffet/url_utils.h" + +const char buffet::kErrorDomainOAuth2[] = "oauth2"; +const char buffet::kErrorDomainGCD[] = "gcd"; +const char buffet::kErrorDomainGCDServer[] = "gcd_server"; +const char buffet::kErrorDomainBuffet[] = "buffet"; + +namespace buffet { +namespace storage_keys { + +// Persistent keys +const char kClientId[] = "client_id"; +const char kClientSecret[] = "client_secret"; +const char kApiKey[] = "api_key"; +const char kRefreshToken[] = "refresh_token"; +const char kDeviceId[] = "device_id"; +const char kOAuthURL[] = "oauth_url"; +const char kServiceURL[] = "service_url"; +const char kRobotAccount[] = "robot_account"; +// Transient keys +const char kDeviceKind[] = "device_kind"; +const char kSystemName[] = "system_name"; +const char kDisplayName[] = "display_name"; + +} // namespace storage_keys +} // namespace buffet + +namespace { + +const base::FilePath::CharType kDeviceInfoFilePath[] = + FILE_PATH_LITERAL("/var/lib/buffet/device_reg_info"); + +bool GetParamValue( + const std::map<std::string, std::shared_ptr<base::Value>>& params, + const std::string& param_name, + std::string* param_value) { + auto p = params.find(param_name); + if (p == params.end()) + return false; + + return p->second->GetAsString(param_value); +} + +std::pair<std::string, std::string> BuildAuthHeader( + const std::string& access_token_type, + const std::string& access_token) { + std::string authorization = + buffet::string_utils::Join(' ', access_token_type, access_token); + // Linter doesn't like the ; after } on the following line... + return {buffet::http::request_header::kAuthorization, + authorization}; // NOLINT +} + +std::unique_ptr<base::DictionaryValue> ParseOAuthResponse( + const buffet::http::Response* response, buffet::ErrorPtr* error) { + int code = 0; + auto resp = buffet::http::ParseJsonResponse(response, &code, error); + if (resp && code >= buffet::http::status_code::BadRequest) { + if (error) { + std::string error_code, error_message; + if (resp->GetString("error", &error_code) && + resp->GetString("error_description", &error_message)) { + buffet::Error::AddTo(error, buffet::kErrorDomainOAuth2, error_code, + error_message); + } else { + buffet::Error::AddTo(error, buffet::kErrorDomainOAuth2, + "unexpected_response", "Unexpected OAuth error"); + } + } + return std::unique_ptr<base::DictionaryValue>(); + } + return resp; +} + +inline void SetUnexpectedError(buffet::ErrorPtr* error) { + buffet::Error::AddTo(error, buffet::kErrorDomainGCD, "unexpected_response", + "Unexpected GCD error"); +} + +void ParseGCDError(const base::DictionaryValue* json, buffet::ErrorPtr* error) { + if (!error) + return; + + const base::Value* list_value = nullptr; + const base::ListValue* error_list = nullptr; + if (!json->Get("error.errors", &list_value) || + !list_value->GetAsList(&error_list)) { + SetUnexpectedError(error); + return; + } + + for (size_t i = 0; i < error_list->GetSize(); i++) { + const base::Value* error_value = nullptr; + const base::DictionaryValue* error_object = nullptr; + if (!error_list->Get(i, &error_value) || + !error_value->GetAsDictionary(&error_object)) { + SetUnexpectedError(error); + continue; + } + std::string error_code, error_message; + if (error_object->GetString("reason", &error_code) && + error_object->GetString("message", &error_message)) { + buffet::Error::AddTo(error, buffet::kErrorDomainGCDServer, + error_code, error_message); + } else { + SetUnexpectedError(error); + } + } +} + +std::string BuildURL(const std::string& url, + const std::vector<std::string>& subpaths, + const buffet::data_encoding::WebParamList& params) { + std::string result = buffet::url::CombineMultiple(url, subpaths); + return buffet::url::AppendQueryParams(result, params); +} + +} // anonymous namespace + +namespace buffet { + +DeviceRegistrationInfo::DeviceRegistrationInfo() + : transport_(new http::curl::Transport()), + // TODO(avakulenko): Figure out security implications of storing + // this data unencrypted. + storage_(new FileStorage(base::FilePath(kDeviceInfoFilePath))) { +} + +DeviceRegistrationInfo::DeviceRegistrationInfo( + std::shared_ptr<http::Transport> transport, + std::shared_ptr<StorageInterface> storage) : transport_(transport), + storage_(storage) { +} + +std::pair<std::string, std::string> + DeviceRegistrationInfo::GetAuthorizationHeader() const { + return BuildAuthHeader("Bearer", access_token_); +} + +std::string DeviceRegistrationInfo::GetServiceURL( + const std::string& subpath, + const data_encoding::WebParamList& params) const { + return BuildURL(service_url_, {subpath}, params); +} + +std::string DeviceRegistrationInfo::GetDeviceURL( + const std::string& subpath, + const data_encoding::WebParamList& params) const { + CHECK(!device_id_.empty()) << "Must have a valid device ID"; + return BuildURL(service_url_, {"devices", device_id_, subpath}, params); +} + +std::string DeviceRegistrationInfo::GetOAuthURL( + const std::string& subpath, + const data_encoding::WebParamList& params) const { + return BuildURL(oauth_url_, {subpath}, params); +} + +std::string DeviceRegistrationInfo::GetDeviceId(ErrorPtr* error) { + return CheckRegistration(error) ? device_id_ : std::string(); +} + +bool DeviceRegistrationInfo::Load() { + auto value = storage_->Load(); + const base::DictionaryValue* dict = nullptr; + if (!value || !value->GetAsDictionary(&dict)) + return false; + + // Get the values into temp variables first to make sure we can get + // all the data correctly before changing the state of this object. + std::string client_id; + if (!dict->GetString(storage_keys::kClientId, &client_id)) + return false; + std::string client_secret; + if (!dict->GetString(storage_keys::kClientSecret, &client_secret)) + return false; + std::string api_key; + if (!dict->GetString(storage_keys::kApiKey, &api_key)) + return false; + std::string refresh_token; + if (!dict->GetString(storage_keys::kRefreshToken, &refresh_token)) + return false; + std::string device_id; + if (!dict->GetString(storage_keys::kDeviceId, &device_id)) + return false; + std::string oauth_url; + if (!dict->GetString(storage_keys::kOAuthURL, &oauth_url)) + return false; + std::string service_url; + if (!dict->GetString(storage_keys::kServiceURL, &service_url)) + return false; + std::string device_robot_account; + if (!dict->GetString(storage_keys::kRobotAccount, &device_robot_account)) + return false; + + client_id_ = client_id; + client_secret_ = client_secret; + api_key_ = api_key; + refresh_token_ = refresh_token; + device_id_ = device_id; + oauth_url_ = oauth_url; + service_url_ = service_url; + device_robot_account_ = device_robot_account; + return true; +} + +bool DeviceRegistrationInfo::Save() const { + base::DictionaryValue dict; + dict.SetString(storage_keys::kClientId, client_id_); + dict.SetString(storage_keys::kClientSecret, client_secret_); + dict.SetString(storage_keys::kApiKey, api_key_); + dict.SetString(storage_keys::kRefreshToken, refresh_token_); + dict.SetString(storage_keys::kDeviceId, device_id_); + dict.SetString(storage_keys::kOAuthURL, oauth_url_); + dict.SetString(storage_keys::kServiceURL, service_url_); + dict.SetString(storage_keys::kRobotAccount, device_robot_account_); + return storage_->Save(&dict); +} + +bool DeviceRegistrationInfo::CheckRegistration(ErrorPtr* error) { + LOG(INFO) << "Checking device registration record."; + if (refresh_token_.empty() || + device_id_.empty() || + device_robot_account_.empty()) { + LOG(INFO) << "No valid device registration record found."; + Error::AddTo(error, kErrorDomainGCD, "device_not_registered", + "No valid device registration record found"); + return false; + } + + LOG(INFO) << "Device registration record found."; + return ValidateAndRefreshAccessToken(error); +} + +bool DeviceRegistrationInfo::ValidateAndRefreshAccessToken(ErrorPtr* error) { + LOG(INFO) << "Checking access token expiration."; + if (!access_token_.empty() && + !access_token_expiration_.is_null() && + access_token_expiration_ > base::Time::Now()) { + LOG(INFO) << "Access token is still valid."; + return true; + } + + auto response = http::PostFormData(GetOAuthURL("token"), { + {"refresh_token", refresh_token_}, + {"client_id", client_id_}, + {"client_secret", client_secret_}, + {"grant_type", "refresh_token"}, + }, transport_, error); + if (!response) + return false; + + auto json = ParseOAuthResponse(response.get(), error); + if (!json) + return false; + + int expires_in = 0; + if (!json->GetString("access_token", &access_token_) || + !json->GetInteger("expires_in", &expires_in) || + access_token_.empty() || + expires_in <= 0) { + LOG(ERROR) << "Access token unavailable."; + Error::AddTo(error, kErrorDomainOAuth2, "unexpected_server_response", + "Access token unavailable"); + return false; + } + + access_token_expiration_ = base::Time::Now() + + base::TimeDelta::FromSeconds(expires_in); + + LOG(INFO) << "Access token is refreshed for additional " << expires_in + << " seconds."; + return true; +} + +std::unique_ptr<base::Value> DeviceRegistrationInfo::GetDeviceInfo( + ErrorPtr* error) { + if (!CheckRegistration(error)) + return std::unique_ptr<base::Value>(); + + auto response = http::Get(GetDeviceURL(), + {GetAuthorizationHeader()}, transport_, error); + int status_code = 0; + std::unique_ptr<base::DictionaryValue> json = + http::ParseJsonResponse(response.get(), &status_code, error); + if (json) { + if (status_code >= http::status_code::BadRequest) { + LOG(WARNING) << "Failed to retrieve the device info. Response code = " + << status_code; + ParseGCDError(json.get(), error); + return std::unique_ptr<base::Value>(); + } + } + return std::unique_ptr<base::Value>(json.release()); +} + +bool CheckParam(const std::string& param_name, + const std::string& param_value, + ErrorPtr* error) { + if (!param_value.empty()) + return true; + + Error::AddTo(error, kErrorDomainBuffet, "missing_parameter", + "Parameter " + param_name + " not specified"); + return false; +} + +std::string DeviceRegistrationInfo::StartRegistration( + const std::map<std::string, std::shared_ptr<base::Value>>& params, + ErrorPtr* error) { + GetParamValue(params, storage_keys::kClientId, &client_id_); + GetParamValue(params, storage_keys::kClientSecret, &client_secret_); + GetParamValue(params, storage_keys::kApiKey, &api_key_); + GetParamValue(params, storage_keys::kDeviceId, &device_id_); + GetParamValue(params, storage_keys::kDeviceKind, &device_kind_); + GetParamValue(params, storage_keys::kSystemName, &system_name_); + GetParamValue(params, storage_keys::kDisplayName, &display_name_); + GetParamValue(params, storage_keys::kOAuthURL, &oauth_url_); + GetParamValue(params, storage_keys::kServiceURL, &service_url_); + + if (!CheckParam(storage_keys::kClientId, client_id_, error)) + return std::string(); + if (!CheckParam(storage_keys::kClientSecret, client_secret_, error)) + return std::string(); + if (!CheckParam(storage_keys::kApiKey, api_key_, error)) + return std::string(); + if (!CheckParam(storage_keys::kDeviceKind, device_kind_, error)) + return std::string(); + if (!CheckParam(storage_keys::kSystemName, system_name_, error)) + return std::string(); + if (!CheckParam(storage_keys::kOAuthURL, oauth_url_, error)) + return std::string(); + if (!CheckParam(storage_keys::kServiceURL, service_url_, error)) + return std::string(); + + std::vector<std::pair<std::string, std::vector<std::string>>> commands = { + {"SetDeviceConfiguration", {"data"}} + }; + + base::DictionaryValue req_json; + base::ListValue* set_device_configuration_params = new base::ListValue; + base::DictionaryValue* param1 = new base::DictionaryValue; + param1->SetString("name", "data"); + set_device_configuration_params->Append(param1); + + base::ListValue* vendor_commands = new base::ListValue; + for (auto&& pair : commands) { + base::ListValue* params = new base::ListValue; + for (auto&& param_name : pair.second) { + base::DictionaryValue* param = new base::DictionaryValue; + param->SetString("name", param_name); + params->Append(param); + } + base::DictionaryValue* command = new base::DictionaryValue; + command->SetString("name", pair.first); + command->Set("parameter", params); + vendor_commands->Append(command); + } + + req_json.SetString("oauthClientId", client_id_); + req_json.SetString("deviceDraft.deviceKind", device_kind_); + req_json.SetString("deviceDraft.systemName", system_name_); + req_json.SetString("deviceDraft.displayName", display_name_); + req_json.SetString("deviceDraft.channel.supportedType", "xmpp"); + req_json.Set("deviceDraft.commands.base.vendorCommands", vendor_commands); + + std::string url = GetServiceURL("registrationTickets", {{"key", api_key_}}); + auto resp_json = http::ParseJsonResponse( + http::PostJson(url, &req_json, transport_, error).get(), nullptr, error); + if (!resp_json) + return std::string(); + + if (!resp_json->GetString("id", &ticket_id_)) { + Error::AddTo(error, kErrorDomainGCD, "unexpected_response", + "Device ID missing"); + return std::string(); + } + + std::string auth_url = GetOAuthURL("auth", { + {"scope", "https://www.googleapis.com/auth/clouddevices"}, + {"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, + {"response_type", "code"}, + {"client_id", client_id_} + }); + + base::DictionaryValue json; + json.SetString("ticket_id", ticket_id_); + json.SetString("auth_url", auth_url); + + std::string ret; + base::JSONWriter::Write(&json, &ret); + return ret; +} + +bool DeviceRegistrationInfo::FinishRegistration( + const std::string& user_auth_code, ErrorPtr* error) { + if (ticket_id_.empty()) { + LOG(ERROR) << "Finish registration without ticket ID"; + Error::AddTo(error, kErrorDomainBuffet, "registration_not_started", + "Device registration not started"); + return false; + } + + std::string url = GetServiceURL("registrationTickets/" + ticket_id_); + std::unique_ptr<http::Response> response; + if (!user_auth_code.empty()) { + response = http::PostFormData(GetOAuthURL("token"), { + {"code", user_auth_code}, + {"client_id", client_id_}, + {"client_secret", client_secret_}, + {"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, + {"grant_type", "authorization_code"} + }, transport_, error); + if (!response) + return false; + + auto json_resp = ParseOAuthResponse(response.get(), error); + if (!json_resp) + return false; + + std::string user_access_token; + std::string token_type; + if (!json_resp->GetString("access_token", &user_access_token) || + !json_resp->GetString("token_type", &token_type)) { + Error::AddTo(error, kErrorDomainOAuth2, "unexpected_response", + "User access_token is missing in response"); + return false; + } + + base::DictionaryValue user_info; + user_info.SetString("userEmail", "me"); + response = http::PatchJson( + url, &user_info, {BuildAuthHeader(token_type, user_access_token)}, + transport_, error); + + auto json = http::ParseJsonResponse(response.get(), nullptr, error); + if (!json) + return false; + } + + std::string auth_code; + url += "/finalize?key=" + api_key_; + LOG(INFO) << "Sending request to: " << url; + response = http::PostBinary(url, nullptr, 0, transport_, error); + if (!response) + return false; + auto json_resp = http::ParseJsonResponse(response.get(), nullptr, error); + if (!json_resp) + return false; + if (!response->IsSuccessful()) { + ParseGCDError(json_resp.get(), error); + return false; + } + if (!json_resp->GetString("robotAccountEmail", &device_robot_account_) || + !json_resp->GetString("robotAccountAuthorizationCode", &auth_code) || + !json_resp->GetString("deviceDraft.id", &device_id_)) { + Error::AddTo(error, kErrorDomainGCD, "unexpected_response", + "Device account missing in response"); + return false; + } + + // Now get access_token and refresh_token + response = http::PostFormData(GetOAuthURL("token"), { + {"code", auth_code}, + {"client_id", client_id_}, + {"client_secret", client_secret_}, + {"redirect_uri", "oob"}, + {"scope", "https://www.googleapis.com/auth/clouddevices"}, + {"grant_type", "authorization_code"} + }, transport_, error); + if (!response) + return false; + + json_resp = ParseOAuthResponse(response.get(), error); + int expires_in = 0; + if (!json_resp || + !json_resp->GetString("access_token", &access_token_) || + !json_resp->GetString("refresh_token", &refresh_token_) || + !json_resp->GetInteger("expires_in", &expires_in) || + access_token_.empty() || + refresh_token_.empty() || + expires_in <= 0) { + Error::AddTo(error, kErrorDomainGCD, "unexpected_response", + "Device access_token missing in response"); + return false; + } + + access_token_expiration_ = base::Time::Now() + + base::TimeDelta::FromSeconds(expires_in); + + Save(); + return true; +} + +} // namespace buffet
diff --git a/buffet/device_registration_info.h b/buffet/device_registration_info.h new file mode 100644 index 0000000..da0d409 --- /dev/null +++ b/buffet/device_registration_info.h
@@ -0,0 +1,146 @@ +// 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_DEVICE_REGISTRATION_INFO_H_ +#define BUFFET_DEVICE_REGISTRATION_INFO_H_ + +#include <string> +#include <map> +#include <memory> +#include <utility> + +#include <base/basictypes.h> +#include <base/time/time.h> + +#include "buffet/data_encoding.h" +#include "buffet/error.h" +#include "buffet/http_transport.h" +#include "buffet/storage_interface.h" + +namespace base { + class Value; +} // namespace base + +namespace buffet { + +extern const char kErrorDomainOAuth2[]; +extern const char kErrorDomainGCD[]; +extern const char kErrorDomainGCDServer[]; +extern const char kErrorDomainBuffet[]; + +// The DeviceRegistrationInfo class represents device registration information. +class DeviceRegistrationInfo { + public: + // This is a helper class for unit testing. + class TestHelper; + // Default-constructed uses CURL HTTP transport. + DeviceRegistrationInfo(); + // This constructor allows to pass in a custom HTTP transport + // (mainly for testing). + DeviceRegistrationInfo(std::shared_ptr<http::Transport> transport, + std::shared_ptr<StorageInterface> storage); + + // Returns the authorization HTTP header that can be used to talk + // to GCD server for authenticated device communication. + // Make sure ValidateAndRefreshAccessToken() is called before this call. + std::pair<std::string, std::string> GetAuthorizationHeader() const; + + // Returns the GCD service request URL. If |subpath| is specified, it is + // appended to the base URL which is normally + // https://www.googleapis.com/clouddevices/v1/". + // If |params| are specified, each key-value pair is formatted using + // data_encoding::WebParamsEncode() and appended to URL as a query + // string. + // So, calling: + // GetServiceURL("ticket", {{"key","apiKey"}}) + // will return something like: + // https://www.googleapis.com/clouddevices/v1/ticket?key=apiKey + std::string GetServiceURL( + const std::string& subpath = {}, + const data_encoding::WebParamList& params = {}) const; + + // Returns a service URL to access the registered device on GCD server. + // The base URL used to construct the full URL looks like this: + // https://www.googleapis.com/clouddevices/v1/devices/<device_id>/ + std::string GetDeviceURL( + const std::string& subpath = {}, + const data_encoding::WebParamList& params = {}) const; + + // Similar to GetServiceURL, GetOAuthURL() returns a URL of OAuth 2.0 server. + // The base URL used is https://accounts.google.com/o/oauth2/. + std::string GetOAuthURL( + const std::string& subpath = {}, + const data_encoding::WebParamList& params = {}) const; + + // Returns the registered device ID (GUID) or empty string if failed + std::string GetDeviceId(ErrorPtr* error); + + // Loads the device registration information from cache. + bool Load(); + + // Checks for the valid device registration as well as refreshes + // the device access token, if available. + bool CheckRegistration(ErrorPtr* error); + + // Gets the full device description JSON object, or nullptr if + // the device is not registered or communication failure. + std::unique_ptr<base::Value> GetDeviceInfo(ErrorPtr* error); + + // Starts device registration procedure. |params| are a list of + // key-value pairs of device information, such as client_id, client_secret, + // and so on. If a particular key-value pair is omitted, a default value + // is used when possible. Returns a device claim ID on success. + std::string StartRegistration( + const std::map<std::string, std::shared_ptr<base::Value>>& params, + ErrorPtr* error); + + // Finalizes the device registration. If |user_auth_code| is provided, then + // the device record is populated with user email on user's behalf. Otherwise + // the user is responsible to issue a PATCH request to provide a valid + // email address before calling FinishRegistration. + bool FinishRegistration(const std::string& user_auth_code, + ErrorPtr* error); + + private: + // Saves the device registration to cache. + bool Save() const; + + // Makes sure the access token is available and up-to-date. + bool ValidateAndRefreshAccessToken(ErrorPtr* error); + + // Persistent data. Some of default values for testing purposes are used. + // TODO(avakulenko): remove these default values in the future. + // http://crbug.com/364692 + std::string client_id_ = + "583509257718-lnmeofvjef3b1tm33sbjmckfnumfvn8j.apps.googleusercontent.com"; + std::string client_secret_ = "6fzZwQhgnsHhvYYvvFdpv5SD"; + std::string api_key_ = "AIzaSyAp7KVig5m9g4LWWKr79mTS8sXWfUU6w9g"; + std::string refresh_token_; + std::string device_id_; + std::string device_robot_account_; + std::string oauth_url_ = "https://accounts.google.com/o/oauth2/"; + std::string service_url_ = + "https://www-googleapis-staging.sandbox.google.com/" + "clouddevices/v1/"; + + // Transient data + std::string access_token_; + base::Time access_token_expiration_; + std::string ticket_id_; + std::string device_kind_ = "vendor"; + std::string system_name_ = "coffee_pot"; + std::string display_name_ = "Coffee Pot"; + + // HTTP transport used for communications. + std::shared_ptr<http::Transport> transport_; + // Serialization interface to save and load device registration info. + std::shared_ptr<StorageInterface> storage_; + + friend class TestHelper; + DISALLOW_COPY_AND_ASSIGN(DeviceRegistrationInfo); +}; + +} // namespace buffet + +#endif // BUFFET_DEVICE_REGISTRATION_INFO_H_
diff --git a/buffet/device_registration_info_unittest.cc b/buffet/device_registration_info_unittest.cc new file mode 100644 index 0000000..5108e35 --- /dev/null +++ b/buffet/device_registration_info_unittest.cc
@@ -0,0 +1,392 @@ +// 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 <base/json/json_reader.h> +#include <base/values.h> +#include <gtest/gtest.h> + +#include "buffet/bind_lambda.h" +#include "buffet/device_registration_info.h" +#include "buffet/device_registration_storage_keys.h" +#include "buffet/http_request.h" +#include "buffet/http_transport_fake.h" +#include "buffet/mime_utils.h" +#include "buffet/storage_impls.h" + +using namespace buffet; // NOLINT(build/namespaces) +using namespace buffet::http; // NOLINT(build/namespaces) + +namespace { + +namespace test_data { + +const char kServiceURL[] = "http://gcd.server.com/"; +const char kOAuthURL[] = "http://oauth.server.com/"; +const char kApiKey[] = "GOadRdTf9FERf0k4w6EFOof56fUJ3kFDdFL3d7f"; +const char kClientId[] = "123543821385-sfjkjshdkjhfk234sdfsdfkskd" + "fkjh7f.apps.googleusercontent.com"; +const char kClientSecret[] = "5sdGdGlfolGlrFKfdFlgP6FG"; +const char kDeviceId[] = "4a7ea2d1-b331-1e1f-b206-e863c7635196"; +const char kClaimTicketId[] = "RTcUE"; +const char kAccessToken[] = "ya29.1.AADtN_V-dLUM-sVZ0qVjG9Dxm5NgdS9J" + "Mx_JLUqhC9bED_YFjzHZtYt65ZzXCS35NMAeaVZ" + "Dei530-w0yE2urpQ"; +const char kRefreshToken[] = "1/zQmxR6PKNvhcxf9SjXUrCjcmCrcqRKXctc6cp" + "1nI-GQ"; +const char kRobotAccountAuthCode[] = "4/Mf_ujEhPejVhOq-OxW9F5cSOnWzx." + "YgciVjTYGscRshQV0ieZDAqiTIjMigI"; +const char kRobotAccountEmail[] = "6ed0b3f54f9bd619b942f4ad2441c252@" + "clouddevices.gserviceaccount.com"; +const char kUserAccountAuthCode[] = "2/sd_GD1TGFKpJOLJ34-0g5fK0fflp.GlT" + "I0F5g7hNtFgj5HFGOf8FlGK9eflO"; +const char kUserAccessToken[] = "sd56.4.FGDjG_F-gFGF-dFG6gGOG9Dxm5NgdS9" + "JMx_JLUqhC9bED_YFjLKjlkjLKJlkjLKjlKJea" + "VZDei530-w0yE2urpQ"; +const char kUserRefreshToken[] = "1/zQLKjlKJlkLkLKjLkjLKjLkjLjLkjl0ftc6" + "cp1nI-GQ"; + +} // namespace test_data + +// Fill in the storage with default environment information (URLs, etc). +void InitDefaultStorage(base::DictionaryValue* data) { + data->SetString(storage_keys::kClientId, test_data::kClientId); + data->SetString(storage_keys::kClientSecret, test_data::kClientSecret); + data->SetString(storage_keys::kApiKey, test_data::kApiKey); + data->SetString(storage_keys::kRefreshToken, ""); + data->SetString(storage_keys::kDeviceId, ""); + data->SetString(storage_keys::kOAuthURL, test_data::kOAuthURL); + data->SetString(storage_keys::kServiceURL, test_data::kServiceURL); + data->SetString(storage_keys::kRobotAccount, ""); +} + +// Add the test device registration information. +void SetDefaultDeviceRegistration(base::DictionaryValue* data) { + data->SetString(storage_keys::kRefreshToken, test_data::kRefreshToken); + data->SetString(storage_keys::kDeviceId, test_data::kDeviceId); + data->SetString(storage_keys::kRobotAccount, test_data::kRobotAccountEmail); +} + +void OAuth2Handler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + base::DictionaryValue json; + if (request.GetFormField("grant_type") == "refresh_token") { + // Refresh device access token. + EXPECT_EQ(test_data::kRefreshToken, request.GetFormField("refresh_token")); + EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id")); + EXPECT_EQ(test_data::kClientSecret, request.GetFormField("client_secret")); + json.SetString("access_token", test_data::kAccessToken); + } else if (request.GetFormField("grant_type") == "authorization_code") { + // Obtain access token. + std::string code = request.GetFormField("code"); + if (code == test_data::kUserAccountAuthCode) { + // Get user access token. + EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id")); + EXPECT_EQ(test_data::kClientSecret, + request.GetFormField("client_secret")); + EXPECT_EQ("urn:ietf:wg:oauth:2.0:oob", + request.GetFormField("redirect_uri")); + json.SetString("access_token", test_data::kUserAccessToken); + json.SetString("token_type", "Bearer"); + json.SetString("refresh_token", test_data::kUserRefreshToken); + } else if (code == test_data::kRobotAccountAuthCode) { + // Get device access token. + EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id")); + EXPECT_EQ(test_data::kClientSecret, + request.GetFormField("client_secret")); + EXPECT_EQ("oob", request.GetFormField("redirect_uri")); + EXPECT_EQ("https://www.googleapis.com/auth/clouddevices", + request.GetFormField("scope")); + json.SetString("access_token", test_data::kAccessToken); + json.SetString("token_type", "Bearer"); + json.SetString("refresh_token", test_data::kRefreshToken); + } else { + FAIL() << "Unexpected authorization code"; + } + } else { + FAIL() << "Unexpected grant type"; + } + json.SetInteger("expires_in", 3600); + response->ReplyJson(status_code::Ok, &json); +} + +void DeviceInfoHandler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + std::string auth = "Bearer "; + auth += test_data::kAccessToken; + EXPECT_EQ(auth, request.GetHeader(http::request_header::kAuthorization)); + response->ReplyJson(status_code::Ok, { + {"channel.supportedType", "xmpp"}, + {"deviceKind", "vendor"}, + {"id", test_data::kDeviceId}, + {"kind", "clouddevices#device"}, + }); +} + +void FinalizeTicketHandler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(test_data::kApiKey, request.GetFormField("key")); + EXPECT_TRUE(request.GetData().empty()); + + response->ReplyJson(status_code::Ok, { + {"id", test_data::kClaimTicketId}, + {"kind", "clouddevices#registrationTicket"}, + {"oauthClientId", test_data::kClientId}, + {"userEmail", "user@email.com"}, + {"deviceDraft.id", test_data::kDeviceId}, + {"deviceDraft.kind", "clouddevices#device"}, + {"deviceDraft.channel.supportedType", "xmpp"}, + {"robotAccountEmail", test_data::kRobotAccountEmail}, + {"robotAccountAuthorizationCode", test_data::kRobotAccountAuthCode}, + }); +} + +} // anonymous namespace + +// This is a helper class that allows the unit tests to set the private +// member DeviceRegistrationInfo::ticket_id_, since TestHelper is declared +// as a friend to DeviceRegistrationInfo. +class DeviceRegistrationInfo::TestHelper { + public: + static void SetTestTicketId(DeviceRegistrationInfo* info) { + info->ticket_id_ = test_data::kClaimTicketId; + } +}; + +class DeviceRegistrationInfoTest : public ::testing::Test { + protected: + virtual void SetUp() override { + InitDefaultStorage(&data); + storage = std::make_shared<MemStorage>(); + storage->Save(&data); + transport = std::make_shared<fake::Transport>(); + dev_reg = std::unique_ptr<DeviceRegistrationInfo>( + new DeviceRegistrationInfo(transport, storage)); + } + + base::DictionaryValue data; + std::shared_ptr<MemStorage> storage; + std::shared_ptr<fake::Transport> transport; + std::unique_ptr<DeviceRegistrationInfo> dev_reg; +}; + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(DeviceRegistrationInfoTest, GetServiceURL) { + EXPECT_TRUE(dev_reg->Load()); + EXPECT_EQ(test_data::kServiceURL, dev_reg->GetServiceURL()); + std::string url = test_data::kServiceURL; + url += "registrationTickets"; + EXPECT_EQ(url, dev_reg->GetServiceURL("registrationTickets")); + url += "?key="; + url += test_data::kApiKey; + EXPECT_EQ(url, dev_reg->GetServiceURL("registrationTickets", { + {"key", test_data::kApiKey} + })); + url += "&restart=true"; + EXPECT_EQ(url, dev_reg->GetServiceURL("registrationTickets", { + {"key", test_data::kApiKey}, + {"restart", "true"}, + })); +} + +TEST_F(DeviceRegistrationInfoTest, GetOAuthURL) { + EXPECT_TRUE(dev_reg->Load()); + EXPECT_EQ(test_data::kOAuthURL, dev_reg->GetOAuthURL()); + std::string url = test_data::kOAuthURL; + url += "auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fclouddevices&"; + url += "redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&"; + url += "response_type=code&"; + url += "client_id="; + url += test_data::kClientId; + EXPECT_EQ(url, dev_reg->GetOAuthURL("auth", { + {"scope", "https://www.googleapis.com/auth/clouddevices"}, + {"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, + {"response_type", "code"}, + {"client_id", test_data::kClientId} + })); +} + +TEST_F(DeviceRegistrationInfoTest, CheckRegistration) { + EXPECT_TRUE(dev_reg->Load()); + EXPECT_FALSE(dev_reg->CheckRegistration(nullptr)); + EXPECT_EQ(0, transport->GetRequestCount()); + + SetDefaultDeviceRegistration(&data); + storage->Save(&data); + EXPECT_TRUE(dev_reg->Load()); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + transport->ResetRequestCount(); + EXPECT_TRUE(dev_reg->CheckRegistration(nullptr)); + EXPECT_EQ(1, transport->GetRequestCount()); +} + +TEST_F(DeviceRegistrationInfoTest, GetDeviceInfo) { + SetDefaultDeviceRegistration(&data); + storage->Save(&data); + EXPECT_TRUE(dev_reg->Load()); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + transport->AddHandler(dev_reg->GetDeviceURL(), request_type::kGet, + base::Bind(DeviceInfoHandler)); + transport->ResetRequestCount(); + auto device_info = dev_reg->GetDeviceInfo(nullptr); + EXPECT_EQ(2, transport->GetRequestCount()); + EXPECT_NE(nullptr, device_info.get()); + base::DictionaryValue* dict = nullptr; + EXPECT_TRUE(device_info->GetAsDictionary(&dict)); + std::string id; + EXPECT_TRUE(dict->GetString("id", &id)); + EXPECT_EQ(test_data::kDeviceId, id); +} + +TEST_F(DeviceRegistrationInfoTest, GetDeviceId) { + SetDefaultDeviceRegistration(&data); + storage->Save(&data); + EXPECT_TRUE(dev_reg->Load()); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + transport->AddHandler(dev_reg->GetDeviceURL(), request_type::kGet, + base::Bind(DeviceInfoHandler)); + std::string id = dev_reg->GetDeviceId(nullptr); + EXPECT_EQ(test_data::kDeviceId, id); +} + +TEST_F(DeviceRegistrationInfoTest, StartRegistration) { + EXPECT_TRUE(dev_reg->Load()); + + auto create_ticket = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(test_data::kApiKey, request.GetFormField("key")); + auto json = request.GetDataAsJson(); + EXPECT_NE(nullptr, json.get()); + std::string value; + EXPECT_TRUE(json->GetString("deviceDraft.channel.supportedType", &value)); + EXPECT_EQ("xmpp", value); + EXPECT_TRUE(json->GetString("oauthClientId", &value)); + EXPECT_EQ(test_data::kClientId, value); + EXPECT_TRUE(json->GetString("deviceDraft.deviceKind", &value)); + EXPECT_EQ("vendor", value); + + base::DictionaryValue json_resp; + json_resp.SetString("id", test_data::kClaimTicketId); + json_resp.SetString("kind", "clouddevices#registrationTicket"); + json_resp.SetString("oauthClientId", test_data::kClientId); + base::DictionaryValue* device_draft = nullptr; + EXPECT_TRUE(json->GetDictionary("deviceDraft", &device_draft)); + device_draft = device_draft->DeepCopy(); + device_draft->SetString("id", test_data::kDeviceId); + device_draft->SetString("kind", "clouddevices#device"); + json_resp.Set("deviceDraft", device_draft); + + response->ReplyJson(status_code::Ok, &json_resp); + }; + + transport->AddHandler(dev_reg->GetServiceURL("registrationTickets"), + request_type::kPost, + base::Bind(create_ticket)); + std::map<std::string, std::shared_ptr<base::Value>> params; + std::string json_resp = dev_reg->StartRegistration(params, nullptr); + auto json = std::unique_ptr<base::Value>(base::JSONReader::Read(json_resp)); + EXPECT_NE(nullptr, json.get()); + base::DictionaryValue* dict = nullptr; + EXPECT_TRUE(json->GetAsDictionary(&dict)); + std::string value; + EXPECT_TRUE(dict->GetString("ticket_id", &value)); + EXPECT_EQ(test_data::kClaimTicketId, value); +} + +TEST_F(DeviceRegistrationInfoTest, FinishRegistration_NoAuth) { + // Test finalizing ticket with no user authorization token. + // This assumes that a client would patch in their email separately. + EXPECT_TRUE(dev_reg->Load()); + + // General ticket finalization handler. + std::string ticket_url = + dev_reg->GetServiceURL("registrationTickets/" + + std::string(test_data::kClaimTicketId)); + transport->AddHandler(ticket_url + "/finalize", request_type::kPost, + base::Bind(FinalizeTicketHandler)); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + + storage->reset_save_count(); + DeviceRegistrationInfo::TestHelper::SetTestTicketId(dev_reg.get()); + EXPECT_TRUE(dev_reg->FinishRegistration("", nullptr)); + EXPECT_EQ(1, storage->save_count()); // The device info must have been saved. + EXPECT_EQ(2, transport->GetRequestCount()); + + // Validate the device info saved to storage... + auto storage_data = storage->Load(); + base::DictionaryValue* dict = nullptr; + EXPECT_TRUE(storage_data->GetAsDictionary(&dict)); + std::string value; + EXPECT_TRUE(dict->GetString(storage_keys::kApiKey, &value)); + EXPECT_EQ(test_data::kApiKey, value); + EXPECT_TRUE(dict->GetString(storage_keys::kClientId, &value)); + EXPECT_EQ(test_data::kClientId, value); + EXPECT_TRUE(dict->GetString(storage_keys::kClientSecret, &value)); + EXPECT_EQ(test_data::kClientSecret, value); + EXPECT_TRUE(dict->GetString(storage_keys::kDeviceId, &value)); + EXPECT_EQ(test_data::kDeviceId, value); + EXPECT_TRUE(dict->GetString(storage_keys::kOAuthURL, &value)); + EXPECT_EQ(test_data::kOAuthURL, value); + EXPECT_TRUE(dict->GetString(storage_keys::kRefreshToken, &value)); + EXPECT_EQ(test_data::kRefreshToken, value); + EXPECT_TRUE(dict->GetString(storage_keys::kRobotAccount, &value)); + EXPECT_EQ(test_data::kRobotAccountEmail, value); + EXPECT_TRUE(dict->GetString(storage_keys::kServiceURL, &value)); + EXPECT_EQ(test_data::kServiceURL, value); +} + +TEST_F(DeviceRegistrationInfoTest, FinishRegistration_Auth) { + // Test finalizing ticket with user authorization token. + EXPECT_TRUE(dev_reg->Load()); + + // General ticket finalization handler. + std::string ticket_url = + dev_reg->GetServiceURL("registrationTickets/" + + std::string(test_data::kClaimTicketId)); + transport->AddHandler(ticket_url + "/finalize", request_type::kPost, + base::Bind(FinalizeTicketHandler)); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + + // Handle patching in the user email onto the device record. + auto email_patch_handler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + std::string auth_header = "Bearer "; + auth_header += test_data::kUserAccessToken; + EXPECT_EQ(auth_header, + request.GetHeader(http::request_header::kAuthorization)); + auto json = request.GetDataAsJson(); + EXPECT_NE(nullptr, json.get()); + std::string value; + EXPECT_TRUE(json->GetString("userEmail", &value)); + EXPECT_EQ("me", value); + + response->ReplyJson(status_code::Ok, { + {"id", test_data::kClaimTicketId}, + {"kind", "clouddevices#registrationTicket"}, + {"oauthClientId", test_data::kClientId}, + {"userEmail", "user@email.com"}, + {"deviceDraft.id", test_data::kDeviceId}, + {"deviceDraft.kind", "clouddevices#device"}, + {"deviceDraft.channel.supportedType", "xmpp"}, + }); + }; + transport->AddHandler(ticket_url, request_type::kPatch, + base::Bind(email_patch_handler)); + + storage->reset_save_count(); + DeviceRegistrationInfo::TestHelper::SetTestTicketId(dev_reg.get()); + EXPECT_TRUE(dev_reg->FinishRegistration(test_data::kUserAccountAuthCode, + nullptr)); + EXPECT_EQ(1, storage->save_count()); // The device info must have been saved. + EXPECT_EQ(4, transport->GetRequestCount()); +}
diff --git a/buffet/device_registration_storage_keys.h b/buffet/device_registration_storage_keys.h new file mode 100644 index 0000000..a6c9239 --- /dev/null +++ b/buffet/device_registration_storage_keys.h
@@ -0,0 +1,31 @@ +// 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_DEVICE_REGISTRATION_STORAGE_KEYS_H_ +#define BUFFET_DEVICE_REGISTRATION_STORAGE_KEYS_H_ + +// These are the keys used to identify specific device registration information +// being saved to a storage. Used mostly internally by DeviceRegistrationInfo +// but also exposed so that tests can access them. +namespace buffet { +namespace storage_keys { + +// Persistent keys +extern const char kClientId[]; +extern const char kClientSecret[]; +extern const char kApiKey[]; +extern const char kRefreshToken[]; +extern const char kDeviceId[]; +extern const char kOAuthURL[]; +extern const char kServiceURL[]; +extern const char kRobotAccount[]; +// Transient keys +extern const char kDeviceKind[]; +extern const char kSystemName[]; +extern const char kDisplayName[]; + +} // namespace storage_keys +} // namespace buffet + +#endif // BUFFET_DEVICE_REGISTRATION_STORAGE_KEYS_H_
diff --git a/buffet/error.cc b/buffet/error.cc new file mode 100644 index 0000000..bccb9b8 --- /dev/null +++ b/buffet/error.cc
@@ -0,0 +1,63 @@ +// 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/error.h" + +#include <base/logging.h> + +using buffet::Error; +using buffet::ErrorPtr; + +ErrorPtr Error::Create(const std::string& domain, + const std::string& code, + const std::string& message) { + return Create(domain, code, message, ErrorPtr()); +} + +ErrorPtr Error::Create(const std::string& domain, + const std::string& code, + const std::string& message, + ErrorPtr inner_error) { + LOG(ERROR) << "Error::Create: Domain=" << domain + << ", Code=" << code << ", Message=" << message; + return ErrorPtr(new Error(domain, code, message, std::move(inner_error))); +} + +void Error::AddTo(ErrorPtr* error, const std::string& domain, + const std::string& code, const std::string& message) { + if (error) { + *error = Create(domain, code, message, std::move(*error)); + } else { + // Create already logs the error, but if |error| is nullptr, + // we still want to log the error... + LOG(ERROR) << "Error::Create: Domain=" << domain + << ", Code=" << code << ", Message=" << message; + } +} + +bool Error::HasDomain(const std::string& domain) const { + const Error* err = this; + while (err) { + if (err->GetDomain() == domain) + return true; + err = err->GetInnerError(); + } + return false; +} + +bool Error::HasError(const std::string& domain, const std::string& code) const { + const Error* err = this; + while (err) { + if (err->GetDomain() == domain && err->GetCode() == code) + return true; + err = err->GetInnerError(); + } + return false; +} + +Error::Error(const std::string& domain, const std::string& code, + const std::string& message, ErrorPtr inner_error) : + domain_(domain), code_(code), message_(message), + inner_error_(std::move(inner_error)) { +}
diff --git a/buffet/error.h b/buffet/error.h new file mode 100644 index 0000000..2f5ba66 --- /dev/null +++ b/buffet/error.h
@@ -0,0 +1,71 @@ +// 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_ERROR_H_ +#define BUFFET_ERROR_H_ + +#include <memory> +#include <string> + +#include <base/basictypes.h> + +namespace buffet { + +class Error; // Forward declaration. + +typedef std::unique_ptr<Error> ErrorPtr; + +class Error { + public: + virtual ~Error() = default; + + // Creates an instance of Error class. + static ErrorPtr Create(const std::string& domain, const std::string& code, + const std::string& message); + static ErrorPtr Create(const std::string& domain, const std::string& code, + const std::string& message, ErrorPtr inner_error); + // If |error| is not nullptr, creates another instance of Error class, + // initializes it with specified arguments and adds it to the head of + // the error chain pointed to by |error|. + static void AddTo(ErrorPtr* error, const std::string& domain, + const std::string& code, const std::string& message); + + // Returns the error domain, code and message + const std::string& GetDomain() const { return domain_; } + const std::string& GetCode() const { return code_; } + const std::string& GetMessage() const { return message_; } + + // Checks if this or any of the inner error in the chain has the specified + // error domain. + bool HasDomain(const std::string& domain) const; + // Checks if this or any of the inner error in the chain matches the specified + // error domain and code. + bool HasError(const std::string& domain, const std::string& code) const; + + // Gets a pointer to the inner error, if present. Returns nullptr otherwise. + const Error* GetInnerError() const { return inner_error_.get(); } + + protected: + // Constructor is protected since this object is supposed to be + // created via the Create factory methods. + Error(const std::string& domain, const std::string& code, + const std::string& message, ErrorPtr inner_error); + + // Error domain. The domain defines the scopes for error codes. + // Two errors with the same code but different domains are different errors. + std::string domain_; + // Error code. A unique error code identifier within the given domain. + std::string code_; + // Human-readable error message. + std::string message_; + // Pointer to inner error, if any. This forms a chain of errors. + ErrorPtr inner_error_; + + private: + DISALLOW_COPY_AND_ASSIGN(Error); +}; + +} // namespace buffet + +#endif // BUFFET_ERROR_H_
diff --git a/buffet/error_unittest.cc b/buffet/error_unittest.cc new file mode 100644 index 0000000..cf943ad --- /dev/null +++ b/buffet/error_unittest.cc
@@ -0,0 +1,56 @@ +// 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 <gtest/gtest.h> +#include <base/files/file_path.h> + +#include "buffet/error.h" + +using buffet::Error; + +namespace { + +buffet::ErrorPtr GenerateNetworkError() { + return Error::Create("network", "not_found", "Resource not found"); +} + +buffet::ErrorPtr GenerateHttpError() { + auto inner = GenerateNetworkError(); + return Error::Create("HTTP", "404", "Not found", std::move(inner)); +} + +} // namespace + +TEST(Error, Single) { + auto err = GenerateNetworkError(); + EXPECT_EQ("network", err->GetDomain()); + EXPECT_EQ("not_found", err->GetCode()); + EXPECT_EQ("Resource not found", err->GetMessage()); + EXPECT_EQ(nullptr, err->GetInnerError()); + EXPECT_TRUE(err->HasDomain("network")); + EXPECT_FALSE(err->HasDomain("HTTP")); + EXPECT_FALSE(err->HasDomain("foo")); + EXPECT_TRUE(err->HasError("network", "not_found")); + EXPECT_FALSE(err->HasError("network", "404")); + EXPECT_FALSE(err->HasError("HTTP", "404")); + EXPECT_FALSE(err->HasError("HTTP", "not_found")); + EXPECT_FALSE(err->HasError("foo", "bar")); +} + +TEST(Error, Nested) { + auto err = GenerateHttpError(); + EXPECT_EQ("HTTP", err->GetDomain()); + EXPECT_EQ("404", err->GetCode()); + EXPECT_EQ("Not found", err->GetMessage()); + EXPECT_NE(nullptr, err->GetInnerError()); + EXPECT_EQ("network", err->GetInnerError()->GetDomain()); + EXPECT_TRUE(err->HasDomain("network")); + EXPECT_TRUE(err->HasDomain("HTTP")); + EXPECT_FALSE(err->HasDomain("foo")); + EXPECT_TRUE(err->HasError("network", "not_found")); + EXPECT_FALSE(err->HasError("network", "404")); + EXPECT_TRUE(err->HasError("HTTP", "404")); + EXPECT_FALSE(err->HasError("HTTP", "not_found")); + EXPECT_FALSE(err->HasError("foo", "bar")); +}
diff --git a/buffet/exported_object_manager.cc b/buffet/exported_object_manager.cc new file mode 100644 index 0000000..c1403ad --- /dev/null +++ b/buffet/exported_object_manager.cc
@@ -0,0 +1,133 @@ +// 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/exported_object_manager.h" + +#include <dbus/object_manager.h> + +#include "buffet/async_event_sequencer.h" + +namespace buffet { + +namespace dbus_utils { + +ExportedObjectManager::ExportedObjectManager(scoped_refptr<dbus::Bus> bus, + const dbus::ObjectPath& path) + : bus_(bus), exported_object_(bus->GetExportedObject(path)) {} + +void ExportedObjectManager::Init(const OnInitFinish& cb) { + bus_->AssertOnOriginThread(); + scoped_refptr<dbus_utils::AsyncEventSequencer> sequencer( + new dbus_utils::AsyncEventSequencer()); + exported_object_->ExportMethod( + dbus::kObjectManagerInterface, + dbus::kObjectManagerGetManagedObjects, + base::Bind(&ExportedObjectManager::HandleGetManagedObjects, + AsWeakPtr()), + sequencer->GetExportHandler( + dbus::kObjectManagerInterface, + dbus::kObjectManagerGetManagedObjects, + "Failed exporting GetManagedObjects method of ObjectManager", + false)); + sequencer->OnAllTasksCompletedCall({cb}); +} + +void ExportedObjectManager::ClaimInterface( + const dbus::ObjectPath& path, + const std::string& interface_name, + const PropertyWriter& property_writer) { + bus_->AssertOnOriginThread(); + // We're sending signals that look like: + // org.freedesktop.DBus.ObjectManager.InterfacesAdded ( + // OBJPATH object_path, + // DICT<STRING,DICT<STRING,VARIANT>> interfaces_and_properties); + dbus::Signal signal(dbus::kObjectManagerInterface, + dbus::kObjectManagerInterfacesAdded); + dbus::MessageWriter signal_writer(&signal); + dbus::MessageWriter all_interfaces(&signal); + dbus::MessageWriter each_interface(&signal); + signal_writer.AppendObjectPath(path); + signal_writer.OpenArray("{sa{sv}}", &all_interfaces); + all_interfaces.OpenDictEntry(&each_interface); + each_interface.AppendString(interface_name); + property_writer.Run(&each_interface); + all_interfaces.CloseContainer(&each_interface); + signal_writer.CloseContainer(&all_interfaces); + exported_object_->SendSignal(&signal); + registered_objects_[path][interface_name] = property_writer; +} + +void ExportedObjectManager::ReleaseInterface( + const dbus::ObjectPath& path, const std::string& interface_name) { + bus_->AssertOnOriginThread(); + auto interfaces_for_path_itr = registered_objects_.find(path); + CHECK(interfaces_for_path_itr != registered_objects_.end()) + << "Attempting to signal interface removal for path " << path.value() + << " which was never registered."; + auto interfaces_for_path = interfaces_for_path_itr->second; + auto property_for_interface_itr = interfaces_for_path.find(interface_name); + CHECK(property_for_interface_itr != interfaces_for_path.end()) + << "Attempted to remove interface " << interface_name << " from " + << path.value() << ", but this interface was never registered."; + interfaces_for_path.erase(interface_name); + if (interfaces_for_path.size() < 1) { + registered_objects_.erase(path); + } + // We're sending signals that look like: + // org.freedesktop.DBus.ObjectManager.InterfacesRemoved ( + // OBJPATH object_path, ARRAY<STRING> interfaces); + dbus::Signal signal(dbus::kObjectManagerInterface, + dbus::kObjectManagerInterfacesRemoved); + dbus::MessageWriter signal_writer(&signal); + signal_writer.AppendObjectPath(path); + dbus::MessageWriter interface_writer(nullptr); + signal_writer.OpenArray("s", &interface_writer); + interface_writer.AppendString(interface_name); + signal_writer.CloseContainer(&interface_writer); + exported_object_->SendSignal(&signal); +} + +void ExportedObjectManager::HandleGetManagedObjects( + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) const { + // Implements the GetManagedObjects method: + // + // org.freedesktop.DBus.ObjectManager.GetManagedObjects ( + // out DICT<OBJPATH, + // DICT<STRING, + // DICT<STRING,VARIANT>>> ) + bus_->AssertOnOriginThread(); + scoped_ptr<dbus::Response> response( + dbus::Response::FromMethodCall(method_call)); + dbus::MessageWriter response_writer(response.get()); + dbus::MessageWriter all_object_paths(nullptr); + dbus::MessageWriter each_object_path(nullptr); + dbus::MessageWriter all_interfaces(nullptr); + dbus::MessageWriter each_interface(nullptr); + + response_writer.OpenArray("{oa{sa{sv}}}", &all_object_paths); + for (const auto path_pair : registered_objects_) { + const dbus::ObjectPath& path = path_pair.first; + const InterfaceProperties& interface2properties = path_pair.second; + all_object_paths.OpenDictEntry(&each_object_path); + each_object_path.AppendObjectPath(path); + each_object_path.OpenArray("{sa{sv}}", &all_interfaces); + for (const auto interface : interface2properties) { + const std::string& interface_name = interface.first; + const PropertyWriter& property_writer = interface.second; + all_interfaces.OpenDictEntry(&each_interface); + each_interface.AppendString(interface_name); + property_writer.Run(&each_interface); + all_interfaces.CloseContainer(&each_interface); + } + each_object_path.CloseContainer(&all_interfaces); + all_object_paths.CloseContainer(&each_object_path); + } + response_writer.CloseContainer(&all_object_paths); + response_sender.Run(response.Pass()); +} + +} // namespace dbus_utils + +} // namespace buffet
diff --git a/buffet/exported_object_manager.h b/buffet/exported_object_manager.h new file mode 100644 index 0000000..d483db0 --- /dev/null +++ b/buffet/exported_object_manager.h
@@ -0,0 +1,117 @@ +// 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_EXPORTED_OBJECT_MANAGER_H_ +#define BUFFET_EXPORTED_OBJECT_MANAGER_H_ + +#include <map> +#include <string> + +#include <base/memory/weak_ptr.h> +#include <dbus/bus.h> +#include <dbus/exported_object.h> +#include <dbus/object_path.h> + +#include "buffet/exported_property_set.h" + +namespace buffet { + +namespace dbus_utils { + +// ExportedObjectManager is a delegate that implements the +// org.freedesktop.DBus.ObjectManager interface on behalf of another +// object. It handles sending signals when new interfaces are added. +// +// This class is very similar to the ExportedPropertySet class, except that +// it allows objects to expose an object manager interface rather than the +// properties interface. +// +// Example usage: +// +// class ExampleObjectManager { +// public: +// ExampleObjectManager(dbus::Bus* bus) +// : object_manager_(bus, "/my/objects/path") { } +// +// void Init(const OnInitFinish& cb) { object_manager_.Init(cb); } +// void ClaimInterface(const dbus::ObjectPath& path, +// const std::string& interface_name, +// const PropertyWriter& writer) { +// object_manager_->ClaimInterface(...); +// } +// void ReleaseInterface(const dbus::ObjectPath& path, +// const std::string& interface_name) { +// object_manager_->ReleaseInterface(...); +// } +// +// private: +// ExportedObjectManager object_manager_; +// }; +// +// class MyObjectClaimingAnInterface { +// public: +// MyObjectClaimingAnInterface(ExampleObjectManager* object_manager) +// : object_manager_(object_manager) {} +// +// void OnInitFinish(bool success) { +// if (!success) { /* handle that */ } +// object_manager_->ClaimInterface( +// my_path_, my_interface_, my_properties_.GetWriter()); +// } +// +// private: +// struct Properties : public ExportedPropertySet { +// public: +// /* Lots of interesting properties. */ +// }; +// +// Properties my_properties_; +// ExampleObjectManager* object_manager_; +// }; +class ExportedObjectManager + : public base::SupportsWeakPtr<ExportedObjectManager> { + public: + // Writes a dictionary of property name to property value variants to writer. + typedef base::Callback<void(dbus::MessageWriter* writer)> PropertyWriter; + typedef base::Callback<void(bool success)> OnInitFinish; + typedef std::map<std::string, PropertyWriter> InterfaceProperties; + + ExportedObjectManager(scoped_refptr<dbus::Bus> bus, + const dbus::ObjectPath& path); + + // Registers methods implementing the ObjectManager interface on the object + // exported on the path given in the constructor. Must be called on the + // origin thread. + void Init(const OnInitFinish& cb); + + // Trigger a signal that |path| has added an interface |interface_name| + // with properties as given by |writer|. + void ClaimInterface(const dbus::ObjectPath& path, + const std::string& interface_name, + const PropertyWriter& writer); + + // Trigger a signal that |path| has removed an interface |interface_name|. + void ReleaseInterface(const dbus::ObjectPath& path, + const std::string& interface_name); + + private: + void HandleGetManagedObjects( + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) const; + + // Both |bus_| and |exported_object_| outlive *this. + dbus::Bus* const bus_; + dbus::ExportedObject* const exported_object_; + // Tracks all objects currently known to the ExportedObjectManager. + std::map<dbus::ObjectPath, InterfaceProperties> registered_objects_; + + friend class ExportedObjectManagerTest; + DISALLOW_COPY_AND_ASSIGN(ExportedObjectManager); +}; + +} // namespace dbus_utils + +} // namespace buffet + +#endif // BUFFET_EXPORTED_OBJECT_MANAGER_H_
diff --git a/buffet/exported_object_manager_unittest.cc b/buffet/exported_object_manager_unittest.cc new file mode 100644 index 0000000..696b76f --- /dev/null +++ b/buffet/exported_object_manager_unittest.cc
@@ -0,0 +1,205 @@ +// 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/exported_object_manager.h" + +#include <base/bind.h> +#include <dbus/mock_bus.h> +#include <dbus/mock_exported_object.h> +#include <dbus/object_manager.h> +#include <dbus/object_path.h> +#include <gtest/gtest.h> + +using ::testing::AnyNumber; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::Return; +using ::testing::_; + +namespace buffet { + +namespace dbus_utils { + +namespace { + +const dbus::ObjectPath kTestPath(std::string("/test/om_path")); +const dbus::ObjectPath kClaimedTestPath(std::string("/test/claimed_path")); +const std::string kClaimedInterface("claimed.interface"); +const std::string kTestPropertyName("PropertyName"); +const std::string kTestPropertyValue("PropertyValue"); + +void WriteTestPropertyDict(dbus::MessageWriter* writer) { + dbus::MessageWriter all_properties(nullptr); + dbus::MessageWriter each_property(nullptr); + writer->OpenArray("{sv}", &all_properties); + all_properties.OpenDictEntry(&each_property); + each_property.AppendString(kTestPropertyName); + each_property.AppendVariantOfString(kTestPropertyValue); + all_properties.CloseContainer(&each_property); + writer->CloseContainer(&all_properties); +} + +void ReadTestPropertyDict(dbus::MessageReader* reader) { + dbus::MessageReader all_properties(nullptr); + dbus::MessageReader each_property(nullptr); + ASSERT_TRUE(reader->PopArray(&all_properties)); + ASSERT_TRUE(all_properties.PopDictEntry(&each_property)); + std::string property_name; + std::string property_value; + ASSERT_TRUE(each_property.PopString(&property_name)); + ASSERT_TRUE(each_property.PopVariantOfString(&property_value)); + EXPECT_FALSE(each_property.HasMoreData()); + EXPECT_FALSE(all_properties.HasMoreData()); + EXPECT_EQ(property_name, kTestPropertyName); + EXPECT_EQ(property_value, kTestPropertyValue); +} + +void VerifyInterfaceClaimSignal(dbus::Signal* signal) { + EXPECT_EQ(signal->GetInterface(), + std::string(dbus::kObjectManagerInterface)); + EXPECT_EQ(signal->GetMember(), + std::string(dbus::kObjectManagerInterfacesAdded)); + // org.freedesktop.DBus.ObjectManager.InterfacesAdded ( + // OBJPATH object_path, + // DICT<STRING,DICT<STRING,VARIANT>> interfaces_and_properties); + dbus::MessageReader reader(signal); + dbus::MessageReader all_interfaces(nullptr); + dbus::MessageReader each_interface(nullptr); + dbus::ObjectPath path; + ASSERT_TRUE(reader.PopObjectPath(&path)); + ASSERT_TRUE(reader.PopArray(&all_interfaces)); + ASSERT_TRUE(all_interfaces.PopDictEntry(&each_interface)); + std::string interface_name; + ASSERT_TRUE(each_interface.PopString(&interface_name)); + ReadTestPropertyDict(&each_interface); + EXPECT_FALSE(each_interface.HasMoreData()); + EXPECT_FALSE(all_interfaces.HasMoreData()); + EXPECT_FALSE(reader.HasMoreData()); + EXPECT_EQ(interface_name, kClaimedInterface); + EXPECT_EQ(path, kClaimedTestPath); +} + +void VerifyInterfaceDropSignal(dbus::Signal* signal) { + EXPECT_EQ(signal->GetInterface(), + std::string(dbus::kObjectManagerInterface)); + EXPECT_EQ(signal->GetMember(), + std::string(dbus::kObjectManagerInterfacesRemoved)); + // org.freedesktop.DBus.ObjectManager.InterfacesRemoved ( + // OBJPATH object_path, ARRAY<STRING> interfaces); + dbus::MessageReader reader(signal); + dbus::MessageReader each_interface(nullptr); + dbus::ObjectPath path; + ASSERT_TRUE(reader.PopObjectPath(&path)); + ASSERT_TRUE(reader.PopArray(&each_interface)); + std::string interface_name; + ASSERT_TRUE(each_interface.PopString(&interface_name)); + EXPECT_FALSE(each_interface.HasMoreData()); + EXPECT_FALSE(reader.HasMoreData()); + EXPECT_EQ(interface_name, kClaimedInterface); + EXPECT_EQ(path, kClaimedTestPath); +} + +} // namespace + +class ExportedObjectManagerTest: public ::testing::Test { + public: + virtual void SetUp() { + dbus::Bus::Options options; + options.bus_type = dbus::Bus::SYSTEM; + bus_ = new dbus::MockBus(options); + // By default, don't worry about threading assertions. + EXPECT_CALL(*bus_, AssertOnOriginThread()).Times(AnyNumber()); + EXPECT_CALL(*bus_, AssertOnDBusThread()).Times(AnyNumber()); + // Use a mock exported object. + mock_exported_object_ = new dbus::MockExportedObject( + bus_.get(), kTestPath); + EXPECT_CALL(*bus_, GetExportedObject(kTestPath)) + .Times(1).WillOnce(Return(mock_exported_object_.get())); + om_.reset(new ExportedObjectManager(bus_.get(), kTestPath)); + property_writer_ = base::Bind(&WriteTestPropertyDict); + response_storer_ = base::Bind(&ExportedObjectManagerTest::StoreResponse, + base::Unretained(this)); + } + + void StoreResponse(scoped_ptr<dbus::Response> method_response) { + last_response_.reset(method_response.release()); + } + + void CallHandleGetManagedObjects( + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender sender) { + om_->HandleGetManagedObjects(method_call, response_storer_); + } + + scoped_refptr<dbus::MockBus> bus_; + scoped_refptr<dbus::MockExportedObject> mock_exported_object_; + scoped_ptr<ExportedObjectManager> om_; + ExportedObjectManager::PropertyWriter property_writer_; + dbus::ExportedObject::ResponseSender response_storer_; + scoped_ptr<dbus::Response> last_response_; +}; + +TEST_F(ExportedObjectManagerTest, ClaimInterfaceSendsSignals) { + EXPECT_CALL(*mock_exported_object_, SendSignal(_)) + .Times(1).WillOnce(Invoke(&VerifyInterfaceClaimSignal)); + om_->ClaimInterface(kClaimedTestPath, kClaimedInterface, property_writer_); +} + +TEST_F(ExportedObjectManagerTest, ReleaseInterfaceSendsSignals) { + InSequence dummy; + EXPECT_CALL(*mock_exported_object_, SendSignal(_)).Times(1); + EXPECT_CALL(*mock_exported_object_, SendSignal(_)) + .Times(1).WillOnce(Invoke(&VerifyInterfaceDropSignal)); + om_->ClaimInterface(kClaimedTestPath, kClaimedInterface, property_writer_); + om_->ReleaseInterface(kClaimedTestPath, kClaimedInterface); +} + +TEST_F(ExportedObjectManagerTest, GetManagedObjectsResponseEmptyCorrectness) { + dbus::MethodCall method_call(dbus::kObjectManagerInterface, + dbus::kObjectManagerGetManagedObjects); + method_call.SetSerial(123); + CallHandleGetManagedObjects(&method_call, response_storer_); + dbus::MessageReader reader(last_response_.get()); + dbus::MessageReader all_paths(nullptr); + ASSERT_TRUE(reader.PopArray(&all_paths)); + EXPECT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedObjectManagerTest, GetManagedObjectsResponseCorrectness) { + // org.freedesktop.DBus.ObjectManager.GetManagedObjects ( + // out DICT<OBJPATH, + // DICT<STRING, + // DICT<STRING,VARIANT>>> ) + dbus::MethodCall method_call(dbus::kObjectManagerInterface, + dbus::kObjectManagerGetManagedObjects); + method_call.SetSerial(123); + EXPECT_CALL(*mock_exported_object_, SendSignal(_)).Times(1); + om_->ClaimInterface(kClaimedTestPath, kClaimedInterface, property_writer_); + CallHandleGetManagedObjects(&method_call, response_storer_); + dbus::MessageReader reader(last_response_.get()); + dbus::MessageReader all_paths(nullptr); + dbus::MessageReader each_path(nullptr); + dbus::MessageReader all_interfaces(nullptr); + dbus::MessageReader each_interface(nullptr); + ASSERT_TRUE(reader.PopArray(&all_paths)); + ASSERT_TRUE(all_paths.PopDictEntry(&each_path)); + dbus::ObjectPath path; + ASSERT_TRUE(each_path.PopObjectPath(&path)); + ASSERT_TRUE(each_path.PopArray(&all_interfaces)); + ASSERT_TRUE(all_interfaces.PopDictEntry(&each_interface)); + std::string interface_name; + ASSERT_TRUE(each_interface.PopString(&interface_name)); + ReadTestPropertyDict(&each_interface); + EXPECT_FALSE(each_interface.HasMoreData()); + EXPECT_FALSE(all_interfaces.HasMoreData()); + EXPECT_FALSE(each_path.HasMoreData()); + EXPECT_FALSE(all_paths.HasMoreData()); + EXPECT_FALSE(reader.HasMoreData()); + EXPECT_EQ(path, kClaimedTestPath); + EXPECT_EQ(interface_name, kClaimedInterface); +} + +} // namespace dbus_utils + +} // namespace buffet
diff --git a/buffet/exported_property_set.cc b/buffet/exported_property_set.cc new file mode 100644 index 0000000..b1b5633 --- /dev/null +++ b/buffet/exported_property_set.cc
@@ -0,0 +1,338 @@ +// 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/exported_property_set.h" + +#include <base/bind.h> +#include <dbus/bus.h> // For kPropertyInterface +#include <dbus/property.h> // For kPropertyInterface + +#include "buffet/async_event_sequencer.h" +#include "buffet/dbus_utils.h" + +namespace buffet { + +namespace dbus_utils { + +namespace { +const char kExportFailedMessage[] = "Failed to register DBus method."; +} // namespace + +ExportedPropertySet::ExportedPropertySet(dbus::Bus* bus, + const dbus::ObjectPath& path) + : bus_(bus), exported_object_(bus->GetExportedObject(path)), + weak_ptr_factory_(this) { } + +void ExportedPropertySet::Init(const OnInitFinish& cb) { + bus_->AssertOnOriginThread(); + scoped_refptr<AsyncEventSequencer> sequencer(new AsyncEventSequencer()); + exported_object_->ExportMethod( + dbus::kPropertiesInterface, dbus::kPropertiesGetAll, + base::Bind(&ExportedPropertySet::HandleGetAll, + weak_ptr_factory_.GetWeakPtr()), + sequencer->GetExportHandler( + dbus::kPropertiesInterface, dbus::kPropertiesGetAll, + kExportFailedMessage, false)); + exported_object_->ExportMethod( + dbus::kPropertiesInterface, dbus::kPropertiesGet, + base::Bind(&ExportedPropertySet::HandleGet, + weak_ptr_factory_.GetWeakPtr()), + sequencer->GetExportHandler( + dbus::kPropertiesInterface, dbus::kPropertiesGet, + kExportFailedMessage, false)); + exported_object_->ExportMethod( + dbus::kPropertiesInterface, dbus::kPropertiesSet, + base::Bind(&ExportedPropertySet::HandleSet, + weak_ptr_factory_.GetWeakPtr()), + sequencer->GetExportHandler( + dbus::kPropertiesInterface, dbus::kPropertiesSet, + kExportFailedMessage, false)); + sequencer->OnAllTasksCompletedCall({cb}); +} + +ExportedPropertySet::PropertyWriter ExportedPropertySet::GetPropertyWriter( + const std::string& interface) { + return base::Bind(&ExportedPropertySet::WritePropertiesDictToMessage, + weak_ptr_factory_.GetWeakPtr(), + interface); +} + +void ExportedPropertySet::RegisterProperty( + const std::string& interface_name, + const std::string& property_name, + ExportedPropertyBase* exported_property) { + bus_->AssertOnOriginThread(); + properties_[interface_name][property_name] = exported_property; + // Technically, the property set exists longer than the properties themselves, + // so we could use Unretained here rather than a weak pointer. + ExportedPropertyBase::OnUpdateCallback cb = base::Bind( + &ExportedPropertySet::HandlePropertyUpdated, + weak_ptr_factory_.GetWeakPtr(), + interface_name, property_name); + exported_property->SetUpdateCallback(cb); +} + +void ExportedPropertySet::HandleGetAll( + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) { + bus_->AssertOnOriginThread(); + dbus::MessageReader reader(method_call); + std::string interface_name; + if (!reader.PopString(&interface_name)) { + response_sender.Run( + GetBadArgsError(method_call, "No interface name specified.")); + return; + } + if (reader.HasMoreData()) { + response_sender.Run( + GetBadArgsError(method_call, "Too many arguments to GetAll.")); + return; + } + scoped_ptr<dbus::Response> response( + dbus::Response::FromMethodCall(method_call)); + dbus::MessageWriter resp_writer(response.get()); + WritePropertiesDictToMessage(interface_name, &resp_writer); + response_sender.Run(response.Pass()); +} + +void ExportedPropertySet::WritePropertiesDictToMessage( + const std::string& interface_name, + dbus::MessageWriter* writer) { + dbus::MessageWriter dict_writer(nullptr); + writer->OpenArray("{sv}", &dict_writer); + auto property_map_itr = properties_.find(interface_name); + if (property_map_itr != properties_.end()) { + for (const auto& kv : property_map_itr->second) { + dbus::MessageWriter entry_writer(nullptr); + dict_writer.OpenDictEntry(&entry_writer); + entry_writer.AppendString(kv.first); + kv.second->AppendValueToWriter(&entry_writer); + dict_writer.CloseContainer(&entry_writer); + } + } else { + LOG(WARNING) << "No properties found for interface interface_name"; + } + writer->CloseContainer(&dict_writer); +} + +void ExportedPropertySet::HandleGet( + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) { + bus_->AssertOnOriginThread(); + dbus::MessageReader reader(method_call); + std::string interface_name, property_name; + if (!reader.PopString(&interface_name)) { + response_sender.Run( + GetBadArgsError(method_call, "No interface name specified.")); + return; + } + if (!reader.PopString(&property_name)) { + response_sender.Run( + GetBadArgsError(method_call, "No property name specified.")); + return; + } + if (reader.HasMoreData()) { + response_sender.Run( + GetBadArgsError(method_call, "Too many arguments to Get.")); + return; + } + auto property_map_itr = properties_.find(interface_name); + if (property_map_itr == properties_.end()) { + response_sender.Run( + GetBadArgsError(method_call, "No such interface on object.")); + return; + } + LOG(ERROR) << "Looking for " << property_name << " on " << interface_name; + auto property_itr = property_map_itr->second.find(property_name); + if (property_itr == property_map_itr->second.end()) { + response_sender.Run( + GetBadArgsError(method_call, "No such property on interface.")); + return; + } + scoped_ptr<dbus::Response> response( + dbus::Response::FromMethodCall(method_call)); + dbus::MessageWriter resp_writer(response.get()); + property_itr->second->AppendValueToWriter(&resp_writer); + response_sender.Run(response.Pass()); +} + +void ExportedPropertySet::HandleSet( + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) { + bus_->AssertOnOriginThread(); + scoped_ptr<dbus::ErrorResponse> error_resp( + dbus::ErrorResponse::FromMethodCall( + method_call, "org.freedesktop.DBus.Error.NotSupported", "")); + scoped_ptr<dbus::Response> response(error_resp.release()); + response_sender.Run(response.Pass()); +} + +void ExportedPropertySet::HandlePropertyUpdated( + const std::string& interface, + const std::string& name, + const ExportedPropertyBase* property) { + bus_->AssertOnOriginThread(); + dbus::Signal signal(dbus::kPropertiesInterface, dbus::kPropertiesChanged); + dbus::MessageWriter writer(&signal); + dbus::MessageWriter array_writer(nullptr); + dbus::MessageWriter dict_writer(nullptr); + writer.AppendString(interface); + writer.OpenArray("{sv}", &array_writer); + array_writer.OpenDictEntry(&dict_writer); + dict_writer.AppendString(name); + property->AppendValueToWriter(&dict_writer); + array_writer.CloseContainer(&dict_writer); + writer.CloseContainer(&array_writer); + // The interface specification tells us to include this list of properties + // which have changed, but for whom no value is conveyed. Currently, we + // don't do anything interesting here. + writer.OpenArray("s", &array_writer); + writer.CloseContainer(&array_writer); + // This sends the signal asyncronously. However, the raw message inside + // the signal object is ref-counted, so we're fine to allocate the Signal + // object on our local stack. + exported_object_->SendSignal(&signal); +} + +template <typename T> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const T& value); + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const bool& value) { + writer->AppendVariantOfBool(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const uint8& value) { + writer->AppendVariantOfByte(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const int16& value) { + writer->AppendVariantOfInt16(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const uint16& value) { + writer->AppendVariantOfUint16(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const int32& value) { + writer->AppendVariantOfInt32(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const uint32& value) { + writer->AppendVariantOfUint32(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const int64& value) { + writer->AppendVariantOfInt64(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const uint64& value) { + writer->AppendVariantOfUint64(value); +} + +template <> +void AppendPropertyToWriter(dbus::MessageWriter* writer, const double& value) { + writer->AppendVariantOfDouble(value); +} + +template <> +void AppendPropertyToWriter( + dbus::MessageWriter* writer, const std::string& value) { + writer->AppendVariantOfString(value); +} + +template <> +void AppendPropertyToWriter( + dbus::MessageWriter* writer, const dbus::ObjectPath& value) { + writer->AppendVariantOfObjectPath(value); +} + +template <> +void AppendPropertyToWriter( + dbus::MessageWriter* writer, const std::vector<std::string>& value) { + dbus::MessageWriter variant_writer(nullptr); + writer->OpenVariant("as", &variant_writer); + variant_writer.AppendArrayOfStrings(value); + writer->CloseContainer(&variant_writer); +} + +template <> +void AppendPropertyToWriter( + dbus::MessageWriter* writer, const std::vector<dbus::ObjectPath>& value) { + dbus::MessageWriter variant_writer(nullptr); + writer->OpenVariant("ao", &variant_writer); + variant_writer.AppendArrayOfObjectPaths(value); + writer->CloseContainer(&variant_writer); +} + +template <> +void AppendPropertyToWriter( + dbus::MessageWriter* writer, const std::vector<uint8>& value) { + dbus::MessageWriter variant_writer(nullptr); + writer->OpenVariant("ay", &variant_writer); + variant_writer.AppendArrayOfBytes(value.data(), value.size()); + writer->CloseContainer(&variant_writer); +} + +template <typename T> +ExportedProperty<T>::ExportedProperty() {} + +template <typename T> +ExportedProperty<T>::~ExportedProperty() {} + +template <typename T> +const T& ExportedProperty<T>::value() const { return value_; } + +template <typename T> +void ExportedProperty<T>::SetValue(const T& new_value) { + if (value_ == new_value) { + return; + } + value_ = new_value; + // These is a brief period after the construction of an ExportedProperty + // when this callback is not initialized because the property has not + // been registered with the parent ExportedPropertySet. During this period + // users should be initializing values via SetValue, and no notifications + // should be triggered by the ExportedPropertySet. + if (!on_update_.is_null()) { + on_update_.Run(this); + } +} + +template <typename T> +void ExportedProperty<T>::SetUpdateCallback(const OnUpdateCallback& cb) { + on_update_ = cb; +} + +template <typename T> +void ExportedProperty<T>::AppendValueToWriter( + dbus::MessageWriter* writer) const { + AppendPropertyToWriter(writer, value_); +} + +template class ExportedProperty<bool>; +template class ExportedProperty<uint8>; +template class ExportedProperty<int16>; +template class ExportedProperty<uint16>; +template class ExportedProperty<int32>; +template class ExportedProperty<uint32>; +template class ExportedProperty<int64>; +template class ExportedProperty<uint64>; +template class ExportedProperty<double>; +template class ExportedProperty<std::string>; +template class ExportedProperty<dbus::ObjectPath>; +template class ExportedProperty<std::vector<std::string>>; +template class ExportedProperty<std::vector<dbus::ObjectPath>>; +template class ExportedProperty<std::vector<uint8>>; + +} // namespace dbus_utils + +} // namespace buffet
diff --git a/buffet/exported_property_set.h b/buffet/exported_property_set.h new file mode 100644 index 0000000..405ed7d --- /dev/null +++ b/buffet/exported_property_set.h
@@ -0,0 +1,200 @@ +// 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_EXPORTED_PROPERTY_SET_H_ +#define BUFFET_EXPORTED_PROPERTY_SET_H_ + +#include <map> +#include <string> +#include <vector> + +#include <base/memory/weak_ptr.h> +#include <dbus/exported_object.h> +#include <dbus/message.h> + +namespace buffet { + +namespace dbus_utils { + +// This class may be used to implement the org.freedesktop.DBus.Properties +// interface. It sends the update signal on property updates: +// +// org.freedesktop.DBus.Properties.PropertiesChanged ( +// STRING interface_name, +// DICT<STRING,VARIANT> changed_properties, +// ARRAY<STRING> invalidated_properties); +// +// +// and implements the required methods of the interface: +// +// org.freedesktop.DBus.Properties.Get(in STRING interface_name, +// in STRING property_name, +// out VARIANT value); +// org.freedesktop.DBus.Properties.Set(in STRING interface_name, +// in STRING property_name, +// in VARIANT value); +// org.freedesktop.DBus.Properties.GetAll(in STRING interface_name, +// out DICT<STRING,VARIANT> props); +// +// This class is very similar to the PropertySet class in Chrome, except that +// it allows objects to expose properties rather than to consume them. +// +// Example usage: +// +// class ExampleObjectExportingProperties { +// public: +// ExampleObjectExportingProperties(ExportedObject* exported_object) +// : p_(exported_object) { +// // Initialize properties appropriately. Do this before +// // claiming the Properties interface so that daemons watching +// // this object don't see partial or inaccurate state. +// p_.ClaimPropertiesInterface(); +// } +// +// private: +// struct Properties : public buffet::dbus::ExportedPropertySet { +// public: +// buffet::dbus::ExportedProperty<std::string> name_; +// buffet::dbus::ExportedProperty<uint16> version_; +// buffet::dbus::ExportedProperty<dbus::ObjectPath> parent_; +// buffet::dbus::ExportedProperty<std::vector<std::string>> children_; +// +// Properties(dbus::ExportedObject* exported_object) +// : buffet::dbus::ExportedPropertySet(exported_object) { +// RegisterProperty(kExampleInterfaceName, "Name", &name_); +// RegisterProperty(kExampleInterfaceName, "Version", &version_); +// RegisterProperty(kExampleInterfaceName, "Parent", &parent_); +// RegisterProperty(kExampleInterfaceName, "Children", &children_); +// } +// virtual ~Properties() {} +// }; +// +// Properties p_; +// }; + +class ExportedPropertyBase { + public: + ExportedPropertyBase() {} + virtual ~ExportedPropertyBase() {} + + typedef base::Callback<void(const ExportedPropertyBase*)> OnUpdateCallback; + + // Called by ExportedPropertySet to register a callback. This callback + // triggers ExportedPropertySet to send a signal from the properties + // interface of the exported object. + virtual void SetUpdateCallback(const OnUpdateCallback& cb) = 0; + + // Appends a variant of the contained value to the writer. This is + // needed to write out properties to Get and GetAll methods implemented + // by the ExportedPropertySet since it doesn't actually know the type + // of each property. + virtual void AppendValueToWriter(dbus::MessageWriter* writer) const = 0; +}; + +class ExportedPropertySet { + public: + typedef base::Callback<void(bool success)> OnInitFinish; + typedef base::Callback<void(dbus::MessageWriter* writer)> PropertyWriter; + + ExportedPropertySet(dbus::Bus* bus, const dbus::ObjectPath& path); + virtual ~ExportedPropertySet() = default; + + // Claims the method associated with the org.freedesktop.DBus.Properties + // interface. This needs to be done after all properties are initialized to + // appropriate values. This method will call |cb| when all methods + // are exported to the DBus object. |cb| will be called on the origin + // thread. + void Init(const OnInitFinish& cb); + + // Return a callback that knows how to write this property set's properties + // to a message. This writer retains a weak pointer to this, and must + // only be invoked on the same thread as the rest of ExportedPropertySet. + PropertyWriter GetPropertyWriter(const std::string& interface); + + protected: + void RegisterProperty(const std::string& interface_name, + const std::string& property_name, + ExportedPropertyBase* exported_property); + + private: + // Used to write the dictionary of string->variant to a message. + // This dictionary represents the property name/value pairs for the + // given interface. + void WritePropertiesDictToMessage(const std::string& interface_name, + dbus::MessageWriter* writer); + void HandleGetAll(dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender); + void HandleGet(dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender); + // While Properties.Set has a handler to complete the interface, we don't + // support writable properties. This is almost a feature, since bindings for + // many languages don't support errors coming back from invalid writes. + // Instead, use setters in exposed interfaces. + void HandleSet(dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender); + void HandlePropertyUpdated(const std::string& interface, + const std::string& name, + const ExportedPropertyBase* property); + + dbus::Bus* bus_; + dbus::ExportedObject* exported_object_; // weak; owned by the Bus object. + // This is a map from interface name -> property name -> pointer to property. + std::map<std::string, + std::map<std::string, ExportedPropertyBase*>> properties_; + + // D-Bus callbacks may last longer the property set exporting those methods. + base::WeakPtrFactory<ExportedPropertySet> weak_ptr_factory_; + + friend class ExportedPropertySetTest; + DISALLOW_COPY_AND_ASSIGN(ExportedPropertySet); +}; + +template <typename T> +class ExportedProperty : public ExportedPropertyBase { + public: + ExportedProperty(); + virtual ~ExportedProperty() override; + + // Retrieves the current value. + const T& value() const; + + // Set the value exposed to remote applications. This triggers notifications + // of changes over the Properties interface. + void SetValue(const T& new_value); + + // Called by ExportedPropertySet. This update callback triggers + // ExportedPropertySet to send a signal from the properties interface of the + // exported object. + virtual void SetUpdateCallback(const OnUpdateCallback& cb) override; + + // Implementation provided by specialization. + virtual void AppendValueToWriter(dbus::MessageWriter* writer) const override; + + private: + OnUpdateCallback on_update_; + T value_{}; // NOLINT - initializer list + + DISALLOW_COPY_AND_ASSIGN(ExportedProperty); +}; + +extern template class ExportedProperty<bool>; +extern template class ExportedProperty<uint8>; +extern template class ExportedProperty<int16>; +extern template class ExportedProperty<uint16>; +extern template class ExportedProperty<int32>; +extern template class ExportedProperty<uint32>; +extern template class ExportedProperty<int64>; +extern template class ExportedProperty<uint64>; +extern template class ExportedProperty<double>; +extern template class ExportedProperty<std::string>; +extern template class ExportedProperty<dbus::ObjectPath>; +extern template class ExportedProperty<std::vector<std::string>>; +extern template class ExportedProperty<std::vector<dbus::ObjectPath>>; +extern template class ExportedProperty<std::vector<uint8>>; + +} // namespace dbus_utils + +} // namespace buffet + +#endif // BUFFET_EXPORTED_PROPERTY_SET_H_
diff --git a/buffet/exported_property_set_unittest.cc b/buffet/exported_property_set_unittest.cc new file mode 100644 index 0000000..ce8a3fe --- /dev/null +++ b/buffet/exported_property_set_unittest.cc
@@ -0,0 +1,506 @@ +// 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/exported_property_set.h" + +#include <string> +#include <vector> + +#include <base/basictypes.h> +#include <base/bind.h> +#include <dbus/message.h> +#include <dbus/property.h> +#include <dbus/object_path.h> +#include <dbus/mock_bus.h> +#include <dbus/mock_exported_object.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +using ::testing::AnyNumber; +using ::testing::Return; +using ::testing::Invoke; +using ::testing::_; + +namespace buffet { + +namespace dbus_utils { + +namespace { + +const char kBoolPropName[] = "BoolProp"; +const char kUint8PropName[] = "Uint8Prop"; +const char kInt16PropName[] = "Int16Prop"; +const char kUint16PropName[] = "Uint16Prop"; +const char kInt32PropName[] = "Int32Prop"; +const char kUint32PropName[] = "Uint32Prop"; +const char kInt64PropName[] = "Int64Prop"; +const char kUint64PropName[] = "Uint64Prop"; +const char kDoublePropName[] = "DoubleProp"; +const char kStringPropName[] = "StringProp"; +const char kPathPropName[] = "PathProp"; +const char kStringListPropName[] = "StringListProp"; +const char kPathListPropName[] = "PathListProp"; +const char kUint8ListPropName[] = "Uint8ListProp"; + +const char kTestInterface1[] = "org.chromium.TestInterface1"; +const char kTestInterface2[] = "org.chromium.TestInterface2"; +const char kTestInterface3[] = "org.chromium.TestInterface3"; + +const std::string kTestString("lies"); +const dbus::ObjectPath kMethodsExportedOnPath(std::string("/export")); +const dbus::ObjectPath kTestObjectPathInit(std::string("/path_init")); +const dbus::ObjectPath kTestObjectPathUpdate(std::string("/path_update")); + +} // namespace + +class ExportedPropertySetTest : public ::testing::Test { + public: + struct Properties : public ExportedPropertySet { + public: + ExportedProperty<bool> bool_prop_; + ExportedProperty<uint8> uint8_prop_; + ExportedProperty<int16> int16_prop_; + ExportedProperty<uint16> uint16_prop_; + ExportedProperty<int32> int32_prop_; + ExportedProperty<uint32> uint32_prop_; + ExportedProperty<int64> int64_prop_; + ExportedProperty<uint64> uint64_prop_; + ExportedProperty<double> double_prop_; + ExportedProperty<std::string> string_prop_; + ExportedProperty<dbus::ObjectPath> path_prop_; + ExportedProperty<std::vector<std::string>> stringlist_prop_; + ExportedProperty<std::vector<dbus::ObjectPath>> pathlist_prop_; + ExportedProperty<std::vector<uint8>> uint8list_prop_; + + Properties(dbus::Bus* bus, const dbus::ObjectPath& path) + : ExportedPropertySet(bus, path) { + // The empty string is not a valid value for an ObjectPath. + path_prop_.SetValue(kTestObjectPathInit); + RegisterProperty(kTestInterface1, kBoolPropName, &bool_prop_); + RegisterProperty(kTestInterface1, kUint8PropName, &uint8_prop_); + RegisterProperty(kTestInterface1, kInt16PropName, &int16_prop_); + // I chose this weird grouping because N=2 is about all the permutations + // of GetAll that I want to anticipate. + RegisterProperty(kTestInterface2, kUint16PropName, &uint16_prop_); + RegisterProperty(kTestInterface2, kInt32PropName, &int32_prop_); + RegisterProperty(kTestInterface3, kUint32PropName, &uint32_prop_); + RegisterProperty(kTestInterface3, kInt64PropName, &int64_prop_); + RegisterProperty(kTestInterface3, kUint64PropName, &uint64_prop_); + RegisterProperty(kTestInterface3, kDoublePropName, &double_prop_); + RegisterProperty(kTestInterface3, kStringPropName, &string_prop_); + RegisterProperty(kTestInterface3, kPathPropName, &path_prop_); + RegisterProperty(kTestInterface3, kStringListPropName, &stringlist_prop_); + RegisterProperty(kTestInterface3, kPathListPropName, &pathlist_prop_); + RegisterProperty(kTestInterface3, kUint8ListPropName, &uint8list_prop_); + } + virtual ~Properties() {} + + void CallHandleGetAll( + dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) { + HandleGetAll(method_call, response_sender); + } + + void CallHandleGet(dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) { + HandleGet(method_call, response_sender); + } + + void CallHandleSet(dbus::MethodCall* method_call, + dbus::ExportedObject::ResponseSender response_sender) { + HandleSet(method_call, response_sender); + } + }; + + virtual void SetUp() { + dbus::Bus::Options options; + options.bus_type = dbus::Bus::SYSTEM; + bus_ = new dbus::MockBus(options); + // By default, don't worry about threading assertions. + EXPECT_CALL(*bus_, AssertOnOriginThread()).Times(AnyNumber()); + EXPECT_CALL(*bus_, AssertOnDBusThread()).Times(AnyNumber()); + // Use a mock exported object. + mock_exported_object_ = new dbus::MockExportedObject( + bus_.get(), kMethodsExportedOnPath); + EXPECT_CALL(*bus_, GetExportedObject(kMethodsExportedOnPath)) + .Times(1).WillOnce(Return(mock_exported_object_.get())); + p_.reset(new Properties(bus_.get(), kMethodsExportedOnPath)); + } + + void StoreResponse(scoped_ptr<dbus::Response> method_response) { + last_response_.reset(method_response.release()); + } + + void AssertGetAllReturnsError(dbus::MethodCall* method_call) { + auto response_sender = base::Bind(&ExportedPropertySetTest::StoreResponse, + base::Unretained(this)); + method_call->SetSerial(123); + p_->CallHandleGetAll(method_call, response_sender); + ASSERT_NE(dynamic_cast<dbus::ErrorResponse*>(last_response_.get()), + nullptr); + } + + void AssertGetReturnsError(dbus::MethodCall* method_call) { + auto response_sender = base::Bind(&ExportedPropertySetTest::StoreResponse, + base::Unretained(this)); + method_call->SetSerial(123); + p_->CallHandleGet(method_call, response_sender); + ASSERT_NE(dynamic_cast<dbus::ErrorResponse*>(last_response_.get()), + nullptr); + } + + scoped_ptr<dbus::Response> GetPropertyOnInterface( + const std::string& interface_name, const std::string& property_name) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGet); + method_call.SetSerial(123); + dbus::MessageWriter writer(&method_call); + writer.AppendString(interface_name); + writer.AppendString(property_name); + auto response_sender = base::Bind(&ExportedPropertySetTest::StoreResponse, + base::Unretained(this)); + p_->CallHandleGet(&method_call, response_sender); + return last_response_.Pass(); + } + + scoped_ptr<dbus::Response> last_response_; + scoped_refptr<dbus::MockBus> bus_; + scoped_refptr<dbus::MockExportedObject> mock_exported_object_; + scoped_ptr<Properties> p_; +}; + +TEST_F(ExportedPropertySetTest, UpdateNotifications) { + EXPECT_CALL(*mock_exported_object_, SendSignal(_)).Times(14); + p_->bool_prop_.SetValue(true); + p_->uint8_prop_.SetValue(1); + p_->int16_prop_.SetValue(1); + p_->uint16_prop_.SetValue(1); + p_->int32_prop_.SetValue(1); + p_->uint32_prop_.SetValue(1); + p_->int64_prop_.SetValue(1); + p_->uint64_prop_.SetValue(1); + p_->double_prop_.SetValue(1.0); + p_->string_prop_.SetValue(kTestString); + p_->path_prop_.SetValue(kTestObjectPathUpdate); + p_->stringlist_prop_.SetValue({kTestString}); + p_->pathlist_prop_.SetValue({kTestObjectPathUpdate}); + p_->uint8list_prop_.SetValue({1}); +} + +TEST_F(ExportedPropertySetTest, UpdateToSameValue) { + EXPECT_CALL(*mock_exported_object_, SendSignal(_)).Times(1); + p_->bool_prop_.SetValue(true); + p_->bool_prop_.SetValue(true); +} + +TEST_F(ExportedPropertySetTest, GetAllNoArgs) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGetAll); + AssertGetAllReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetAllInvalidInterface) { + dbus::MethodCall method_call( + dbus::kPropertiesInterface, dbus::kPropertiesGetAll); + method_call.SetSerial(123); + dbus::MessageWriter writer(&method_call); + writer.AppendString("org.chromium.BadInterface"); + auto response_sender = base::Bind(&ExportedPropertySetTest::StoreResponse, + base::Unretained(this)); + p_->CallHandleGetAll(&method_call, response_sender); + dbus::MessageReader response_reader(last_response_.get()); + dbus::MessageReader dict_reader(nullptr); + ASSERT_TRUE(response_reader.PopArray(&dict_reader)); + // The response should just be a an empty array, since there are no properties + // on this interface. The spec doesn't say much about error conditions here, + // so I'm going to assume this is a valid implementation. + ASSERT_FALSE(dict_reader.HasMoreData()); + ASSERT_FALSE(response_reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetAllExtraArgs) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGetAll); + dbus::MessageWriter writer(&method_call); + writer.AppendString(kTestInterface1); + writer.AppendString(kTestInterface1); + AssertGetAllReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetAllCorrectness) { + dbus::MethodCall method_call( + dbus::kPropertiesInterface, dbus::kPropertiesGetAll); + method_call.SetSerial(123); + dbus::MessageWriter writer(&method_call); + writer.AppendString(kTestInterface2); + auto response_sender = base::Bind(&ExportedPropertySetTest::StoreResponse, + base::Unretained(this)); + p_->CallHandleGetAll(&method_call, response_sender); + dbus::MessageReader response_reader(last_response_.get()); + dbus::MessageReader dict_reader(nullptr); + dbus::MessageReader entry_reader(nullptr); + ASSERT_TRUE(response_reader.PopArray(&dict_reader)); + ASSERT_TRUE(dict_reader.PopDictEntry(&entry_reader)); + std::string property_name; + ASSERT_TRUE(entry_reader.PopString(&property_name)); + uint16 value16; + int32 value32; + if (property_name.compare(kUint16PropName) == 0) { + ASSERT_TRUE(entry_reader.PopVariantOfUint16(&value16)); + ASSERT_FALSE(entry_reader.HasMoreData()); + ASSERT_TRUE(dict_reader.PopDictEntry(&entry_reader)); + ASSERT_TRUE(entry_reader.PopString(&property_name)); + ASSERT_EQ(property_name.compare(kInt32PropName), 0); + ASSERT_TRUE(entry_reader.PopVariantOfInt32(&value32)); + } else { + ASSERT_EQ(property_name.compare(kInt32PropName), 0); + ASSERT_TRUE(entry_reader.PopVariantOfInt32(&value32)); + ASSERT_FALSE(entry_reader.HasMoreData()); + ASSERT_TRUE(dict_reader.PopDictEntry(&entry_reader)); + ASSERT_TRUE(entry_reader.PopString(&property_name)); + ASSERT_EQ(property_name.compare(kUint16PropName), 0); + ASSERT_TRUE(entry_reader.PopVariantOfUint16(&value16)); + } + ASSERT_FALSE(entry_reader.HasMoreData()); + ASSERT_FALSE(dict_reader.HasMoreData()); + ASSERT_FALSE(response_reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetNoArgs) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGet); + AssertGetReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetInvalidInterface) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGet); + dbus::MessageWriter writer(&method_call); + writer.AppendString("org.chromium.BadInterface"); + writer.AppendString(kInt16PropName); + AssertGetReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetBadPropertyName) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGet); + dbus::MessageWriter writer(&method_call); + writer.AppendString(kTestInterface1); + writer.AppendString("IAmNotAProperty"); + AssertGetReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetPropIfMismatch) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGet); + dbus::MessageWriter writer(&method_call); + writer.AppendString(kTestInterface1); + writer.AppendString(kStringPropName); + AssertGetReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetNoPropertyName) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGet); + dbus::MessageWriter writer(&method_call); + writer.AppendString(kTestInterface1); + AssertGetReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetExtraArgs) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesGet); + dbus::MessageWriter writer(&method_call); + writer.AppendString(kTestInterface1); + writer.AppendString(kBoolPropName); + writer.AppendString("Extra param"); + AssertGetReturnsError(&method_call); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithBool) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface1, kBoolPropName); + dbus::MessageReader reader(response.get()); + bool value; + ASSERT_TRUE(reader.PopVariantOfBool(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithUint8) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface1, kUint8PropName); + dbus::MessageReader reader(response.get()); + uint8 value; + ASSERT_TRUE(reader.PopVariantOfByte(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithInt16) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface1, kInt16PropName); + dbus::MessageReader reader(response.get()); + int16 value; + ASSERT_TRUE(reader.PopVariantOfInt16(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithUint16) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface2, kUint16PropName); + dbus::MessageReader reader(response.get()); + uint16 value; + ASSERT_TRUE(reader.PopVariantOfUint16(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithInt32) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface2, kInt32PropName); + dbus::MessageReader reader(response.get()); + int32 value; + ASSERT_TRUE(reader.PopVariantOfInt32(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithUint32) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kUint32PropName); + dbus::MessageReader reader(response.get()); + uint32 value; + ASSERT_TRUE(reader.PopVariantOfUint32(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithInt64) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kInt64PropName); + dbus::MessageReader reader(response.get()); + int64 value; + ASSERT_TRUE(reader.PopVariantOfInt64(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithUint64) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kUint64PropName); + dbus::MessageReader reader(response.get()); + uint64 value; + ASSERT_TRUE(reader.PopVariantOfUint64(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithDouble) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kDoublePropName); + dbus::MessageReader reader(response.get()); + double value; + ASSERT_TRUE(reader.PopVariantOfDouble(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithString) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kStringPropName); + dbus::MessageReader reader(response.get()); + std::string value; + ASSERT_TRUE(reader.PopVariantOfString(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithPath) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kPathPropName); + dbus::MessageReader reader(response.get()); + dbus::ObjectPath value; + ASSERT_TRUE(reader.PopVariantOfObjectPath(&value)); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithStringList) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kStringListPropName); + dbus::MessageReader reader(response.get()); + dbus::MessageReader variant_reader(nullptr); + std::vector<std::string> value; + ASSERT_TRUE(reader.PopVariant(&variant_reader)); + ASSERT_TRUE(variant_reader.PopArrayOfStrings(&value)); + ASSERT_FALSE(variant_reader.HasMoreData()); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithPathList) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kPathListPropName); + dbus::MessageReader reader(response.get()); + dbus::MessageReader variant_reader(nullptr); + std::vector<dbus::ObjectPath> value; + ASSERT_TRUE(reader.PopVariant(&variant_reader)); + ASSERT_TRUE(variant_reader.PopArrayOfObjectPaths(&value)); + ASSERT_FALSE(variant_reader.HasMoreData()); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, GetWorksWithUint8List) { + scoped_ptr<dbus::Response> response = GetPropertyOnInterface( + kTestInterface3, kPathListPropName); + dbus::MessageReader reader(response.get()); + dbus::MessageReader variant_reader(nullptr); + const uint8* buffer; + size_t buffer_len; + ASSERT_TRUE(reader.PopVariant(&variant_reader)); + // |buffer| remains under the control of the MessageReader. + ASSERT_TRUE(variant_reader.PopArrayOfBytes(&buffer, &buffer_len)); + ASSERT_FALSE(variant_reader.HasMoreData()); + ASSERT_FALSE(reader.HasMoreData()); +} + +TEST_F(ExportedPropertySetTest, SetFailsGracefully) { + dbus::MethodCall method_call(dbus::kPropertiesInterface, + dbus::kPropertiesSet); + method_call.SetSerial(123); + auto response_sender = base::Bind(&ExportedPropertySetTest::StoreResponse, + base::Unretained(this)); + p_->CallHandleSet(&method_call, response_sender); + ASSERT_TRUE( + dynamic_cast<dbus::ErrorResponse*>(last_response_.get()) != nullptr); +} + +namespace { + +void VerifySignal(dbus::Signal* signal) { + ASSERT_NE(signal, nullptr); + std::string interface_name; + std::string property_name; + uint8 value; + dbus::MessageReader reader(signal); + dbus::MessageReader array_reader(signal); + dbus::MessageReader dict_reader(signal); + ASSERT_TRUE(reader.PopString(&interface_name)); + ASSERT_TRUE(reader.PopArray(&array_reader)); + ASSERT_TRUE(array_reader.PopDictEntry(&dict_reader)); + ASSERT_TRUE(dict_reader.PopString(&property_name)); + ASSERT_TRUE(dict_reader.PopVariantOfByte(&value)); + ASSERT_FALSE(dict_reader.HasMoreData()); + ASSERT_FALSE(array_reader.HasMoreData()); + ASSERT_TRUE(reader.HasMoreData()); + // Read the (empty) list of invalidated property names. + ASSERT_TRUE(reader.PopArray(&array_reader)); + ASSERT_FALSE(array_reader.HasMoreData()); + ASSERT_FALSE(reader.HasMoreData()); + ASSERT_EQ(value, 57); + ASSERT_EQ(property_name, std::string(kUint8PropName)); + ASSERT_EQ(interface_name, std::string(kTestInterface1)); +} + +} // namespace + +TEST_F(ExportedPropertySetTest, SignalsAreParsable) { + EXPECT_CALL(*mock_exported_object_, SendSignal(_)) + .Times(1).WillOnce(Invoke(&VerifySignal)); + p_->uint8_prop_.SetValue(57); +} + +} // namespace dbus_utils + +} // namespace buffet
diff --git a/buffet/http_connection.h b/buffet/http_connection.h new file mode 100644 index 0000000..ab67905 --- /dev/null +++ b/buffet/http_connection.h
@@ -0,0 +1,84 @@ +// 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_HTTP_CONNECTION_H_ +#define BUFFET_HTTP_CONNECTION_H_ + +#include <string> +#include <vector> + +#include <base/basictypes.h> + +#include "buffet/error.h" +#include "buffet/http_transport.h" + +namespace buffet { +namespace http { + +/////////////////////////////////////////////////////////////////////////////// +// Connection class is the base class for HTTP communication session. +// It abstracts the implementation of underlying transport library (ex libcurl). +// When the Connection-derived class is constructed, it is pre-set up with +// basic initialization information necessary to initiate the server request +// connection (such as the URL, request method, etc - see +// Transport::CreateConnection() for more details). But most implementations +// would not probably initiate the physical connection until SendHeaders +// is called. +// You normally shouldn't worry about using this class directly. +// http::Request and http::Response classes use it for communication. +/////////////////////////////////////////////////////////////////////////////// +class Connection { + public: + explicit Connection(std::shared_ptr<Transport> transport) + : transport_(transport) {} + virtual ~Connection() = default; + + // Called by http::Request to initiate the connection with the server. + // This normally opens the socket and sends the request headers. + virtual bool SendHeaders(const HeaderList& headers, ErrorPtr* error) = 0; + // If needed, this function can be called to send the request body data. + // This function can be called repeatedly until all data is sent. + virtual bool WriteRequestData(const void* data, size_t size, + ErrorPtr* error) = 0; + // This function is called when all the data is sent off and it's time + // to receive the response data. + virtual bool FinishRequest(ErrorPtr* error) = 0; + + // Returns the HTTP status code (e.g. 200 for success). + virtual int GetResponseStatusCode() const = 0; + // Returns the status text (e.g. for error 403 it could be "NOT AUTHORIZED"). + virtual std::string GetResponseStatusText() const = 0; + // Returns the HTTP protocol version (e.g. "HTTP/1.1"). + virtual std::string GetProtocolVersion() const = 0; + // Returns the value of particular response header, or empty string if the + // headers wasn't received. + virtual std::string GetResponseHeader( + const std::string& header_name) const = 0; + // Returns the response data size, if known. For chunked (streaming) + // transmission this might not be known until all the data is sent. + // In this case GetResponseDataSize() will return 0. + virtual uint64_t GetResponseDataSize() const = 0; + // This function is called to read a block of response data. + // It needs to be called repeatedly until it returns false or |size_read| is + // set to 0. |data| is the destination buffer to read the data into. + // |buffer_size| is the size of the buffer (amount of data to read). + // |read_size| is the amount of data actually read, which could be less than + // the size requested or 0 if there is no more data available. + virtual bool ReadResponseData(void* data, size_t buffer_size, + size_t* size_read, ErrorPtr* error) = 0; + + protected: + // |transport_| is mainly used to keep the object alive as long as the + // connection exists. But some implementations of Connection could use + // the Transport-derived class for their own needs as well. + std::shared_ptr<Transport> transport_; + + private: + DISALLOW_COPY_AND_ASSIGN(Connection); +}; + +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_CONNECTION_H_
diff --git a/buffet/http_connection_curl.cc b/buffet/http_connection_curl.cc new file mode 100644 index 0000000..0dae74c --- /dev/null +++ b/buffet/http_connection_curl.cc
@@ -0,0 +1,226 @@ +// 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/http_connection_curl.h" + +#include <base/logging.h> + +#include "buffet/http_request.h" +#include "buffet/http_transport_curl.h" +#include "buffet/string_utils.h" + +namespace buffet { +namespace http { +namespace curl { + +static int curl_trace(CURL *handle, curl_infotype type, + char *data, size_t size, void *userp) { + std::string msg(data, size); + + switch (type) { + case CURLINFO_TEXT: + VLOG(3) << "== Info: " << msg; + break; + case CURLINFO_HEADER_OUT: + VLOG(3) << "=> Send headers:\n" << msg; + break; + case CURLINFO_DATA_OUT: + VLOG(3) << "=> Send data:\n" << msg; + break; + case CURLINFO_SSL_DATA_OUT: + VLOG(3) << "=> Send SSL data" << msg; + break; + case CURLINFO_HEADER_IN: + VLOG(3) << "<= Recv header: " << msg; + break; + case CURLINFO_DATA_IN: + VLOG(3) << "<= Recv data:\n" << msg; + break; + case CURLINFO_SSL_DATA_IN: + VLOG(3) << "<= Recv SSL data" << msg; + break; + default: + break; + } + return 0; +} + +Connection::Connection(CURL* curl_handle, const std::string& method, + std::shared_ptr<http::Transport> transport) : + http::Connection(transport), method_(method), curl_handle_(curl_handle) { + VLOG(1) << "curl::Connection created: " << method_; +} + +Connection::~Connection() { + VLOG(1) << "curl::Connection destroyed"; +} + +bool Connection::SendHeaders(const HeaderList& headers, ErrorPtr* error) { + headers_.insert(headers.begin(), headers.end()); + return true; +} + +bool Connection::WriteRequestData(const void* data, size_t size, + ErrorPtr* error) { + if (size > 0) { + auto data_ptr = reinterpret_cast<const unsigned char*>(data); + request_data_.insert(request_data_.end(), data_ptr, data_ptr + size); + } + return true; +} + +bool Connection::FinishRequest(ErrorPtr* error) { + if (VLOG_IS_ON(3)) { + curl_easy_setopt(curl_handle_, CURLOPT_DEBUGFUNCTION, curl_trace); + curl_easy_setopt(curl_handle_, CURLOPT_VERBOSE, 1L); + } + + // Set up HTTP request data. + if (method_ == request_type::kPut) { + curl_easy_setopt(curl_handle_, CURLOPT_INFILESIZE_LARGE, + curl_off_t(request_data_.size())); + } else { + curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDSIZE_LARGE, + curl_off_t(request_data_.size())); + } + if (!request_data_.empty()) { + curl_easy_setopt(curl_handle_, + CURLOPT_READFUNCTION, &Connection::read_callback); + curl_easy_setopt(curl_handle_, CURLOPT_READDATA, this); + VLOG(2) << "Raw request data: " + << std::string(reinterpret_cast<const char*>(request_data_.data()), + request_data_.size()); + } + + curl_slist* header_list = nullptr; + if (!headers_.empty()) { + for (auto pair : headers_) { + std::string header = string_utils::Join(": ", pair.first, pair.second); + VLOG(2) << "Request header: " << header; + header_list = curl_slist_append(header_list, header.c_str()); + } + curl_easy_setopt(curl_handle_, CURLOPT_HTTPHEADER, header_list); + } + + headers_.clear(); + + // Set up HTTP response data. + if (method_ != request_type::kHead) { + curl_easy_setopt(curl_handle_, + CURLOPT_WRITEFUNCTION, &Connection::write_callback); + curl_easy_setopt(curl_handle_, CURLOPT_WRITEDATA, this); + } + + // HTTP response headers + curl_easy_setopt(curl_handle_, + CURLOPT_HEADERFUNCTION, &Connection::header_callback); + curl_easy_setopt(curl_handle_, CURLOPT_HEADERDATA, this); + + CURLcode ret = curl_easy_perform(curl_handle_); + if (header_list) + curl_slist_free_all(header_list); + if (ret != CURLE_OK) { + Error::AddTo(error, http::curl::kErrorDomain, std::to_string(ret), + curl_easy_strerror(ret)); + } else { + LOG(INFO) << "Response: " << GetResponseStatusCode() << " (" + << GetResponseStatusText() << ")"; + VLOG(2) << "Response data (" << response_data_.size() << "): " + << std::string(reinterpret_cast<const char*>(response_data_.data()), + response_data_.size()); + } + return (ret == CURLE_OK); +} + +int Connection::GetResponseStatusCode() const { + long status_code = 0; // NOLINT(runtime/int) - curl expects a long here. + curl_easy_getinfo(curl_handle_, CURLINFO_RESPONSE_CODE, &status_code); + return status_code; +} + +std::string Connection::GetResponseStatusText() const { + return status_text_; +} + +std::string Connection::GetProtocolVersion() const { + return protocol_version_; +} + +std::string Connection::GetResponseHeader( + const std::string& header_name) const { + auto p = headers_.find(header_name); + return p != headers_.end() ? p->second : std::string(); +} + +uint64_t Connection::GetResponseDataSize() const { + return response_data_.size(); +} + +bool Connection::ReadResponseData(void* data, size_t buffer_size, + size_t* size_read, ErrorPtr* error) { + size_t size_to_read = response_data_.size() - response_data_ptr_; + if (size_to_read > buffer_size) + size_to_read = buffer_size; + memcpy(data, response_data_.data() + response_data_ptr_, size_to_read); + if (size_read) + *size_read = size_to_read; + response_data_ptr_ += size_to_read; + return true; +} + +size_t Connection::write_callback(char* ptr, size_t size, + size_t num, void* data) { + Connection* me = reinterpret_cast<Connection*>(data); + size_t data_len = size * num; + me->response_data_.insert(me->response_data_.end(), ptr, ptr + data_len); + return data_len; +} + +size_t Connection::read_callback(char* ptr, size_t size, + size_t num, void* data) { + Connection* me = reinterpret_cast<Connection*>(data); + size_t data_len = size * num; + + if (me->request_data_ptr_ >= me->request_data_.size()) + return 0; + + if (me->request_data_ptr_ + data_len > me->request_data_.size()) + data_len = me->request_data_.size() - me->request_data_ptr_; + + memcpy(ptr, me->request_data_.data() + me->request_data_ptr_, data_len); + me->request_data_ptr_ += data_len; + + return data_len; +} + +size_t Connection::header_callback(char* ptr, size_t size, + size_t num, void* data) { + Connection* me = reinterpret_cast<Connection*>(data); + size_t hdr_len = size * num; + std::string header(ptr, hdr_len); + // Remove newlines at the end of header line. + while (!header.empty() && (header.back() == '\r' || header.back() == '\n')) { + header.pop_back(); + } + + VLOG(2) << "Response header: " << header; + + if (!me->status_text_set_) { + // First header - response code as "HTTP/1.1 200 OK". + // Need to extract the OK part + auto pair = string_utils::SplitAtFirst(header, ' '); + me->protocol_version_ = pair.first; + me->status_text_ = string_utils::SplitAtFirst(pair.second, ' ').second; + me->status_text_set_ = true; + } else { + auto pair = string_utils::SplitAtFirst(header, ':'); + if (!pair.second.empty()) + me->headers_.insert(pair); + } + return hdr_len; +} + +} // namespace curl +} // namespace http +} // namespace buffet
diff --git a/buffet/http_connection_curl.h b/buffet/http_connection_curl.h new file mode 100644 index 0000000..9a82e26 --- /dev/null +++ b/buffet/http_connection_curl.h
@@ -0,0 +1,86 @@ +// 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_HTTP_CONNECTION_CURL_H_ +#define BUFFET_HTTP_CONNECTION_CURL_H_ + +#include <map> +#include <string> +#include <vector> + +#include <base/basictypes.h> +#include <curl/curl.h> + +#include "buffet/http_connection.h" + +namespace buffet { +namespace http { +namespace curl { + +// This is a libcurl-based implementation of http::Connection. +class Connection : public http::Connection { + public: + Connection(CURL* curl_handle, const std::string& method, + std::shared_ptr<http::Transport> transport); + virtual ~Connection(); + + // Overrides from http::Connection. + // See http_connection.h for description of these methods. + virtual bool SendHeaders(const HeaderList& headers, ErrorPtr* error) override; + virtual bool WriteRequestData(const void* data, size_t size, + ErrorPtr* error) override; + virtual bool FinishRequest(ErrorPtr* error) override; + + virtual int GetResponseStatusCode() const override; + virtual std::string GetResponseStatusText() const override; + virtual std::string GetProtocolVersion() const override; + virtual std::string GetResponseHeader( + const std::string& header_name) const override; + virtual uint64_t GetResponseDataSize() const override; + virtual bool ReadResponseData(void* data, size_t buffer_size, + size_t* size_read, ErrorPtr* error) override; + + protected: + // Write data callback. Used by CURL when receiving response data. + static size_t write_callback(char* ptr, size_t size, size_t num, void* data); + // Read data callback. Used by CURL when sending request body data. + static size_t read_callback(char* ptr, size_t size, size_t num, void* data); + // Write header data callback. Used by CURL when receiving response headers. + static size_t header_callback(char* ptr, size_t size, size_t num, void* data); + + // HTTP request verb, such as "GET", "POST", "PUT", ... + std::string method_; + + // Binary data for request body. + std::vector<unsigned char> request_data_; + // Read pointer for request data. Used when streaming data to the server. + size_t request_data_ptr_ = 0; + + // Received response data. + std::vector<unsigned char> response_data_; + size_t response_data_ptr_ = 0; + + // List of optional request headers provided by the caller. + // After request has been sent, contains the received response headers. + std::map<std::string, std::string> headers_; + + // HTTP protocol version, such as HTTP/1.1 + std::string protocol_version_; + // Response status text, such as "OK" for 200, or "Forbidden" for 403 + std::string status_text_; + // Flag used when parsing response headers to separate the response status + // from the rest of response headers. + bool status_text_set_ = false; + + CURL* curl_handle_ = nullptr; + + private: + DISALLOW_COPY_AND_ASSIGN(Connection); +}; + +} // namespace curl +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_CONNECTION_CURL_H_
diff --git a/buffet/http_connection_fake.cc b/buffet/http_connection_fake.cc new file mode 100644 index 0000000..87fbf23 --- /dev/null +++ b/buffet/http_connection_fake.cc
@@ -0,0 +1,94 @@ +// 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/http_connection_fake.h" + +#include <base/logging.h> + +#include "buffet/http_request.h" +#include "buffet/mime_utils.h" +#include "buffet/string_utils.h" + +namespace buffet { +namespace http { +namespace fake { + +Connection::Connection(const std::string& url, const std::string& method, + std::shared_ptr<http::Transport> transport) : + http::Connection(transport), request_(url, method) { + VLOG(1) << "fake::Connection created: " << method; +} + +Connection::~Connection() { + VLOG(1) << "fake::Connection destroyed"; +} + +bool Connection::SendHeaders(const HeaderList& headers, ErrorPtr* error) { + request_.AddHeaders(headers); + return true; +} + +bool Connection::WriteRequestData(const void* data, size_t size, + ErrorPtr* error) { + request_.AddData(data, size); + return true; +} + +bool Connection::FinishRequest(ErrorPtr* error) { + request_.AddHeaders({{request_header::kContentLength, + std::to_string(request_.GetData().size())}}); + fake::Transport* transport = static_cast<fake::Transport*>(transport_.get()); + CHECK(transport) << "Expecting a fake transport"; + auto handler = transport->GetHandler(request_.GetURL(), request_.GetMethod()); + if (handler.is_null()) { + LOG(ERROR) << "Received unexpected " << request_.GetMethod() + << " request at " << request_.GetURL(); + response_.ReplyText(status_code::NotFound, + "<html><body>Not found</body></html>", + mime::text::kHtml); + } else { + handler.Run(request_, &response_); + } + return true; +} + +int Connection::GetResponseStatusCode() const { + return response_.GetStatusCode(); +} + +std::string Connection::GetResponseStatusText() const { + return response_.GetStatusText(); +} + +std::string Connection::GetProtocolVersion() const { + return response_.GetProtocolVersion(); +} + +std::string Connection::GetResponseHeader( + const std::string& header_name) const { + return response_.GetHeader(header_name); +} + +uint64_t Connection::GetResponseDataSize() const { + // HEAD requests must not return body. + return (request_.GetMethod() != request_type::kHead) ? + response_.GetData().size() : 0; +} + +bool Connection::ReadResponseData(void* data, size_t buffer_size, + size_t* size_read, ErrorPtr* error) { + size_t size_to_read = GetResponseDataSize() - response_data_ptr_; + if (size_to_read > buffer_size) + size_to_read = buffer_size; + if (size_to_read > 0) + memcpy(data, response_.GetData().data() + response_data_ptr_, size_to_read); + if (size_read) + *size_read = size_to_read; + response_data_ptr_ += size_to_read; + return true; +} + +} // namespace fake +} // namespace http +} // namespace buffet
diff --git a/buffet/http_connection_fake.h b/buffet/http_connection_fake.h new file mode 100644 index 0000000..57bf016 --- /dev/null +++ b/buffet/http_connection_fake.h
@@ -0,0 +1,62 @@ +// 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_HTTP_CONNECTION_FAKE_H_ +#define BUFFET_HTTP_CONNECTION_FAKE_H_ + +#include <map> +#include <string> +#include <vector> + +#include <base/basictypes.h> + +#include "buffet/http_connection.h" +#include "buffet/http_transport_fake.h" + +namespace buffet { +namespace http { +namespace fake { + +// This is a fake implementation of http::Connection for unit testing. +class Connection : public http::Connection { + public: + Connection(const std::string& url, const std::string& method, + std::shared_ptr<http::Transport> transport); + virtual ~Connection(); + + // Overrides from http::Connection. + // See http_connection.h for description of these methods. + virtual bool SendHeaders(const HeaderList& headers, ErrorPtr* error) override; + virtual bool WriteRequestData(const void* data, size_t size, + ErrorPtr* error) override; + virtual bool FinishRequest(ErrorPtr* error) override; + + virtual int GetResponseStatusCode() const override; + virtual std::string GetResponseStatusText() const override; + virtual std::string GetProtocolVersion() const override; + virtual std::string GetResponseHeader( + const std::string& header_name) const override; + virtual uint64_t GetResponseDataSize() const override; + virtual bool ReadResponseData(void* data, size_t buffer_size, + size_t* size_read, ErrorPtr* error) override; + + private: + // Request and response objects passed to the user-provided request handler + // callback. The request object contains all the request information. + // The response object is the server response that is created by + // the handler in response to the request. + ServerRequest request_; + ServerResponse response_; + + // Internal read data pointer needed for ReadResponseData() implementation. + size_t response_data_ptr_ = 0; + + DISALLOW_COPY_AND_ASSIGN(Connection); +}; + +} // namespace fake +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_CONNECTION_FAKE_H_
diff --git a/buffet/http_request.cc b/buffet/http_request.cc new file mode 100644 index 0000000..df09ca4 --- /dev/null +++ b/buffet/http_request.cc
@@ -0,0 +1,297 @@ +// 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/http_request.h" + +#include <base/logging.h> + +#include "buffet/http_connection_curl.h" +#include "buffet/http_transport_curl.h" +#include "buffet/map_utils.h" +#include "buffet/mime_utils.h" +#include "buffet/string_utils.h" + +namespace buffet { +namespace http { + +// request_type +const char request_type::kOptions[] = "OPTIONS"; +const char request_type::kGet[] = "GET"; +const char request_type::kHead[] = "HEAD"; +const char request_type::kPost[] = "POST"; +const char request_type::kPut[] = "PUT"; +const char request_type::kPatch[] = "PATCH"; +const char request_type::kDelete[] = "DELETE"; +const char request_type::kTrace[] = "TRACE"; +const char request_type::kConnect[] = "CONNECT"; +const char request_type::kCopy[] = "COPY"; +const char request_type::kMove[] = "MOVE"; + +// request_header +const char request_header::kAccept[] = "Accept"; +const char request_header::kAcceptCharset[] = "Accept-Charset"; +const char request_header::kAcceptEncoding[] = "Accept-Encoding"; +const char request_header::kAcceptLanguage[] = "Accept-Language"; +const char request_header::kAllow[] = "Allow"; +const char request_header::kAuthorization[] = "Authorization"; +const char request_header::kCacheControl[] = "Cache-Control"; +const char request_header::kConnection[] = "Connection"; +const char request_header::kContentEncoding[] = "Content-Encoding"; +const char request_header::kContentLanguage[] = "Content-Language"; +const char request_header::kContentLength[] = "Content-Length"; +const char request_header::kContentLocation[] = "Content-Location"; +const char request_header::kContentMd5[] = "Content-MD5"; +const char request_header::kContentRange[] = "Content-Range"; +const char request_header::kContentType[] = "Content-Type"; +const char request_header::kCookie[] = "Cookie"; +const char request_header::kDate[] = "Date"; +const char request_header::kExpect[] = "Expect"; +const char request_header::kExpires[] = "Expires"; +const char request_header::kFrom[] = "From"; +const char request_header::kHost[] = "Host"; +const char request_header::kIfMatch[] = "If-Match"; +const char request_header::kIfModifiedSince[] = "If-Modified-Since"; +const char request_header::kIfNoneMatch[] = "If-None-Match"; +const char request_header::kIfRange[] = "If-Range"; +const char request_header::kIfUnmodifiedSince[] = "If-Unmodified-Since"; +const char request_header::kLastModified[] = "Last-Modified"; +const char request_header::kMaxForwards[] = "Max-Forwards"; +const char request_header::kPragma[] = "Pragma"; +const char request_header::kProxyAuthorization[] = "Proxy-Authorization"; +const char request_header::kRange[] = "Range"; +const char request_header::kReferer[] = "Referer"; +const char request_header::kTE[] = "TE"; +const char request_header::kTrailer[] = "Trailer"; +const char request_header::kTransferEncoding[] = "Transfer-Encoding"; +const char request_header::kUpgrade[] = "Upgrade"; +const char request_header::kUserAgent[] = "User-Agent"; +const char request_header::kVia[] = "Via"; +const char request_header::kWarning[] = "Warning"; + +// response_header +const char response_header::kAcceptRanges[] = "Accept-Ranges"; +const char response_header::kAge[] = "Age"; +const char response_header::kAllow[] = "Allow"; +const char response_header::kCacheControl[] = "Cache-Control"; +const char response_header::kConnection[] = "Connection"; +const char response_header::kContentEncoding[] = "Content-Encoding"; +const char response_header::kContentLanguage[] = "Content-Language"; +const char response_header::kContentLength[] = "Content-Length"; +const char response_header::kContentLocation[] = "Content-Location"; +const char response_header::kContentMd5[] = "Content-MD5"; +const char response_header::kContentRange[] = "Content-Range"; +const char response_header::kContentType[] = "Content-Type"; +const char response_header::kDate[] = "Date"; +const char response_header::kETag[] = "ETag"; +const char response_header::kExpires[] = "Expires"; +const char response_header::kLastModified[] = "Last-Modified"; +const char response_header::kLocation[] = "Location"; +const char response_header::kPragma[] = "Pragma"; +const char response_header::kProxyAuthenticate[] = "Proxy-Authenticate"; +const char response_header::kRetryAfter[] = "Retry-After"; +const char response_header::kServer[] = "Server"; +const char response_header::kSetCookie[] = "Set-Cookie"; +const char response_header::kTrailer[] = "Trailer"; +const char response_header::kTransferEncoding[] = "Transfer-Encoding"; +const char response_header::kUpgrade[] = "Upgrade"; +const char response_header::kVary[] = "Vary"; +const char response_header::kVia[] = "Via"; +const char response_header::kWarning[] = "Warning"; +const char response_header::kWwwAuthenticate[] = "WWW-Authenticate"; + +// *********************************************************** +// ********************** Request Class ********************** +// *********************************************************** +Request::Request(const std::string& url, const char* method, + std::shared_ptr<Transport> transport) : + transport_(transport), request_url_(url), method_(method) { + VLOG(1) << "http::Request created"; + if (!transport_) + transport_.reset(new http::curl::Transport()); +} + +Request::~Request() { + VLOG(1) << "http::Request destroyed"; +} + +void Request::AddRange(int64_t bytes) { + if (bytes < 0) { + ranges_.emplace_back(Request::range_value_omitted, -bytes); + } else { + ranges_.emplace_back(bytes, Request::range_value_omitted); + } +} + +void Request::AddRange(uint64_t from_byte, uint64_t to_byte) { + ranges_.emplace_back(from_byte, to_byte); +} + +std::unique_ptr<Response> Request::GetResponse(ErrorPtr* error) { + if (!SendRequestIfNeeded(error) || !connection_->FinishRequest(error)) + return std::unique_ptr<Response>(); + std::unique_ptr<Response> response(new Response(std::move(connection_))); + transport_.reset(); // Indicate that the response has been received + return response; +} + +void Request::SetAccept(const char* accept_mime_types) { + accept_ = accept_mime_types; +} + +std::string Request::GetAccept() const { + return accept_; +} + +void Request::SetContentType(const char* contentType) { + content_type_ = contentType; +} + +std::string Request::GetContentType() const { + return content_type_; +} + +void Request::AddHeader(const char* header, const char* value) { + headers_[header] = value; +} + +void Request::AddHeaders(const HeaderList& headers) { + headers_.insert(headers.begin(), headers.end()); +} + +bool Request::AddRequestBody(const void* data, size_t size, ErrorPtr* error) { + if (!SendRequestIfNeeded(error)) + return false; + return connection_->WriteRequestData(data, size, error); +} + +void Request::SetReferer(const char* referer) { + referer_ = referer; +} + +std::string Request::GetReferer() const { + return referer_; +} + +void Request::SetUserAgent(const char* user_agent) { + user_agent_ = user_agent; +} + +std::string Request::GetUserAgent() const { + return user_agent_; +} + +bool Request::SendRequestIfNeeded(ErrorPtr* error) { + if (transport_) { + if (!connection_) { + http::HeaderList headers = MapToVector(headers_); + std::vector<std::string> ranges; + if (method_ != request_type::kHead) { + ranges.reserve(ranges_.size()); + for (auto p : ranges_) { + if (p.first != range_value_omitted || + p.second != range_value_omitted) { + std::string range; + if (p.first != range_value_omitted) { + range = std::to_string(p.first); + } + range += '-'; + if (p.second != range_value_omitted) { + range += std::to_string(p.second); + } + ranges.push_back(range); + } + } + } + if (!ranges.empty()) + headers.emplace_back(request_header::kRange, + "bytes=" + string_utils::Join(',', ranges)); + + headers.emplace_back(request_header::kAccept, GetAccept()); + if (method_ != request_type::kGet && method_ != request_type::kHead) { + if (!content_type_.empty()) + headers.emplace_back(request_header::kContentType, content_type_); + } + connection_ = transport_->CreateConnection(transport_, request_url_, + method_, headers, + user_agent_, referer_, + error); + } + + if (connection_) + return true; + } else { + Error::AddTo(error, http::curl::kErrorDomain, + "request_already_received", "HTTP response already received"); + } + return false; +} + +// ************************************************************ +// ********************** Response Class ********************** +// ************************************************************ +Response::Response(std::unique_ptr<Connection> connection) + : connection_(std::move(connection)) { + VLOG(1) << "http::Response created"; + // Response object doesn't have streaming interface for response data (yet), + // so read the data into a buffer and cache it. + if (connection_) { + size_t size = static_cast<size_t>(connection_->GetResponseDataSize()); + response_data_.reserve(size); + unsigned char buffer[1024]; + size_t read = 0; + while (connection_->ReadResponseData(buffer, sizeof(buffer), + &read, nullptr) && read > 0) { + response_data_.insert(response_data_.end(), buffer, buffer + read); + } + } +} + +Response::~Response() { + VLOG(1) << "http::Response destroyed"; +} + +bool Response::IsSuccessful() const { + int code = GetStatusCode(); + return code >= status_code::Continue && code < status_code::BadRequest; +} + +int Response::GetStatusCode() const { + if (!connection_) + return -1; + + return connection_->GetResponseStatusCode(); +} + +std::string Response::GetStatusText() const { + if (!connection_) + return std::string(); + + return connection_->GetResponseStatusText(); +} + +std::string Response::GetContentType() const { + return GetHeader(response_header::kContentType); +} + +const std::vector<unsigned char>& Response::GetData() const { + return response_data_; +} + +std::string Response::GetDataAsString() const { + if (response_data_.empty()) + return std::string(); + + const char* data_buf = reinterpret_cast<const char*>(response_data_.data()); + return std::string(data_buf, data_buf + response_data_.size()); +} + +std::string Response::GetHeader(const char* header_name) const { + if (connection_) + return connection_->GetResponseHeader(header_name); + + return std::string(); +} + +} // namespace http +} // namespace buffet
diff --git a/buffet/http_request.h b/buffet/http_request.h new file mode 100644 index 0000000..8aedc45 --- /dev/null +++ b/buffet/http_request.h
@@ -0,0 +1,351 @@ +// 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_HTTP_REQUEST_H_ +#define BUFFET_HTTP_REQUEST_H_ + +#include <map> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include <base/basictypes.h> + +#include "buffet/http_connection.h" +#include "buffet/http_transport.h" +#include "buffet/error.h" + +namespace buffet { +namespace http { + +// HTTP request verbs +namespace request_type { + extern const char kOptions[]; + extern const char kGet[]; + extern const char kHead[]; + extern const char kPost[]; + extern const char kPut[]; + extern const char kPatch[]; // Not a standard HTTP/1.1 request method + extern const char kDelete[]; + extern const char kTrace[]; + extern const char kConnect[]; + extern const char kCopy[]; // Not a standard HTTP/1.1 request method + extern const char kMove[]; // Not a standard HTTP/1.1 request method +} // namespace request_type + +// HTTP request header names +namespace request_header { + extern const char kAccept[]; + extern const char kAcceptCharset[]; + extern const char kAcceptEncoding[]; + extern const char kAcceptLanguage[]; + extern const char kAllow[]; + extern const char kAuthorization[]; + extern const char kCacheControl[]; + extern const char kConnection[]; + extern const char kContentEncoding[]; + extern const char kContentLanguage[]; + extern const char kContentLength[]; + extern const char kContentLocation[]; + extern const char kContentMd5[]; + extern const char kContentRange[]; + extern const char kContentType[]; + extern const char kCookie[]; + extern const char kDate[]; + extern const char kExpect[]; + extern const char kExpires[]; + extern const char kFrom[]; + extern const char kHost[]; + extern const char kIfMatch[]; + extern const char kIfModifiedSince[]; + extern const char kIfNoneMatch[]; + extern const char kIfRange[]; + extern const char kIfUnmodifiedSince[]; + extern const char kLastModified[]; + extern const char kMaxForwards[]; + extern const char kPragma[]; + extern const char kProxyAuthorization[]; + extern const char kRange[]; + extern const char kReferer[]; + extern const char kTE[]; + extern const char kTrailer[]; + extern const char kTransferEncoding[]; + extern const char kUpgrade[]; + extern const char kUserAgent[]; + extern const char kVia[]; + extern const char kWarning[]; +} // namespace request_header + +// HTTP response header names +namespace response_header { + extern const char kAcceptRanges[]; + extern const char kAge[]; + extern const char kAllow[]; + extern const char kCacheControl[]; + extern const char kConnection[]; + extern const char kContentEncoding[]; + extern const char kContentLanguage[]; + extern const char kContentLength[]; + extern const char kContentLocation[]; + extern const char kContentMd5[]; + extern const char kContentRange[]; + extern const char kContentType[]; + extern const char kDate[]; + extern const char kETag[]; + extern const char kExpires[]; + extern const char kLastModified[]; + extern const char kLocation[]; + extern const char kPragma[]; + extern const char kProxyAuthenticate[]; + extern const char kRetryAfter[]; + extern const char kServer[]; + extern const char kSetCookie[]; + extern const char kTrailer[]; + extern const char kTransferEncoding[]; + extern const char kUpgrade[]; + extern const char kVary[]; + extern const char kVia[]; + extern const char kWarning[]; + extern const char kWwwAuthenticate[]; +} // namespace response_header + +// HTTP request status (error) codes +namespace status_code { + // OK to continue with request + static const int Continue = 100; + // Server has switched protocols in upgrade header + static const int SwitchProtocols = 101; + + // Request completed + static const int Ok = 200; + // Object created, reason = new URI + static const int Created = 201; + // Async completion (TBS) + static const int Accepted = 202; + // Partial completion + static const int Partial = 203; + // No info to return + static const int NoContent = 204; + // Request completed, but clear form + static const int ResetContent = 205; + // Partial GET fulfilled + static const int PartialContent = 206; + + // Server couldn't decide what to return + static const int Ambiguous = 300; + // Object permanently moved + static const int Moved = 301; + // Object temporarily moved + static const int Redirect = 302; + // Redirection w/ new access method + static const int RedirectMethod = 303; + // If-Modified-Since was not modified + static const int NotModified = 304; + // Redirection to proxy, location header specifies proxy to use + static const int UseProxy = 305; + // HTTP/1.1: keep same verb + static const int RedirectKeepVerb = 307; + + // Invalid syntax + static const int BadRequest = 400; + // Access denied + static const int Denied = 401; + // Payment required + static const int PaymentRequired = 402; + // Request forbidden + static const int Forbidden = 403; + // Object not found + static const int NotFound = 404; + // Method is not allowed + static const int BadMethod = 405; + // No response acceptable to client found + static const int NoneAcceptable = 406; + // Proxy authentication required + static const int ProxyAuthRequired = 407; + // Server timed out waiting for request + static const int RequestTimeout = 408; + // User should resubmit with more info + static const int Conflict = 409; + // The resource is no longer available + static const int Gone = 410; + // The server refused to accept request w/o a length + static const int LengthRequired = 411; + // Precondition given in request failed + static const int PrecondionFailed = 412; + // Request entity was too large + static const int RequestTooLarge = 413; + // Request URI too long + static const int UriTooLong = 414; + // Unsupported media type + static const int UnsupportedMedia = 415; + // Retry after doing the appropriate action. + static const int RetryWith = 449; + + // Internal server error + static const int InternalServerError = 500; + // Request not supported + static const int NotSupported = 501; + // Error response received from gateway + static const int BadGateway = 502; + // Temporarily overloaded + static const int ServiceUnavailable = 503; + // Timed out waiting for gateway + static const int GatewayTimeout = 504; + // HTTP version not supported + static const int VersionNotSupported = 505; +} // namespace status_code + +class Response; // Just a forward declaration. + +/////////////////////////////////////////////////////////////////////////////// +// Request class is the main object used to set up and initiate an HTTP +// communication session. It is used to specify the HTTP request method, +// request URL and many optional parameters (such as HTTP headers, user agent, +// referer URL and so on. +// +// Once everything is setup, GetResponse() method is used to send the request +// and obtain the server response. The returned Response object can be +// used to inspect the response code, HTTP headers and/or response body. +/////////////////////////////////////////////////////////////////////////////// +class Request { + public: + // The main constructor. |url| specifies the remote host address/path + // to send the request to. |method| is the HTTP request verb and + // |transport| is the HTTP transport implementation for server communications. + Request(const std::string& url, const char* method, + std::shared_ptr<Transport> transport); + ~Request(); + + // Gets/Sets "Accept:" header value. The default value is "*/*" if not set. + void SetAccept(const char* accept_mime_types); + std::string GetAccept() const; + + // Gets/Sets "Content-Type:" header value + void SetContentType(const char* content_type); + std::string GetContentType() const; + + // Adds additional HTTP request header + void AddHeader(const char* header, const char* value); + void AddHeaders(const HeaderList& headers); + + // Removes HTTP request header + void RemoveHeader(const char* header); + + // Adds a request body. This is not to be used with GET method + bool AddRequestBody(const void* data, size_t size, ErrorPtr* error); + + // Makes a request for a subrange of data. Specifies a partial range with + // either from beginning of the data to the specified offset (if |bytes| is + // negative) or from the specified offset to the end of data (if |bytes| is + // positive). + // All individual ranges will be sent as part of "Range:" HTTP request header. + void AddRange(int64_t bytes); + + // Makes a request for a subrange of data. Specifies a full range with + // start and end bytes from the beginning of the requested data. + // All individual ranges will be sent as part of "Range:" HTTP request header. + void AddRange(uint64_t from_byte, uint64_t to_byte); + + // Returns the request URL + std::string GetRequestURL() const; + + // Gets/Sets a request referer URL (sent as "Referer:" request header). + void SetReferer(const char* referer); + std::string GetReferer() const; + + // Gets/Sets a user agent string (sent as "User-Agent:" request header). + void SetUserAgent(const char* user_agent); + std::string GetUserAgent() const; + + // Sends the request to the server and returns the response object. + // In case the server couldn't be reached for whatever reason, returns + // empty unique_ptr (null). In such a case, the additional error information + // can be returned through the optional supplied |error| parameter. + std::unique_ptr<Response> GetResponse(ErrorPtr* error); + + private: + // Helper function to create an http::Connection and send off request headers. + bool SendRequestIfNeeded(ErrorPtr* error); + + // Implementation that provides particular HTTP transport. + std::shared_ptr<Transport> transport_; + + // An established connection for adding request body. This connection + // is maintained by the request object after the headers have been + // sent and before the response is requested. + std::unique_ptr<Connection> connection_; + + // Full request URL, such as "http://www.host.com/path/to/object" + std::string request_url_; + // HTTP request verb, such as "GET", "POST", "PUT", ... + std::string method_; + + // Referrer URL, if any. Sent to the server via "Referer: " header. + std::string referer_; + // User agent string, if any. Sent to the server via "User-Agent: " header. + std::string user_agent_; + // Content type of the request body data. + // Sent to the server via "Content-Type: " header. + std::string content_type_; + // List of acceptable response data types. + // Sent to the server via "Accept: " header. + std::string accept_ = "*/*"; + + // List of optional request headers provided by the caller. + // After request has been sent, contains the received response headers. + std::map<std::string, std::string> headers_; + // List of optional data ranges to request partial content from the server. + // Sent to the server as "Range: " header. + std::vector<std::pair<uint64_t, uint64_t>> ranges_; + + // range_value_omitted is used in |ranges_| list to indicate omitted value. + // E.g. range (10,range_value_omitted) represents bytes from 10 to the end + // of the data stream. + const uint64_t range_value_omitted = (uint64_t)-1; + + DISALLOW_COPY_AND_ASSIGN(Request); +}; + +/////////////////////////////////////////////////////////////////////////////// +// Response class is returned from Request::GetResponse() and is a way +// to get to response status, error codes, response HTTP headers and response +// data (body) if available. +/////////////////////////////////////////////////////////////////////////////// +class Response { + public: + explicit Response(std::unique_ptr<Connection> connection); + ~Response(); + + // Returns true if server returned a success code (status code below 400). + bool IsSuccessful() const; + + // Returns the HTTP status code (e.g. 200 for success) + int GetStatusCode() const; + + // Returns the status text (e.g. for error 403 it could be "NOT AUTHORIZED"). + std::string GetStatusText() const; + + // Returns the content type of the response data. + std::string GetContentType() const; + + // Returns response data as a byte array + const std::vector<unsigned char>& GetData() const; + + // Returns response data as a string + std::string GetDataAsString() const; + + // Returns a value of a given response HTTP header. + std::string GetHeader(const char* header_name) const; + + private: + std::unique_ptr<Connection> connection_; + std::vector<unsigned char> response_data_; + DISALLOW_COPY_AND_ASSIGN(Response); +}; + +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_REQUEST_H_
diff --git a/buffet/http_transport.h b/buffet/http_transport.h new file mode 100644 index 0000000..e26ee77 --- /dev/null +++ b/buffet/http_transport.h
@@ -0,0 +1,54 @@ +// 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_HTTP_TRANSPORT_H_ +#define BUFFET_HTTP_TRANSPORT_H_ + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include <base/basictypes.h> + +#include "buffet/error.h" + +namespace buffet { +namespace http { + +typedef std::vector<std::pair<std::string, std::string>> HeaderList; + +class Request; +class Connection; + +/////////////////////////////////////////////////////////////////////////////// +// Transport is a base class for specific implementation of HTTP communication. +// This class (and its underlying implementation) is used by http::Request and +// http::Response classes to provide HTTP functionality to the clients. +/////////////////////////////////////////////////////////////////////////////// +class Transport { + public: + Transport() = default; + virtual ~Transport() = default; + + // Creates a connection object and initializes it with the specified data. + // |transport| is a shared pointer to this transport object instance, + // used to maintain the object alive as long as the connection exists. + virtual std::unique_ptr<Connection> CreateConnection( + std::shared_ptr<Transport> transport, + const std::string& url, + const std::string& method, + const HeaderList& headers, + const std::string& user_agent, + const std::string& referer, + ErrorPtr* error) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(Transport); +}; + +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_TRANSPORT_H_
diff --git a/buffet/http_transport_curl.cc b/buffet/http_transport_curl.cc new file mode 100644 index 0000000..7ef914b --- /dev/null +++ b/buffet/http_transport_curl.cc
@@ -0,0 +1,81 @@ +// 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/http_transport_curl.h" + +#include <base/logging.h> + +#include "buffet/http_connection_curl.h" +#include "buffet/http_request.h" + +namespace buffet { +namespace http { +namespace curl { + +const char kErrorDomain[] = "http_transport"; + +Transport::Transport() { + VLOG(1) << "curl::Transport created"; +} + +Transport::~Transport() { + VLOG(1) << "curl::Transport destroyed"; +} + +std::unique_ptr<http::Connection> Transport::CreateConnection( + std::shared_ptr<http::Transport> transport, + const std::string& url, + const std::string& method, + const HeaderList& headers, + const std::string& user_agent, + const std::string& referer, + ErrorPtr* error) { + CURL* curl_handle = curl_easy_init(); + if (!curl_handle) { + LOG(ERROR) << "Failed to initialize CURL"; + Error::AddTo(error, http::curl::kErrorDomain, "curl_init_failed", + "Failed to initialize CURL"); + return std::unique_ptr<http::Connection>(); + } + + LOG(INFO) << "Sending a " << method << " request to " << url; + curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str()); + + if (!user_agent.empty()) { + curl_easy_setopt(curl_handle, + CURLOPT_USERAGENT, user_agent.c_str()); + } + + if (!referer.empty()) { + curl_easy_setopt(curl_handle, + CURLOPT_REFERER, referer.c_str()); + } + + // Setup HTTP request method and optional request body. + if (method == request_type::kGet) { + curl_easy_setopt(curl_handle, CURLOPT_HTTPGET, 1L); + } else if (method == request_type::kHead) { + curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 1L); + } else if (method == request_type::kPut) { + curl_easy_setopt(curl_handle, CURLOPT_UPLOAD, 1L); + } else { + // POST and custom request methods + curl_easy_setopt(curl_handle, CURLOPT_POST, 1L); + curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, nullptr); + if (method != request_type::kPost) + curl_easy_setopt(curl_handle, CURLOPT_CUSTOMREQUEST, method.c_str()); + } + + std::unique_ptr<http::Connection> connection( + new http::curl::Connection(curl_handle, method, transport)); + CHECK(connection) << "Unable to create Connection object"; + if (!connection->SendHeaders(headers, error)) { + connection.reset(); + } + return connection; +} + +} // namespace curl +} // namespace http +} // namespace buffet
diff --git a/buffet/http_transport_curl.h b/buffet/http_transport_curl.h new file mode 100644 index 0000000..ddb24ce --- /dev/null +++ b/buffet/http_transport_curl.h
@@ -0,0 +1,47 @@ +// 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_HTTP_TRANSPORT_CURL_H_ +#define BUFFET_HTTP_TRANSPORT_CURL_H_ + +#include <string> + +#include "buffet/http_transport.h" + +namespace buffet { +namespace http { +namespace curl { + +extern const char kErrorDomain[]; + +/////////////////////////////////////////////////////////////////////////////// +// An implementation of http::Transport that uses libcurl for +// HTTP communications. This class (as http::Transport base) +// is used by http::Request and http::Response classes to provide HTTP +// functionality to the clients. +// See http_transport.h for more details. +/////////////////////////////////////////////////////////////////////////////// +class Transport : public http::Transport { + public: + Transport(); + virtual ~Transport(); + + virtual std::unique_ptr<http::Connection> CreateConnection( + std::shared_ptr<http::Transport> transport, + const std::string& url, + const std::string& method, + const HeaderList& headers, + const std::string& user_agent, + const std::string& referer, + ErrorPtr* error) override; + + private: + DISALLOW_COPY_AND_ASSIGN(Transport); +}; + +} // namespace curl +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_TRANSPORT_CURL_H_
diff --git a/buffet/http_transport_fake.cc b/buffet/http_transport_fake.cc new file mode 100644 index 0000000..0e7dfc5 --- /dev/null +++ b/buffet/http_transport_fake.cc
@@ -0,0 +1,260 @@ +// 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/http_transport_fake.h" + +#include <utility> + +#include <base/json/json_reader.h> +#include <base/json/json_writer.h> +#include <base/logging.h> + +#include "buffet/bind_lambda.h" +#include "buffet/http_connection_fake.h" +#include "buffet/http_request.h" +#include "buffet/mime_utils.h" +#include "buffet/url_utils.h" + +namespace buffet { + +using http::fake::Transport; +using http::fake::ServerRequestResponseBase; +using http::fake::ServerRequest; +using http::fake::ServerResponse; + +Transport::Transport() { + VLOG(1) << "fake::Transport created"; +} + +Transport::~Transport() { + VLOG(1) << "fake::Transport destroyed"; +} + +std::unique_ptr<http::Connection> Transport::CreateConnection( + std::shared_ptr<http::Transport> transport, + const std::string& url, + const std::string& method, + const HeaderList& headers, + const std::string& user_agent, + const std::string& referer, + ErrorPtr* error) { + HeaderList headers_copy = headers; + if (!user_agent.empty()) { + headers_copy.push_back(std::make_pair(http::request_header::kUserAgent, + user_agent)); + } + if (!referer.empty()) { + headers_copy.push_back(std::make_pair(http::request_header::kReferer, + referer)); + } + std::unique_ptr<http::Connection> connection( + new http::fake::Connection(url, method, transport)); + CHECK(connection) << "Unable to create Connection object"; + if (!connection->SendHeaders(headers_copy, error)) + connection.reset(); + request_count_++; + return connection; +} + +static inline std::string GetHandlerMapKey(const std::string& url, + const std::string& method) { + return method + ":" + url; +} + +void Transport::AddHandler(const std::string& url, const std::string& method, + const HandlerCallback& handler) { + handlers_.insert(std::make_pair(GetHandlerMapKey(url, method), handler)); +} + +void Transport::AddSimpleReplyHandler(const std::string& url, + const std::string& method, + int status_code, + const std::string& reply_text, + const std::string& mime_type) { + auto handler = [status_code, reply_text, mime_type]( + const ServerRequest& request, ServerResponse* response) { + response->ReplyText(status_code, reply_text, mime_type.c_str()); + }; + AddHandler(url, method, base::Bind(handler)); +} + +Transport::HandlerCallback Transport::GetHandler( + const std::string& url, const std::string& method) const { + // First try the exact combination of URL/Method + auto p = handlers_.find(GetHandlerMapKey(url, method)); + if (p != handlers_.end()) + return p->second; + // If not found, try URL/* + p = handlers_.find(GetHandlerMapKey(url, "*")); + if (p != handlers_.end()) + return p->second; + // If still not found, try */method + p = handlers_.find(GetHandlerMapKey("*", method)); + if (p != handlers_.end()) + return p->second; + // Finally, try */* + p = handlers_.find(GetHandlerMapKey("*", "*")); + return (p != handlers_.end()) ? p->second : HandlerCallback(); +} + +void ServerRequestResponseBase::AddData(const void* data, size_t data_size) { + auto bytes = reinterpret_cast<const unsigned char*>(data); + data_.insert(data_.end(), bytes, bytes + data_size); +} + +std::string ServerRequestResponseBase::GetDataAsString() const { + if (data_.empty()) + return std::string(); + auto chars = reinterpret_cast<const char*>(data_.data()); + return std::string(chars, data_.size()); +} + +std::unique_ptr<base::DictionaryValue> + ServerRequestResponseBase::GetDataAsJson() const { + if (mime::RemoveParameters(GetHeader(request_header::kContentType)) == + mime::application::kJson) { + auto value = base::JSONReader::Read(GetDataAsString()); + if (value) { + base::DictionaryValue* dict = nullptr; + if (value->GetAsDictionary(&dict)) { + return std::unique_ptr<base::DictionaryValue>(dict); + } else { + delete value; + } + } + } + return std::unique_ptr<base::DictionaryValue>(); +} + +void ServerRequestResponseBase::AddHeaders(const HeaderList& headers) { + for (auto&& pair : headers) { + if (pair.second.empty()) + headers_.erase(pair.first); + else + headers_.insert(pair); + } +} + +std::string ServerRequestResponseBase::GetHeader( + const std::string& header_name) const { + auto p = headers_.find(header_name); + return p != headers_.end() ? p->second : std::string(); +} + +ServerRequest::ServerRequest(const std::string& url, + const std::string& method) : method_(method) { + auto params = url::GetQueryStringParameters(url); + url_ = url::RemoveQueryString(url, true); + form_fields_.insert(params.begin(), params.end()); +} + +std::string ServerRequest::GetFormField(const std::string& field_name) const { + if (!form_fields_parsed_) { + std::string mime_type = mime::RemoveParameters( + GetHeader(request_header::kContentType)); + if (mime_type == mime::application::kWwwFormUrlEncoded && + !GetData().empty()) { + auto fields = data_encoding::WebParamsDecode(GetDataAsString()); + form_fields_.insert(fields.begin(), fields.end()); + } + form_fields_parsed_ = true; + } + auto p = form_fields_.find(field_name); + return p != form_fields_.end() ? p->second : std::string(); +} + +void ServerResponse::Reply(int status_code, const void* data, size_t data_size, + const char* mime_type) { + data_.clear(); + status_code_ = status_code; + AddData(data, data_size); + AddHeaders({ + {response_header::kContentLength, std::to_string(data_size)}, + {response_header::kContentType, mime_type} + }); +} + +void ServerResponse::ReplyText(int status_code, const std::string& text, + const char* mime_type) { + Reply(status_code, text.data(), text.size(), mime_type); +} + +void ServerResponse::ReplyJson(int status_code, const base::Value* json) { + std::string text; + base::JSONWriter::WriteWithOptions(json, + base::JSONWriter::OPTIONS_PRETTY_PRINT, + &text); + std::string mime_type = mime::AppendParameter(mime::application::kJson, + mime::parameters::kCharset, + "utf-8"); + ReplyText(status_code, text, mime_type.c_str()); +} + +void ServerResponse::ReplyJson(int status_code, + const http::FormFieldList& fields) { + base::DictionaryValue json; + for (auto&& pair : fields) { + json.SetString(pair.first, pair.second); + } + ReplyJson(status_code, &json); +} + +std::string ServerResponse::GetStatusText() const { + static std::vector<std::pair<int, const char*>> status_text_map = { + {100, "Continue"}, + {101, "Switching Protocols"}, + {102, "Processing"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {207, "Multi-Status"}, + {208, "Already Reported"}, + {226, "IM Used"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {305, "Use Proxy"}, + {306, "Switch Proxy"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Request Entity Too Large"}, + {414, "Request - URI Too Long"}, + {415, "Unsupported Media Type"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + }; + + for (auto&& pair : status_text_map) { + if (pair.first == status_code_) + return pair.second; + } + return std::string(); +} + +} // namespace buffet
diff --git a/buffet/http_transport_fake.h b/buffet/http_transport_fake.h new file mode 100644 index 0000000..7905f3e --- /dev/null +++ b/buffet/http_transport_fake.h
@@ -0,0 +1,223 @@ +// 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_HTTP_TRANSPORT_FAKE_H_ +#define BUFFET_HTTP_TRANSPORT_FAKE_H_ + +#include <map> +#include <string> +#include <type_traits> +#include <vector> + +#include <base/callback.h> +#include <base/values.h> + +#include "buffet/http_transport.h" +#include "buffet/http_utils.h" + +namespace buffet { +namespace http { +namespace fake { + +class ServerRequest; +class ServerResponse; +class Connection; + +/////////////////////////////////////////////////////////////////////////////// +// A fake implementation of http::Transport that simulates HTTP communication +// with a server. +/////////////////////////////////////////////////////////////////////////////// +class Transport : public http::Transport { + public: + Transport(); + virtual ~Transport(); + + // Server handler callback signature. + typedef base::Callback<void(const ServerRequest&, ServerResponse*)> + HandlerCallback; + + // This method allows the test code to provide a callback to handle requests + // for specific URL/HTTP-verb combination. When a specific |method| request + // is made on the given |url|, the |handler| will be invoked and all the + // request data will be filled in the |ServerRequest| parameter. Any server + // response should be returned through the |ServerResponse| parameter. + // Either |method| or |url| (or both) can be specified as "*" to handle + // any requests. So, ("http://localhost","*") will handle any request type + // on that URL and ("*","GET") will handle any GET requests. + // The lookup starts with the most specific data pair to the catch-all (*,*). + void AddHandler(const std::string& url, const std::string& method, + const HandlerCallback& handler); + // Simple version of AddHandler. AddSimpleReplyHandler just returns the + // specified text response of given MIME type. + void AddSimpleReplyHandler(const std::string& url, + const std::string& method, + int status_code, + const std::string& reply_text, + const std::string& mime_type); + // Retrieve a handler for specific |url| and request |method|. + HandlerCallback GetHandler(const std::string& url, + const std::string& method) const; + + // For tests that want to assert on the number of HTTP requests sent, + // these methods can be used to do just that. + int GetRequestCount() const { return request_count_; } + void ResetRequestCount() { request_count_ = 0; } + + // Overload from http::Transport + virtual std::unique_ptr<http::Connection> CreateConnection( + std::shared_ptr<http::Transport> transport, + const std::string& url, + const std::string& method, + const HeaderList& headers, + const std::string& user_agent, + const std::string& referer, + ErrorPtr* error) override; + + private: + // A list of user-supplied request handlers. + std::map<std::string, HandlerCallback> handlers_; + // Counter incremented each time a request is made. + int request_count_ = 0; + + DISALLOW_COPY_AND_ASSIGN(Transport); +}; + +/////////////////////////////////////////////////////////////////////////////// +// A base class for ServerRequest and ServerResponse. It provides common +// functionality to work with request/response HTTP headers and data. +/////////////////////////////////////////////////////////////////////////////// +class ServerRequestResponseBase { + public: + ServerRequestResponseBase() = default; + + // Add/retrieve request/response body data. + void AddData(const void* data, size_t data_size); + const std::vector<unsigned char>& GetData() const { return data_; } + std::string GetDataAsString() const; + std::unique_ptr<base::DictionaryValue> GetDataAsJson() const; + + // Add/retrieve request/response HTTP headers. + void AddHeaders(const HeaderList& headers); + std::string GetHeader(const std::string& header_name) const; + const std::map<std::string, std::string>& GetHeaders() const { + return headers_; + } + + protected: + // Data buffer. + std::vector<unsigned char> data_; + // Header map. + std::map<std::string, std::string> headers_; + + private: + DISALLOW_COPY_AND_ASSIGN(ServerRequestResponseBase); +}; + +/////////////////////////////////////////////////////////////////////////////// +// A container class that encapsulates all the HTTP server request information. +/////////////////////////////////////////////////////////////////////////////// +class ServerRequest : public ServerRequestResponseBase { + public: + ServerRequest(const std::string& url, const std::string& method); + + // Get the actual request URL. Does not include the query string or fragment. + const std::string& GetURL() const { return url_; } + // Get the request method. + const std::string& GetMethod() const { return method_; } + // Get the POST/GET request parameters. These are parsed query string + // parameters from the URL. In addition, for POST requests with + // application/x-www-form-urlencoded content type, the request body is also + // parsed and individual fields can be accessed through this method. + std::string GetFormField(const std::string& field_name) const; + + private: + // Request URL (without query string or URL fragment). + std::string url_; + // Request method + std::string method_; + // List of available request data form fields. + mutable std::map<std::string, std::string> form_fields_; + // Flag used on first request to GetFormField to parse the body of HTTP POST + // request with application/x-www-form-urlencoded content. + mutable bool form_fields_parsed_ = false; + + DISALLOW_COPY_AND_ASSIGN(ServerRequest); +}; + +/////////////////////////////////////////////////////////////////////////////// +// A container class that encapsulates all the HTTP server response information. +// The request handler will use this class to provide a response to the caller. +// Call the Reply() or the appropriate ReplyNNN() specialization to provide +// the response data. Additional calls to AddHeaders() can be made to provide +// custom response headers. The Reply-methods will already provide the +// following response headers: +// Content-Length +// Content-Type +/////////////////////////////////////////////////////////////////////////////// +class ServerResponse : public ServerRequestResponseBase { + public: + ServerResponse() = default; + + // Generic reply method. + void Reply(int status_code, const void* data, size_t data_size, + const char* mime_type); + // Reply with text body. + void ReplyText(int status_code, const std::string& text, + const char* mime_type); + // Reply with JSON object. The content type will be "application/json". + void ReplyJson(int status_code, const base::Value* json); + // Special form for JSON response for simple objects that have a flat + // list of key-value pairs of string type. + void ReplyJson(int status_code, const FormFieldList& fields); + + // Specialized overload to send the binary data as an array of simple + // data elements. Only trivial data types (scalars, POD structures, etc) + // can be used. + template<typename T> + void Reply(int status_code, const std::vector<T>& data, + const char* mime_type) { + // Make sure T doesn't have virtual functions, custom constructors, etc. + static_assert(std::is_trivial<T>::value, "Only simple data is supported"); + Reply(status_code, data.data(), data.size() * sizeof(T), mime_type); + } + + // Specialized overload to send the binary data. + // Only trivial data types (scalars, POD structures, etc) can be used. + template<typename T> + void Reply(int status_code, const T& data, const char* mime_type) { + // Make sure T doesn't have virtual functions, custom constructors, etc. + static_assert(std::is_trivial<T>::value, "Only simple data is supported"); + Reply(status_code, &data, sizeof(T), mime_type); + } + + // For handlers that want to simulate versions of HTTP protocol other + // than HTTP/1.1, call this method with the custom version string, + // for example "HTTP/1.0". + void SetProtocolVersion(const std::string& protocol_version) { + protocol_version_ = protocol_version; + } + + protected: + // These methods are helpers to implement corresponding functionality + // of fake::Connection. + friend class Connection; + // Helper for fake::Connection::GetResponseStatusCode(). + int GetStatusCode() const { return status_code_; } + // Helper for fake::Connection::GetResponseStatusText(). + std::string GetStatusText() const; + // Helper for fake::Connection::GetProtocolVersion(). + std::string GetProtocolVersion() const { return protocol_version_; } + + private: + int status_code_ = 0; + std::string protocol_version_ = "HTTP/1.1"; + + DISALLOW_COPY_AND_ASSIGN(ServerResponse); +}; + +} // namespace fake +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_TRANSPORT_FAKE_H_
diff --git a/buffet/http_utils.cc b/buffet/http_utils.cc new file mode 100644 index 0000000..f7f1190 --- /dev/null +++ b/buffet/http_utils.cc
@@ -0,0 +1,167 @@ +// 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/http_utils.h" + +#include <algorithm> + +#include <base/json/json_reader.h> +#include <base/json/json_writer.h> +#include <base/values.h> + +#include "buffet/mime_utils.h" +#include "buffet/data_encoding.h" + +namespace buffet { +namespace http { + +const char kErrorDomainJSON[] = "json_parser"; + +std::unique_ptr<Response> Get(const std::string& url, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + return SendRequest(request_type::kGet, url, nullptr, 0, nullptr, + headers, transport, error); +} + +std::string GetAsString(const std::string& url, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + auto resp = Get(url, headers, transport, error); + return resp ? resp->GetDataAsString() : std::string(); +} + +std::unique_ptr<Response> Head(const std::string& url, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + Request request(url, request_type::kHead, transport); + return request.GetResponse(error); +} + +std::unique_ptr<Response> PostText(const std::string& url, + const char* data, + const char* mime_type, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + if (mime_type == nullptr) { + mime_type = mime::application::kWwwFormUrlEncoded; + } + + return PostBinary(url, data, strlen(data), mime_type, headers, transport, + error); +} + +std::unique_ptr<Response> SendRequest(const char * method, + const std::string& url, + const void* data, + size_t data_size, + const char* mime_type, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + Request request(url, method, transport); + request.AddHeaders(headers); + if (data_size > 0) { + if (mime_type == nullptr) { + mime_type = mime::application::kOctet_stream; + } + request.SetContentType(mime_type); + if (!request.AddRequestBody(data, data_size, error)) + return std::unique_ptr<Response>(); + } + return request.GetResponse(error); +} + +std::unique_ptr<Response> PostBinary(const std::string & url, const void* data, + size_t data_size, const char* mime_type, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + return SendRequest(request_type::kPost, url, + data, data_size, mime_type, headers, transport, error); +} + +std::unique_ptr<Response> PostFormData(const std::string& url, + const FormFieldList& data, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + std::string encoded_data = data_encoding::WebParamsEncode(data); + return PostBinary(url, encoded_data.c_str(), encoded_data.size(), + mime::application::kWwwFormUrlEncoded, + headers, transport, error); +} + + +std::unique_ptr<Response> PostJson(const std::string& url, + const base::Value* json, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + std::string data; + if (json) + base::JSONWriter::Write(json, &data); + std::string mime_type = mime::AppendParameter(mime::application::kJson, + mime::parameters::kCharset, + "utf-8"); + return PostBinary(url, data.c_str(), data.size(), + mime_type.c_str(), headers, transport, error); +} + +std::unique_ptr<Response> PatchJson(const std::string& url, + const base::Value* json, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + std::string data; + if (json) + base::JSONWriter::Write(json, &data); + std::string mime_type = mime::AppendParameter(mime::application::kJson, + mime::parameters::kCharset, + "utf-8"); + return SendRequest(request_type::kPatch, url, data.c_str(), data.size(), + mime_type.c_str(), headers, transport, error); +} + +std::unique_ptr<base::DictionaryValue> ParseJsonResponse( + const Response* response, int* status_code, ErrorPtr* error) { + if (!response) + return std::unique_ptr<base::DictionaryValue>(); + + if (status_code) + *status_code = response->GetStatusCode(); + + // Make sure we have a correct content type. Do not try to parse + // binary files, or HTML output. Limit to application/json and text/plain. + auto content_type = mime::RemoveParameters(response->GetContentType()); + if (content_type != mime::application::kJson && + content_type != mime::text::kPlain) { + Error::AddTo(error, kErrorDomainJSON, "non_json_content_type", + "Unexpected response content type: " + content_type); + return std::unique_ptr<base::DictionaryValue>(); + } + + std::string json = response->GetDataAsString(); + std::string error_message; + base::Value* value = base::JSONReader::ReadAndReturnError( + json, base::JSON_PARSE_RFC, nullptr, &error_message); + if (!value) { + Error::AddTo(error, kErrorDomainJSON, "json_parse_error", error_message); + return std::unique_ptr<base::DictionaryValue>(); + } + base::DictionaryValue* dict_value = nullptr; + if (!value->GetAsDictionary(&dict_value)) { + delete value; + Error::AddTo(error, kErrorDomainJSON, "json_object_error", + "Response is not a valid JSON object"); + return std::unique_ptr<base::DictionaryValue>(); + } + return std::unique_ptr<base::DictionaryValue>(dict_value); +} + +} // namespace http +} // namespace buffet
diff --git a/buffet/http_utils.h b/buffet/http_utils.h new file mode 100644 index 0000000..20e255b --- /dev/null +++ b/buffet/http_utils.h
@@ -0,0 +1,181 @@ +// 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_HTTP_UTILS_H_ +#define BUFFET_HTTP_UTILS_H_ + +#include "buffet/error.h" +#include "buffet/http_request.h" + +#include <string> +#include <utility> +#include <vector> + +namespace base { + class Value; + class DictionaryValue; +} // namespace base + +namespace buffet { +namespace http { + +extern const char kErrorDomainJSON[]; + +typedef std::vector<std::pair<std::string, std::string>> FormFieldList; + +//////////////////////////////////////////////////////////////////////////////// +// The following are simple utility helper functions for common HTTP operations +// that use http::Request object behind the scenes and set it up accordingly. +// +// For more advanced functionality you need to use Request/Response objects +// directly. +//////////////////////////////////////////////////////////////////////////////// + +// Performs a generic HTTP request with binary data. Success status, +// returned data and additional information (such as returned HTTP headers) +// can be obtained from the returned Response object. +// If data MIME type is not specified, "application/octet-stream" is assumed. +std::unique_ptr<Response> SendRequest( + const char* method, const std::string& url, + const void* data, size_t data_size, const char* mime_type, + const HeaderList& headers, std::shared_ptr<Transport> transport, + ErrorPtr* error); + +// Performs a simple GET request and returns the data as a string. +std::string GetAsString(const std::string& url, const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error); +inline std::string GetAsString(const std::string& url, + std::shared_ptr<Transport> transport, + ErrorPtr* error) { + return GetAsString(url, HeaderList(), transport, error); +} + +// Performs a GET request. Success status, returned data and additional +// information (such as returned HTTP headers) can be obtained from +// the returned Response object. +std::unique_ptr<Response> Get(const std::string& url, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error); +inline std::unique_ptr<Response> Get( + const std::string& url, std::shared_ptr<Transport> transport, + ErrorPtr* error) { + return Get(url, HeaderList(), transport, error); +} + +// Performs a HEAD request. Success status and additional +// information (such as returned HTTP headers) can be obtained from +// the returned Response object. +std::unique_ptr<Response> Head(const std::string& url, + std::shared_ptr<Transport> transport, + ErrorPtr* error); + +// Performs a POST request with binary data. Success status, returned data +// and additional information (such as returned HTTP headers) can be obtained +// from the returned Response object. +// If data MIME type is not specified, "application/octet-stream" is assumed +std::unique_ptr<Response> PostBinary(const std::string& url, + const void* data, + size_t data_size, + const char* mime_type, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error); + +inline std::unique_ptr<Response> PostBinary( + const std::string& url, const void* data, size_t data_size, + const char* mime_type, std::shared_ptr<Transport> transport, + ErrorPtr* error) { + return PostBinary(url, data, data_size, mime_type, HeaderList(), transport, + error); +} + +inline std::unique_ptr<Response> PostBinary( + const std::string& url, const void* data, size_t data_size, + std::shared_ptr<Transport> transport, ErrorPtr* error) { + return PostBinary(url, data, data_size, nullptr, transport, error); +} + +// Performs a POST request with text data. Success status, returned data +// and additional information (such as returned HTTP headers) can be obtained +// from the returned Response object. +// If data MIME type is not specified, "application/x-www-form-urlencoded" +// is assumed. +std::unique_ptr<Response> PostText(const std::string& url, + const char* data, + const char* mime_type, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error); + +inline std::unique_ptr<Response> PostText( + const std::string& url, const char* data, const char* mime_type, + std::shared_ptr<Transport> transport, ErrorPtr* error) { + return PostText(url, data, mime_type, HeaderList(), transport, error); +} + +inline std::unique_ptr<Response> PostText( + const std::string& url, const char* data, + std::shared_ptr<Transport> transport, ErrorPtr* error) { + return PostText(url, data, nullptr, transport, error); +} + +// Performs a POST request with form data. Success status, returned data +// and additional information (such as returned HTTP headers) can be obtained +// from the returned Response object. The form data is a list of key/value +// pairs. The data is posed as "application/x-www-form-urlencoded". +std::unique_ptr<Response> PostFormData( + const std::string& url, const FormFieldList& data, + const HeaderList& headers, std::shared_ptr<Transport> transport, + ErrorPtr* error); + +inline std::unique_ptr<Response> PostFormData( + const std::string& url, const FormFieldList& data, + std::shared_ptr<Transport> transport, ErrorPtr* error) { + return PostFormData(url, data, HeaderList(), transport, error); +} + +// Performs a POST request with JSON data. Success status, returned data +// and additional information (such as returned HTTP headers) can be obtained +// from the returned Response object. If a JSON response is expected, +// use ParseJsonResponse() method on the returned Response object. +std::unique_ptr<Response> PostJson(const std::string& url, + const base::Value* json, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error); + +inline std::unique_ptr<Response> PostJson( + const std::string& url, const base::Value* json, + std::shared_ptr<Transport> transport, ErrorPtr* error) { + return PostJson(url, json, HeaderList(), transport, error); +} + +// Performs a PATCH request with JSON data. Success status, returned data +// and additional information (such as returned HTTP headers) can be obtained +// from the returned Response object. If a JSON response is expected, +// use ParseJsonResponse() method on the returned Response object. +std::unique_ptr<Response> PatchJson(const std::string& url, + const base::Value* json, + const HeaderList& headers, + std::shared_ptr<Transport> transport, + ErrorPtr* error); + +inline std::unique_ptr<Response> PatchJson( + const std::string& url, const base::Value* json, + std::shared_ptr<Transport> transport, ErrorPtr* error) { + return PatchJson(url, json, HeaderList(), transport, error); +} + +// Given an http::Response object, parse the body data into Json object. +// Returns null if failed. Optional |error| can be passed in to +// get the extended error information as to why the parse failed. +std::unique_ptr<base::DictionaryValue> ParseJsonResponse( + const Response* response, int* status_code, ErrorPtr* error); + +} // namespace http +} // namespace buffet + +#endif // BUFFET_HTTP_UTILS_H_
diff --git a/buffet/http_utils_unittest.cc b/buffet/http_utils_unittest.cc new file mode 100644 index 0000000..fc86d31 --- /dev/null +++ b/buffet/http_utils_unittest.cc
@@ -0,0 +1,338 @@ +// 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 <string> +#include <vector> + +#include <base/values.h> +#include <gtest/gtest.h> + +#include "buffet/bind_lambda.h" +#include "buffet/http_utils.h" +#include "buffet/http_transport_fake.h" +#include "buffet/mime_utils.h" +#include "buffet/string_utils.h" +#include "buffet/url_utils.h" + +using namespace buffet; // NOLINT(build/namespaces) +using namespace buffet::http; // NOLINT(build/namespaces) + +static const char kFakeUrl[] = "http://localhost"; +static const char kEchoUrl[] = "http://localhost/echo"; +static const char kMethodEchoUrl[] = "http://localhost/echo/method"; + +///////////////////// Generic helper request handlers ///////////////////////// +// Returns the request data back with the same content type. +static void EchoDataHandler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + response->Reply(status_code::Ok, request.GetData(), + request.GetHeader(request_header::kContentType).c_str()); +}; + +// Returns the request method as a plain text response. +static void EchoMethodHandler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + response->ReplyText(status_code::Ok, request.GetMethod(), mime::text::kPlain); +}; + +/////////////////////////////////////////////////////////////////////////////// +TEST(HttpUtils, SendRequest_BinaryData) { + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kEchoUrl, request_type::kPost, + base::Bind(EchoDataHandler)); + + // Test binary data round-tripping. + std::vector<unsigned char> custom_data({0xFF, 0x00, 0x80, 0x40, 0xC0, 0x7F}); + auto response = http::SendRequest(request_type::kPost, kEchoUrl, + custom_data.data(), custom_data.size(), + mime::application::kOctet_stream, + HeaderList(), transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::application::kOctet_stream, response->GetContentType()); + EXPECT_EQ(custom_data.size(), response->GetData().size()); + EXPECT_EQ(custom_data, response->GetData()); +} + +TEST(HttpUtils, SendRequest_Post) { + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); + + // Test binary data round-tripping. + std::vector<unsigned char> custom_data({0xFF, 0x00, 0x80, 0x40, 0xC0, 0x7F}); + + // Check the correct HTTP method used. + auto response = http::SendRequest(request_type::kPost, kMethodEchoUrl, + custom_data.data(), custom_data.size(), + mime::application::kOctet_stream, + HeaderList(), transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::text::kPlain, response->GetContentType()); + EXPECT_EQ(request_type::kPost, response->GetDataAsString()); +} + +TEST(HttpUtils, SendRequest_Get) { + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); + + auto response = http::SendRequest(request_type::kGet, kMethodEchoUrl, + nullptr, 0, nullptr, + HeaderList(), transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::text::kPlain, response->GetContentType()); + EXPECT_EQ(request_type::kGet, response->GetDataAsString()); +} + +TEST(HttpUtils, SendRequest_Put) { + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); + + auto response = http::SendRequest(request_type::kPut, kMethodEchoUrl, + nullptr, 0, nullptr, + HeaderList(), transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::text::kPlain, response->GetContentType()); + EXPECT_EQ(request_type::kPut, response->GetDataAsString()); +} + +TEST(HttpUtils, SendRequest_NotFound) { + std::shared_ptr<fake::Transport> transport(new fake::Transport); + // Test failed response (URL not found). + auto response = http::SendRequest(request_type::kGet, "http://blah.com", + nullptr, 0, nullptr, + HeaderList(), transport, nullptr); + EXPECT_FALSE(response->IsSuccessful()); + EXPECT_EQ(status_code::NotFound, response->GetStatusCode()); +} + +TEST(HttpUtils, SendRequest_Headers) { + std::shared_ptr<fake::Transport> transport(new fake::Transport); + + static const char json_echo_url[] = "http://localhost/echo/json"; + auto JsonEchoHandler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + base::DictionaryValue json; + json.SetString("method", request.GetMethod()); + json.SetString("data", request.GetDataAsString()); + for (auto&& pair : request.GetHeaders()) { + json.SetString("header." + pair.first, pair.second); + } + response->ReplyJson(status_code::Ok, &json); + }; + transport->AddHandler(json_echo_url, "*", + base::Bind(JsonEchoHandler)); + auto response = http::SendRequest( + request_type::kPost, json_echo_url, "abcd", 4, + mime::application::kOctet_stream, { + {request_header::kCookie, "flavor=vanilla"}, + {request_header::kIfMatch, "*"}, + }, transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::application::kJson, + mime::RemoveParameters(response->GetContentType())); + auto json = ParseJsonResponse(response.get(), nullptr, nullptr); + std::string value; + EXPECT_TRUE(json->GetString("method", &value)); + EXPECT_EQ(request_type::kPost, value); + EXPECT_TRUE(json->GetString("data", &value)); + EXPECT_EQ("abcd", value); + EXPECT_TRUE(json->GetString("header.Cookie", &value)); + EXPECT_EQ("flavor=vanilla", value); + EXPECT_TRUE(json->GetString("header.Content-Type", &value)); + EXPECT_EQ(mime::application::kOctet_stream, value); + EXPECT_TRUE(json->GetString("header.Content-Length", &value)); + EXPECT_EQ("4", value); + EXPECT_TRUE(json->GetString("header.If-Match", &value)); + EXPECT_EQ("*", value); +} + +TEST(HttpUtils, Get) { + // Sends back the "?test=..." portion of URL. + // So if we do GET "http://localhost?test=blah", this handler responds + // with "blah" as text/plain. + auto GetHandler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(request_type::kGet, request.GetMethod()); + EXPECT_EQ("0", request.GetHeader(request_header::kContentLength)); + EXPECT_EQ("", request.GetHeader(request_header::kContentType)); + response->ReplyText(status_code::Ok, request.GetFormField("test"), + mime::text::kPlain); + }; + + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kFakeUrl, request_type::kGet, base::Bind(GetHandler)); + transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); + + // Make sure Get/GetAsString actually do the GET request + auto response = http::Get(kMethodEchoUrl, transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::text::kPlain, response->GetContentType()); + EXPECT_EQ(request_type::kGet, response->GetDataAsString()); + EXPECT_EQ(request_type::kGet, + http::GetAsString(kMethodEchoUrl, transport, nullptr)); + + for (std::string data : {"blah", "some data", ""}) { + std::string url = url::AppendQueryParam(kFakeUrl, "test", data); + EXPECT_EQ(data, http::GetAsString(url, transport, nullptr)); + } +} + +TEST(HttpUtils, Head) { + auto HeadHandler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(request_type::kHead, request.GetMethod()); + EXPECT_EQ("0", request.GetHeader(request_header::kContentLength)); + EXPECT_EQ("", request.GetHeader(request_header::kContentType)); + response->ReplyText(status_code::Ok, "blah", + mime::text::kPlain); + }; + + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kFakeUrl, request_type::kHead, base::Bind(HeadHandler)); + + auto response = http::Head(kFakeUrl, transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::text::kPlain, response->GetContentType()); + EXPECT_EQ("", response->GetDataAsString()); // Must not have actual body. + EXPECT_EQ("4", response->GetHeader(request_header::kContentLength)); +} + +TEST(HttpUtils, PostBinary) { + auto Handler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(request_type::kPost, request.GetMethod()); + EXPECT_EQ("256", request.GetHeader(request_header::kContentLength)); + EXPECT_EQ(mime::application::kOctet_stream, + request.GetHeader(request_header::kContentType)); + auto&& data = request.GetData(); + EXPECT_EQ(256, data.size()); + + // Sum up all the bytes. + int sum = std::accumulate(data.begin(), data.end(), 0); + EXPECT_EQ(32640, sum); // sum(i, i => [0, 255]) = 32640. + response->ReplyText(status_code::Ok, "", mime::text::kPlain); + }; + + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(Handler)); + + /// Fill the data buffer with bytes from 0x00 to 0xFF. + std::vector<unsigned char> data(256); + unsigned char counter = 0xFF; + std::generate(data.begin(), data.end(), [&counter]() { return ++counter; }); + + auto response = http::PostBinary(kFakeUrl, data.data(), data.size(), + transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); +} + +TEST(HttpUtils, PostText) { + std::string fake_data = "Some data"; + auto PostHandler = [fake_data](const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(request_type::kPost, request.GetMethod()); + EXPECT_EQ(fake_data.size(), + std::stoul(request.GetHeader(request_header::kContentLength))); + EXPECT_EQ(mime::text::kPlain, + request.GetHeader(request_header::kContentType)); + response->ReplyText(status_code::Ok, request.GetDataAsString(), + mime::text::kPlain); + }; + + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(PostHandler)); + + auto response = http::PostText(kFakeUrl, fake_data.c_str(), + mime::text::kPlain, transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::text::kPlain, response->GetContentType()); + EXPECT_EQ(fake_data, response->GetDataAsString()); +} + +TEST(HttpUtils, PostFormData) { + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kFakeUrl, request_type::kPost, + base::Bind(EchoDataHandler)); + + auto response = http::PostFormData(kFakeUrl, { + {"key", "value"}, + {"field", "field value"}, + }, transport, nullptr); + EXPECT_TRUE(response->IsSuccessful()); + EXPECT_EQ(mime::application::kWwwFormUrlEncoded, response->GetContentType()); + EXPECT_EQ("key=value&field=field+value", response->GetDataAsString()); +} + +TEST(HttpUtils, PostPatchJson) { + auto JsonHandler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + auto mime_type = mime::RemoveParameters( + request.GetHeader(request_header::kContentType)); + EXPECT_EQ(mime::application::kJson, mime_type); + response->ReplyJson(status_code::Ok, { + {"method", request.GetMethod()}, + {"data", request.GetDataAsString()}, + }); + }; + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kFakeUrl, "*", base::Bind(JsonHandler)); + + base::DictionaryValue json; + json.SetString("key1", "val1"); + json.SetString("key2", "val2"); + std::string value; + + // Test POST + auto response = http::PostJson(kFakeUrl, &json, transport, nullptr); + auto resp_json = http::ParseJsonResponse(response.get(), nullptr, nullptr); + EXPECT_NE(nullptr, resp_json.get()); + EXPECT_TRUE(resp_json->GetString("method", &value)); + EXPECT_EQ(request_type::kPost, value); + EXPECT_TRUE(resp_json->GetString("data", &value)); + EXPECT_EQ("{\"key1\":\"val1\",\"key2\":\"val2\"}", value); + + // Test PATCH + response = http::PatchJson(kFakeUrl, &json, transport, nullptr); + resp_json = http::ParseJsonResponse(response.get(), nullptr, nullptr); + EXPECT_NE(nullptr, resp_json.get()); + EXPECT_TRUE(resp_json->GetString("method", &value)); + EXPECT_EQ(request_type::kPatch, value); + EXPECT_TRUE(resp_json->GetString("data", &value)); + EXPECT_EQ("{\"key1\":\"val1\",\"key2\":\"val2\"}", value); +} + +TEST(HttpUtils, ParseJsonResponse) { + auto JsonHandler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + int status_code = std::stoi(request.GetFormField("code")); + response->ReplyJson(status_code, {{"data", request.GetFormField("value")}}); + }; + std::shared_ptr<fake::Transport> transport(new fake::Transport); + transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(JsonHandler)); + + // Test valid JSON responses (with success or error codes). + for (auto&& item : {"200;data", "400;wrong", "500;Internal Server error"}) { + auto pair = string_utils::SplitAtFirst(item, ';'); + auto response = http::PostFormData(kFakeUrl, { + {"code", pair.first}, + {"value", pair.second}, + }, transport, nullptr); + int code = 0; + auto json = http::ParseJsonResponse(response.get(), &code, nullptr); + EXPECT_NE(nullptr, json.get()); + std::string value; + EXPECT_TRUE(json->GetString("data", &value)); + EXPECT_EQ(pair.first, std::to_string(code)); + EXPECT_EQ(pair.second, value); + } + + // Test invalid (non-JSON) response. + auto response = http::Get("http://bad.url", transport, nullptr); + EXPECT_EQ(status_code::NotFound, response->GetStatusCode()); + EXPECT_EQ(mime::text::kHtml, response->GetContentType()); + int code = 0; + auto json = http::ParseJsonResponse(response.get(), &code, nullptr); + EXPECT_EQ(nullptr, json.get()); + EXPECT_EQ(status_code::NotFound, code); +} +
diff --git a/buffet/main.cc b/buffet/main.cc new file mode 100644 index 0000000..43ba4ab --- /dev/null +++ b/buffet/main.cc
@@ -0,0 +1,141 @@ +// 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 <string> + +#include <base/at_exit.h> +#include <base/command_line.h> +#include <base/file_util.h> +#include <base/files/file_path.h> +#include <base/logging.h> +#include <base/message_loop/message_loop.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> +#include <dbus/bus.h> +#include <sysexits.h> + +#include "buffet/async_event_sequencer.h" +#include "buffet/dbus_constants.h" +#include "buffet/exported_object_manager.h" +#include "buffet/manager.h" + +using buffet::dbus_utils::AsyncEventSequencer; +using buffet::dbus_utils::ExportedObjectManager; + +namespace { + +static const char kLogRoot[] = "logroot"; +static const char kHelp[] = "help"; +static const char kDefaultLogRoot[] = "/var/log"; + +// The help message shown if help flag is passed to the program. +static const char kHelpMessage[] = "\n" + "Available Switches: \n" + " --logroot=/path/to/logroot\n" + " Specifies parent directory to put buffet logs in.\n"; + +// Returns |utime| as a string +std::string GetTimeAsString(time_t utime) { + struct tm tm; + CHECK_EQ(localtime_r(&utime, &tm), &tm); + char str[16]; + CHECK_EQ(strftime(str, sizeof(str), "%Y%m%d-%H%M%S", &tm), 15UL); + return std::string(str); +} + +// Sets up a symlink to point to log file. +void SetupLogSymlink(const std::string& symlink_path, + const std::string& log_path) { + base::DeleteFile(base::FilePath(symlink_path), true); + if (symlink(log_path.c_str(), symlink_path.c_str()) == -1) { + LOG(ERROR) << "Unable to create symlink " << symlink_path + << " pointing at " << log_path; + } +} + +// Creates new log file based on timestamp in |log_root|/buffet. +std::string SetupLogFile(const std::string& log_root) { + const auto log_symlink = log_root + "/buffet.log"; + const auto logs_dir = log_root + "/buffet"; + const auto log_path = + base::StringPrintf("%s/buffet.%s", + logs_dir.c_str(), + GetTimeAsString(::time(NULL)).c_str()); + mkdir(logs_dir.c_str(), 0755); + SetupLogSymlink(log_symlink, log_path); + return log_symlink; +} + +// Sets up logging for buffet. +void SetupLogging(const std::string& log_root) { + const auto log_file = SetupLogFile(log_root); + logging::LoggingSettings settings; + settings.logging_dest = logging::LOG_TO_ALL; + settings.log_file = log_file.c_str(); + settings.lock_log = logging::DONT_LOCK_LOG_FILE; + settings.delete_old = logging::APPEND_TO_OLD_LOG_FILE; + logging::InitLogging(settings); +} + +void TakeServiceOwnership(scoped_refptr<dbus::Bus> bus, bool success) { + // Success should always be true since we've said that failures are + // fatal. + CHECK(success) << "Init of one or more objects has failed."; + CHECK(bus->RequestOwnershipAndBlock(buffet::dbus_constants::kServiceName, + dbus::Bus::REQUIRE_PRIMARY)) + << "Unable to take ownership of " << buffet::dbus_constants::kServiceName; +} + +void EnterMainLoop(base::MessageLoopForIO* message_loop, + scoped_refptr<dbus::Bus> bus) { + scoped_refptr<AsyncEventSequencer> sequencer(new AsyncEventSequencer()); + ExportedObjectManager object_manager( + bus, dbus::ObjectPath(buffet::dbus_constants::kRootServicePath)); + buffet::Manager manager(bus, object_manager.AsWeakPtr()); + object_manager.Init( + sequencer->GetHandler("ObjectManager.Init() failed.", true)); + manager.Init(sequencer->GetHandler("Manager.Init() failed.", true)); + sequencer->OnAllTasksCompletedCall( + {base::Bind(&TakeServiceOwnership, bus)}); + // Release our handle on the sequencer so that it gets deleted after + // both callbacks return. + sequencer = nullptr; + LOG(INFO) << "Entering mainloop."; + message_loop->Run(); +} + +} // namespace + +int main(int argc, char* argv[]) { + // Parse the args and check for extra args. + CommandLine::Init(argc, argv); + CommandLine* cl = CommandLine::ForCurrentProcess(); + + if (cl->HasSwitch(kHelp)) { + LOG(INFO) << kHelpMessage; + return EX_USAGE; + } + + std::string log_root = std::string(kDefaultLogRoot); + if (cl->HasSwitch(kLogRoot)) { + log_root = cl->GetSwitchValueASCII(kLogRoot); + } + + SetupLogging(log_root); + + base::AtExitManager at_exit_manager; + base::MessageLoopForIO message_loop; + + dbus::Bus::Options options; + // TODO(sosa): Should this be on the system bus? + options.bus_type = dbus::Bus::SYSTEM; + scoped_refptr<dbus::Bus> bus(new dbus::Bus(options)); + CHECK(bus->Connect()); + // Our top level objects expect the bus to exist in a connected state for + // the duration of their lifetimes. + EnterMainLoop(&message_loop, bus); + bus->ShutdownAndBlock(); + + return EX_OK; +}
diff --git a/buffet/manager.cc b/buffet/manager.cc new file mode 100644 index 0000000..3639803 --- /dev/null +++ b/buffet/manager.cc
@@ -0,0 +1,301 @@ +// 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/manager.h" + +#include <map> +#include <string> + +#include <base/bind.h> +#include <base/bind_helpers.h> +#include <base/json/json_writer.h> +#include <dbus/bus.h> +#include <dbus/object_path.h> +#include <dbus/values_util.h> + +#include "buffet/async_event_sequencer.h" +#include "buffet/dbus_constants.h" +#include "buffet/dbus_utils.h" +#include "buffet/error.h" +#include "buffet/exported_object_manager.h" + +using buffet::dbus_utils::AsyncEventSequencer; +using buffet::dbus_utils::GetBadArgsError; +using buffet::dbus_utils::GetDBusError; + +namespace buffet { + +Manager::Manager( + scoped_refptr<dbus::Bus> bus, + base::WeakPtr<dbus_utils::ExportedObjectManager> object_manager) + : bus_(bus), + exported_object_(bus->GetExportedObject( + dbus::ObjectPath(dbus_constants::kManagerServicePath))), + object_manager_(object_manager) { } + +Manager::~Manager() { + object_manager_->ReleaseInterface( + dbus::ObjectPath(dbus_constants::kManagerServicePath), + dbus_constants::kManagerInterface); + // Prevent the properties object from making calls to the exported object. + properties_.reset(nullptr); + // Unregister ourselves from the Bus. This prevents the bus from calling + // our callbacks in between the Manager's death and the bus unregistering + // our exported object on shutdown. Unretained makes no promises of memory + // management. + exported_object_->Unregister(); + exported_object_ = nullptr; +} + +void Manager::Init(const OnInitFinish& cb) { + scoped_refptr<AsyncEventSequencer> sequencer( + new AsyncEventSequencer()); + exported_object_->ExportMethod( + dbus_constants::kManagerInterface, + dbus_constants::kManagerCheckDeviceRegistered, + dbus_utils::GetExportableDBusMethod( + base::Bind(&Manager::HandleCheckDeviceRegistered, + base::Unretained(this))), + sequencer->GetExportHandler( + dbus_constants::kManagerInterface, + dbus_constants::kManagerCheckDeviceRegistered, + "Failed exporting CheckDeviceRegistered method", + true)); + exported_object_->ExportMethod( + dbus_constants::kManagerInterface, + dbus_constants::kManagerGetDeviceInfo, + dbus_utils::GetExportableDBusMethod( + base::Bind(&Manager::HandleGetDeviceInfo, + base::Unretained(this))), + sequencer->GetExportHandler( + dbus_constants::kManagerInterface, + dbus_constants::kManagerGetDeviceInfo, + "Failed exporting GetDeviceInfo method", + true)); + exported_object_->ExportMethod( + dbus_constants::kManagerInterface, + dbus_constants::kManagerStartRegisterDevice, + dbus_utils::GetExportableDBusMethod( + base::Bind(&Manager::HandleStartRegisterDevice, + base::Unretained(this))), + sequencer->GetExportHandler( + dbus_constants::kManagerInterface, + dbus_constants::kManagerStartRegisterDevice, + "Failed exporting StartRegisterDevice method", + true)); + exported_object_->ExportMethod( + dbus_constants::kManagerInterface, + dbus_constants::kManagerFinishRegisterDevice, + dbus_utils::GetExportableDBusMethod( + base::Bind(&Manager::HandleFinishRegisterDevice, + base::Unretained(this))), + sequencer->GetExportHandler( + dbus_constants::kManagerInterface, + dbus_constants::kManagerFinishRegisterDevice, + "Failed exporting FinishRegisterDevice method", + true)); + exported_object_->ExportMethod( + dbus_constants::kManagerInterface, + dbus_constants::kManagerUpdateStateMethod, + dbus_utils::GetExportableDBusMethod( + base::Bind(&Manager::HandleUpdateState, + base::Unretained(this))), + sequencer->GetExportHandler( + dbus_constants::kManagerInterface, + dbus_constants::kManagerUpdateStateMethod, + "Failed exporting UpdateState method", + true)); + exported_object_->ExportMethod( + dbus_constants::kManagerInterface, dbus_constants::kManagerTestMethod, + dbus_utils::GetExportableDBusMethod( + base::Bind(&Manager::HandleTestMethod, base::Unretained(this))), + sequencer->GetExportHandler( + dbus_constants::kManagerInterface, dbus_constants::kManagerTestMethod, + "Failed exporting TestMethod method", + true)); + properties_.reset(new Properties(bus_)); + // TODO(wiley): Initialize all properties appropriately before claiming + // the properties interface. + properties_->state_.SetValue("{}"); + properties_->Init( + sequencer->GetHandler("Manager properties export failed.", true)); + auto claim_interface_task = sequencer->WrapCompletionTask( + base::Bind(&dbus_utils::ExportedObjectManager::ClaimInterface, + object_manager_->AsWeakPtr(), + dbus::ObjectPath(dbus_constants::kManagerServicePath), + dbus_constants::kManagerInterface, + properties_->GetPropertyWriter( + dbus_constants::kManagerInterface))); + sequencer->OnAllTasksCompletedCall({claim_interface_task, cb}); + device_info_.Load(); +} + +scoped_ptr<dbus::Response> Manager::HandleCheckDeviceRegistered( + dbus::MethodCall* method_call) { + // Read the parameters to the method. + dbus::MessageReader reader(method_call); + if (reader.HasMoreData()) { + return GetBadArgsError(method_call, + "Too many parameters to CheckDeviceRegistered"); + } + + LOG(INFO) << "Received call to Manager.CheckDeviceRegistered()"; + + buffet::ErrorPtr error; + bool registered = device_info_.CheckRegistration(&error); + // If it fails due to any reason other than 'device not registered', + // treat it as a real error and report it to the caller. + if (!registered && + !error->HasError(kErrorDomainGCD, "device_not_registered")) { + return GetDBusError(method_call, error.get()); + } + + std::string device_id; + if (registered) { + device_id = device_info_.GetDeviceId(&error); + if (device_id.empty()) + return GetDBusError(method_call, error.get()); + } + // Send back our response. + scoped_ptr<dbus::Response> response( + dbus::Response::FromMethodCall(method_call)); + dbus::MessageWriter writer(response.get()); + writer.AppendString(device_id); + return response.Pass(); +} + +scoped_ptr<dbus::Response> Manager::HandleGetDeviceInfo( + dbus::MethodCall* method_call) { + // Read the parameters to the method. + dbus::MessageReader reader(method_call); + if (reader.HasMoreData()) { + return GetBadArgsError(method_call, + "Too many parameters to GetDeviceInfo"); + } + + LOG(INFO) << "Received call to Manager.GetDeviceInfo()"; + + std::string device_info_str; + buffet::ErrorPtr error; + auto device_info = device_info_.GetDeviceInfo(&error); + if (!device_info) + return GetDBusError(method_call, error.get()); + + base::JSONWriter::Write(device_info.get(), &device_info_str); + + // Send back our response. + scoped_ptr<dbus::Response> response( + dbus::Response::FromMethodCall(method_call)); + dbus::MessageWriter writer(response.get()); + writer.AppendString(device_info_str); + return response.Pass(); +} + +scoped_ptr<dbus::Response> Manager::HandleStartRegisterDevice( + dbus::MethodCall* method_call) { + // Read the parameters to the method. + dbus::MessageReader reader(method_call); + if (!reader.HasMoreData()) { + return GetBadArgsError(method_call, "No parameters to StartRegisterDevice"); + } + + dbus::MessageReader array_reader(nullptr); + if (!reader.PopArray(&array_reader)) + return GetBadArgsError(method_call, "Failed to read the parameter array"); + std::map<std::string, std::shared_ptr<base::Value>> params; + while (array_reader.HasMoreData()) { + dbus::MessageReader dict_entry_reader(nullptr); + if (!array_reader.PopDictEntry(&dict_entry_reader)) + return GetBadArgsError(method_call, "Failed to get a call parameter"); + std::string key; + if (!dict_entry_reader.PopString(&key)) + return GetBadArgsError(method_call, "Failed to read parameter key"); + base::Value* value = dbus::PopDataAsValue(&dict_entry_reader); + if (!value) + return GetBadArgsError(method_call, "Failed to read parameter value"); + params.insert(std::make_pair(key, std::shared_ptr<base::Value>(value))); + } + if (reader.HasMoreData()) + return GetBadArgsError(method_call, + "Too many parameters to StartRegisterDevice"); + + LOG(INFO) << "Received call to Manager.StartRegisterDevice()"; + + buffet::ErrorPtr error; + std::string id = device_info_.StartRegistration(params, &error); + if (id.empty()) + return GetDBusError(method_call, error.get()); + + // Send back our response. + scoped_ptr<dbus::Response> response( + dbus::Response::FromMethodCall(method_call)); + dbus::MessageWriter writer(response.get()); + writer.AppendString(id); + return response.Pass(); +} + +scoped_ptr<dbus::Response> Manager::HandleFinishRegisterDevice( + dbus::MethodCall* method_call) { + // Read the parameters to the method. + dbus::MessageReader reader(method_call); + if (!reader.HasMoreData()) { + return GetBadArgsError(method_call, + "No parameters to FinishRegisterDevice"); + } + std::string user_auth_code; + if (!reader.PopString(&user_auth_code)) { + return GetBadArgsError(method_call, "Failed to read UserAuthCode"); + } + if (reader.HasMoreData()) { + return GetBadArgsError(method_call, + "Too many parameters to FinishRegisterDevice"); + } + + LOG(INFO) << "Received call to Manager.FinishRegisterDevice()"; + buffet::ErrorPtr error; + if (!device_info_.FinishRegistration(user_auth_code, &error)) + return GetDBusError(method_call, error.get()); + + std::string device_id = device_info_.GetDeviceId(&error); + if (device_id.empty()) + return GetDBusError(method_call, error.get()); + + // Send back our response. + scoped_ptr<dbus::Response> response( + dbus::Response::FromMethodCall(method_call)); + dbus::MessageWriter writer(response.get()); + writer.AppendString(device_id); + return response.Pass(); +} + +scoped_ptr<dbus::Response> Manager::HandleUpdateState( + dbus::MethodCall *method_call) { + // Read the parameters to the method. + dbus::MessageReader reader(method_call); + if (!reader.HasMoreData()) { + return GetBadArgsError(method_call, "No parameters to UpdateState"); + } + std::string json_state_fragment; + if (!reader.PopString(&json_state_fragment)) { + return GetBadArgsError(method_call, "Failed to read json_state_fragment"); + } + if (reader.HasMoreData()) { + return GetBadArgsError(method_call, "Too many parameters to UpdateState"); + } + + LOG(INFO) << "Received call to Manager.UpdateState()"; + // TODO(wiley): Merge json state blobs intelligently. + properties_->state_.SetValue(json_state_fragment); + + // Send back our response. + return dbus::Response::FromMethodCall(method_call); +} + +scoped_ptr<dbus::Response> Manager::HandleTestMethod( + dbus::MethodCall* method_call) { + LOG(INFO) << "Received call to test method."; + return scoped_ptr<dbus::Response>(); +} + +} // namespace buffet
diff --git a/buffet/manager.h b/buffet/manager.h new file mode 100644 index 0000000..4498c50 --- /dev/null +++ b/buffet/manager.h
@@ -0,0 +1,83 @@ +// 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_MANAGER_H_ +#define BUFFET_MANAGER_H_ + +#include <memory> +#include <string> + +#include <base/basictypes.h> +#include <base/memory/scoped_ptr.h> +#include <base/memory/weak_ptr.h> +#include <base/values.h> +#include <dbus/message.h> +#include <dbus/object_path.h> + +#include "buffet/dbus_constants.h" +#include "buffet/exported_property_set.h" +#include "buffet/device_registration_info.h" + +namespace buffet { + +namespace dbus_utils { +class ExportedObjectManager; +} // namespace dbus_utils + +// The Manager is responsible for global state of Buffet. It exposes +// interfaces which affect the entire device such as device registration and +// device state. +class Manager { + public: + typedef base::Callback<void(bool success)> OnInitFinish; + + Manager(scoped_refptr<dbus::Bus> bus, + base::WeakPtr<dbus_utils::ExportedObjectManager> object_manager); + ~Manager(); + void Init(const OnInitFinish& cb); + + private: + struct Properties: public dbus_utils::ExportedPropertySet { + public: + dbus_utils::ExportedProperty<std::string> state_; + explicit Properties(dbus::Bus* bus) + : dbus_utils::ExportedPropertySet( + bus, dbus::ObjectPath(dbus_constants::kManagerServicePath)) { + RegisterProperty(dbus_constants::kManagerInterface, "State", &state_); + } + virtual ~Properties() {} + }; + + // Handles calls to org.chromium.Buffet.Manager.CheckDeviceRegistered(). + scoped_ptr<dbus::Response> HandleCheckDeviceRegistered( + dbus::MethodCall* method_call); + // Handles calls to org.chromium.Buffet.Manager.GetDeviceInfo(). + scoped_ptr<dbus::Response> HandleGetDeviceInfo( + dbus::MethodCall* method_call); + // Handles calls to org.chromium.Buffet.Manager.StartRegisterDevice(). + scoped_ptr<dbus::Response> HandleStartRegisterDevice( + dbus::MethodCall* method_call); + // Handles calls to org.chromium.Buffet.Manager.FinishRegisterDevice(). + scoped_ptr<dbus::Response> HandleFinishRegisterDevice( + dbus::MethodCall* method_call); + // Handles calls to org.chromium.Buffet.Manager.UpdateState(). + scoped_ptr<dbus::Response> HandleUpdateState( + dbus::MethodCall* method_call); + // Handles calls to org.chromium.Buffet.Manager.Test() + scoped_ptr<::dbus::Response> HandleTestMethod( + ::dbus::MethodCall* method_call); + + scoped_refptr<dbus::Bus> bus_; + dbus::ExportedObject* exported_object_; // weak; owned by the Bus object. + base::WeakPtr<dbus_utils::ExportedObjectManager> object_manager_; + scoped_ptr<Properties> properties_; + + DeviceRegistrationInfo device_info_; + + DISALLOW_COPY_AND_ASSIGN(Manager); +}; + +} // namespace buffet + +#endif // BUFFET_MANAGER_H_
diff --git a/buffet/map_utils.h b/buffet/map_utils.h new file mode 100644 index 0000000..87d8d37 --- /dev/null +++ b/buffet/map_utils.h
@@ -0,0 +1,47 @@ +// 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_MAP_UTILS_H_ +#define BUFFET_MAP_UTILS_H_ + +#include <map> +#include <utility> +#include <vector> + +namespace buffet { + +// Given an STL map returns a vector containing all keys from the map +template<typename T> +std::vector<typename T::key_type> GetMapKeys(const T& map) { + std::vector<typename T::key_type> keys; + keys.reserve(map.size()); + for (auto&& pair : map) + keys.push_back(pair.first); + return keys; +} + +// Given an STL map returns a vector containing all values from the map +template<typename T> +std::vector<typename T::mapped_type> GetMapValues(const T& map) { + std::vector<typename T::mapped_type> values; + values.reserve(map.size()); + for (auto&& pair : map) + values.push_back(pair.second); + return values; +} + +// Given an STL map returns a vector of key-value pairs from the map +template<typename T> +std::vector<std::pair<typename T::key_type, + typename T::mapped_type>> MapToVector(const T& map) { + std::vector<std::pair<typename T::key_type, typename T::mapped_type>> vector; + vector.reserve(map.size()); + for (auto&& pair : map) + vector.push_back(pair); + return vector; +} + +} // namespace buffet + +#endif // BUFFET_MAP_UTILS_H_
diff --git a/buffet/mime_utils.cc b/buffet/mime_utils.cc new file mode 100644 index 0000000..ca221dc --- /dev/null +++ b/buffet/mime_utils.cc
@@ -0,0 +1,156 @@ +// 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/mime_utils.h" + +#include <algorithm> +#include <base/strings/string_util.h> + +#include "buffet/string_utils.h" + +namespace buffet { + +// *************************************************************************** +// ******************************* MIME types ******************************** +// *************************************************************************** +const char mime::types::kApplication[] = "application"; +const char mime::types::kAudio[] = "audio"; +const char mime::types::kImage[] = "image"; +const char mime::types::kMessage[] = "message"; +const char mime::types::kMultipart[] = "multipart"; +const char mime::types::kText[] = "text"; +const char mime::types::kVideo[] = "video"; + +const char mime::parameters::kCharset[] = "charset"; + +const char mime::image::kJpeg[] = "image/jpeg"; +const char mime::image::kPng[] = "image/png"; +const char mime::image::kBmp[] = "image/bmp"; +const char mime::image::kTiff[] = "image/tiff"; +const char mime::image::kGif[] = "image/gif"; + +const char mime::text::kPlain[] = "text/plain"; +const char mime::text::kHtml[] = "text/html"; +const char mime::text::kXml[] = "text/xml"; + +const char mime::application::kOctet_stream[] = "application/octet-stream"; +const char mime::application::kJson[] = "application/json"; +const char mime::application::kWwwFormUrlEncoded[] = + "application/x-www-form-urlencoded"; + +// *************************************************************************** +// **************************** Utility Functions **************************** +// *************************************************************************** +static std::string EncodeParam(const std::string& param) { + // If the string contains one of "tspecials" characters as + // specified in RFC 1521, enclose it in quotes. + if (param.find_first_of("()<>@,;:\\\"/[]?=") != std::string::npos) { + return '"' + param + '"'; + } + return param; +} + +static std::string DecodeParam(const std::string& param) { + if (param.size() > 1 && param.front() == '"' && param.back() == '"') { + return param.substr(1, param.size() - 2); + } + return param; +} + +// *************************************************************************** +// ******************** Main MIME manipulation functions ********************* +// *************************************************************************** + +bool mime::Split(const std::string& mime_string, + std::string* type, std::string* subtype, + mime::Parameters* parameters) { + std::vector<std::string> parts = string_utils::Split(mime_string, ';'); + if (parts.empty()) + return false; + + if (!mime::Split(parts.front(), type, subtype)) + return false; + + if (parameters) { + parameters->clear(); + parameters->reserve(parts.size() - 1); + for (size_t i = 1; i < parts.size(); i++) { + auto pair = string_utils::SplitAtFirst(parts[i], '='); + pair.second = DecodeParam(pair.second); + parameters->push_back(pair); + } + } + return true; +} + +bool mime::Split(const std::string& mime_string, + std::string* type, std::string* subtype) { + std::string mime = mime::RemoveParameters(mime_string); + auto types = string_utils::SplitAtFirst(mime, '/'); + + if (type) + *type = types.first; + + if (subtype) + *subtype = types.second; + + return !types.first.empty() && !types.second.empty(); +} + +std::string mime::Combine(const std::string& type, const std::string& subtype, + const mime::Parameters& parameters) { + std::vector<std::string> parts; + parts.push_back(string_utils::Join('/', type, subtype)); + for (auto&& pair : parameters) { + parts.push_back(string_utils::Join('=', pair.first, + EncodeParam(pair.second))); + } + return string_utils::Join("; ", parts); +} + +std::string mime::GetType(const std::string& mime_string) { + std::string mime = mime::RemoveParameters(mime_string); + return string_utils::SplitAtFirst(mime, '/').first; +} + +std::string mime::GetSubtype(const std::string& mime_string) { + std::string mime = mime::RemoveParameters(mime_string); + return string_utils::SplitAtFirst(mime, '/').second; +} + +mime::Parameters mime::GetParameters(const std::string& mime_string) { + std::string type; + std::string subtype; + mime::Parameters parameters; + + if (mime::Split(mime_string, &type, &subtype, ¶meters)) + return std::move(parameters); + + return mime::Parameters(); +} + +std::string mime::RemoveParameters(const std::string& mime_string) { + return string_utils::SplitAtFirst(mime_string, ';').first; +} + +std::string mime::AppendParameter(const std::string& mime_string, + const std::string& paramName, + const std::string& paramValue) { + std::string mime(mime_string); + mime += "; "; + mime += string_utils::Join('=', paramName, EncodeParam(paramValue)); + return mime; +} + +std::string mime::GetParameterValue(const std::string& mime_string, + const std::string& paramName) { + mime::Parameters params = mime::GetParameters(mime_string); + for (auto&& pair : params) { + if (base::strcasecmp(pair.first.c_str(), paramName.c_str()) == 0) + return pair.second; + } + return std::string(); +} + +} // namespace buffet
diff --git a/buffet/mime_utils.h b/buffet/mime_utils.h new file mode 100644 index 0000000..999b677 --- /dev/null +++ b/buffet/mime_utils.h
@@ -0,0 +1,105 @@ +// 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_MIME_UTILS_H_ +#define BUFFET_MIME_UTILS_H_ + +#include <string> +#include <utility> +#include <vector> + +#include <base/basictypes.h> + +namespace buffet { + +namespace mime { + +namespace types { + // Main MIME type categories + extern const char kApplication[]; // application + extern const char kAudio[]; // audio + extern const char kImage[]; // image + extern const char kMessage[]; // message + extern const char kMultipart[]; // multipart + extern const char kText[]; // test + extern const char kVideo[]; // video +} + +namespace parameters { + // Common MIME parameters + extern const char kCharset[]; // charset=... +} + +namespace image { + // Common image MIME types + extern const char kJpeg[]; // image/jpeg + extern const char kPng[]; // image/png + extern const char kBmp[]; // image/bmp + extern const char kTiff[]; // image/tiff + extern const char kGif[]; // image/gif +} + +namespace text { + // Common text MIME types + extern const char kPlain[]; // text/plain + extern const char kHtml[]; // text/html + extern const char kXml[]; // text/xml +} + +namespace application { + // Common application MIME types + extern const char kOctet_stream[]; // application/octet-stream + extern const char kJson[]; // application/json + extern const char kWwwFormUrlEncoded[]; // application/x-www-form-urlencoded +} + +typedef std::vector<std::pair<std::string, std::string>> Parameters; + +// Combine a MIME type, subtype and parameters into a MIME string. +// e.g. Combine("text", "plain", {{"charset", "utf-8"}}) will give: +// "text/plain; charset=utf-8" +std::string Combine(const std::string& type, const std::string& subtype, + const Parameters& parameters = {}) WARN_UNUSED_RESULT; + +// Splits a MIME string into type and subtype. +// "text/plain;charset=utf-8" => ("text", "plain") +bool Split(const std::string& mime_string, + std::string* type, std::string* subtype); + +// Splits a MIME string into type, subtype, and parameters. +// "text/plain;charset=utf-8" => ("text", "plain", {{"charset","utf-8"}}) +bool Split(const std::string& mime_string, + std::string* type, std::string* subtype, Parameters* parameters); + +// Returns the MIME type from MIME string. +// "text/plain;charset=utf-8" => "text" +std::string GetType(const std::string& mime_string); + +// Returns the MIME sub-type from MIME string. +// "text/plain;charset=utf-8" => "plain" +std::string GetSubtype(const std::string& mime_string); + +// Returns the MIME parameters from MIME string. +// "text/plain;charset=utf-8" => {{"charset","utf-8"}} +Parameters GetParameters(const std::string& mime_string); + +// Removes parameters from a MIME string +// "text/plain;charset=utf-8" => "text/plain" +std::string RemoveParameters(const std::string& mime_string) WARN_UNUSED_RESULT; + +// Appends a parameter to a MIME string. +// "text/plain" => "text/plain; charset=utf-8" +std::string AppendParameter(const std::string& mime_string, + const std::string& paramName, + const std::string& paramValue) WARN_UNUSED_RESULT; + +// Returns the value of a parameter on a MIME string (empty string if missing). +// ("text/plain;charset=utf-8","charset") => "utf-8" +std::string GetParameterValue(const std::string& mime_string, + const std::string& paramName); + +} // namespace mime +} // namespace buffet + +#endif // BUFFET_MIME_UTILS_H_
diff --git a/buffet/mime_utils_unittest.cc b/buffet/mime_utils_unittest.cc new file mode 100644 index 0000000..a705dfa --- /dev/null +++ b/buffet/mime_utils_unittest.cc
@@ -0,0 +1,65 @@ +// Copyright (c) 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/mime_utils.h" + +#include <gtest/gtest.h> + +using namespace buffet; // NOLINT(build/namespaces) + +TEST(MimeUtils, Combine) { + std::string mime_string = mime::Combine(mime::types::kText, "xml"); + EXPECT_EQ(mime::text::kXml, mime_string); + EXPECT_EQ("application/json; charset=utf-8", + mime::Combine(mime::types::kApplication, "json", + {{"charset", "utf-8"}})); +} + +TEST(MimeUtils, Split) { + std::string s1, s2; + EXPECT_TRUE(mime::Split(mime::image::kJpeg, &s1, &s2)); + EXPECT_EQ(mime::types::kImage, s1); + EXPECT_EQ("jpeg", s2); + + mime::Parameters parameters; + EXPECT_TRUE(mime::Split("application/json;charset=utf-8", + &s1, &s2, ¶meters)); + EXPECT_EQ(mime::types::kApplication, s1); + EXPECT_EQ("json", s2); + EXPECT_EQ(mime::application::kJson, mime::Combine(s1, s2)); + EXPECT_EQ(1, parameters.size()); + EXPECT_EQ(mime::parameters::kCharset, parameters.front().first); + EXPECT_EQ("utf-8", parameters.front().second); + EXPECT_EQ("application/json; charset=utf-8", + mime::Combine(s1, s2, parameters)); +} + +TEST(MimeUtils, ExtractParts) { + mime::Parameters parameters; + + EXPECT_EQ(mime::types::kText, mime::GetType(mime::text::kPlain)); + EXPECT_EQ("plain", mime::GetSubtype(mime::text::kPlain)); + + parameters = mime::GetParameters("text/plain; charset=iso-8859-1;foo=bar"); + EXPECT_EQ(2, parameters.size()); + EXPECT_EQ(mime::parameters::kCharset, parameters[0].first); + EXPECT_EQ("iso-8859-1", parameters[0].second); + EXPECT_EQ("foo", parameters[1].first); + EXPECT_EQ("bar", parameters[1].second); +} + +TEST(MimeUtils, AppendRemoveParams) { + std::string mime_string = mime::AppendParameter(mime::text::kXml, + mime::parameters::kCharset, + "utf-8"); + EXPECT_EQ("text/xml; charset=utf-8", mime_string); + mime_string = mime::AppendParameter(mime_string, "foo", "bar"); + EXPECT_EQ("text/xml; charset=utf-8; foo=bar", mime_string); + EXPECT_EQ("utf-8", mime::GetParameterValue(mime_string, + mime::parameters::kCharset)); + EXPECT_EQ("bar", mime::GetParameterValue(mime_string, "foo")); + EXPECT_EQ("", mime::GetParameterValue(mime_string, "baz")); + mime_string = mime::RemoveParameters(mime_string); + EXPECT_EQ(mime::text::kXml, mime_string); +}
diff --git a/buffet/storage_impls.cc b/buffet/storage_impls.cc new file mode 100644 index 0000000..d95538a --- /dev/null +++ b/buffet/storage_impls.cc
@@ -0,0 +1,44 @@ +// 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/storage_impls.h" + +#include <string> + +#include <base/files/important_file_writer.h> +#include <base/json/json_reader.h> +#include <base/json/json_writer.h> + +namespace buffet { + +FileStorage::FileStorage(const base::FilePath& file_path) + : file_path_(file_path) { } + +std::unique_ptr<base::Value> FileStorage::Load() { + std::string json; + if (!base::ReadFileToString(file_path_, &json)) + return std::unique_ptr<base::Value>(); + + return std::unique_ptr<base::Value>(base::JSONReader::Read(json)); +} + +bool FileStorage::Save(const base::Value* config) { + std::string json; + base::JSONWriter::WriteWithOptions( + config, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json); + return base::ImportantFileWriter::WriteFileAtomically(file_path_, json); +} + + +std::unique_ptr<base::Value> MemStorage::Load() { + return std::unique_ptr<base::Value>(cache_->DeepCopy()); +} + +bool MemStorage::Save(const base::Value* config) { + cache_.reset(config->DeepCopy()); + ++save_count_; + return true; +} + +} // namespace buffet
diff --git a/buffet/storage_impls.h b/buffet/storage_impls.h new file mode 100644 index 0000000..6e69084 --- /dev/null +++ b/buffet/storage_impls.h
@@ -0,0 +1,47 @@ +// 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_STORAGE_IMPLS_H_ +#define BUFFET_STORAGE_IMPLS_H_ + +#include <base/basictypes.h> +#include <base/file_util.h> +#include <base/values.h> + +#include "buffet/storage_interface.h" + +namespace buffet { + +// Persists the given Value to an atomically written file. +class FileStorage : public StorageInterface { + public: + explicit FileStorage(const base::FilePath& file_path); + virtual ~FileStorage() = default; + virtual std::unique_ptr<base::Value> Load() override; + virtual bool Save(const base::Value* config) override; + + private: + base::FilePath file_path_; + DISALLOW_COPY_AND_ASSIGN(FileStorage); +}; + +// StorageInterface for testing. Just stores the values in memory. +class MemStorage : public StorageInterface { + public: + MemStorage() = default; + virtual ~MemStorage() = default; + virtual std::unique_ptr<base::Value> Load() override; + virtual bool Save(const base::Value* config) override; + int save_count() { return save_count_; } + void reset_save_count() { save_count_ = 0; } + + private: + int save_count_ = 0; + std::unique_ptr<base::Value> cache_; + DISALLOW_COPY_AND_ASSIGN(MemStorage); +}; + +} // namespace buffet + +#endif // BUFFET_STORAGE_IMPLS_H_
diff --git a/buffet/storage_interface.h b/buffet/storage_interface.h new file mode 100644 index 0000000..26d71c1 --- /dev/null +++ b/buffet/storage_interface.h
@@ -0,0 +1,32 @@ +// 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_STORAGE_INTERFACE_H_ +#define BUFFET_STORAGE_INTERFACE_H_ + +#include <memory> + +#include <base/values.h> + +namespace buffet { + +// We need to persist data in a couple places, and it is convenient to hide +// the details of this storage behind an interface for test purposes. +class StorageInterface { + public: + // Load the device registration configuration from storage. + // If it fails (e.g. the storage container [file?] doesn't exist), then + // it returns empty unique_ptr (aka nullptr). + virtual std::unique_ptr<base::Value> Load() = 0; + + // Save the device registration configuration to storage. + // If saved successfully, returns true. Could fail when writing to + // physical storage like file system for various reasons (out of disk space, + // access permissions, etc). + virtual bool Save(const base::Value* config) = 0; +}; + +} // namespace buffet + +#endif // BUFFET_STORAGE_INTERFACE_H_
diff --git a/buffet/string_utils.cc b/buffet/string_utils.cc new file mode 100644 index 0000000..55c7d3f --- /dev/null +++ b/buffet/string_utils.cc
@@ -0,0 +1,90 @@ +// 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/string_utils.h" + +#include <algorithm> +#include <string.h> +#include <utility> + +#include <base/strings/string_util.h> + +namespace buffet { +namespace string_utils { + +std::vector<std::string> Split(const std::string& str, + char delimiter, + bool trim_whitespaces, + bool purge_empty_strings) { + std::vector<std::string> tokens; + if (delimiter == 0) + return tokens; + + const char* sz = str.c_str(); + if (sz) { + const char* szNext = strchr(sz, delimiter); + while (szNext) { + if (szNext != sz || !purge_empty_strings) + tokens.emplace_back(sz, szNext - sz); + sz = szNext + 1; + szNext = strchr(sz, delimiter); + } + if (*sz != 0 || !purge_empty_strings) + tokens.emplace_back(sz); + } + + if (trim_whitespaces) { + std::for_each(tokens.begin(), tokens.end(), + [](std::string& str) { // NOLINT(runtime/references) + base::TrimWhitespaceASCII(str, base::TRIM_ALL, &str); }); + } + + return tokens; +} + +std::pair<std::string, std::string> SplitAtFirst(const std::string& str, + char delimiter, + bool trim_whitespaces) { + std::pair<std::string, std::string> pair; + if (delimiter == 0) + return pair; + + const char* sz = str.c_str(); + const char* szNext = strchr(sz, delimiter); + if (szNext) { + pair.first = std::string(sz, szNext); + pair.second = std::string(szNext + 1); + } else { + pair.first = str; + } + + if (trim_whitespaces) { + base::TrimWhitespaceASCII(pair.first, base::TRIM_ALL, &pair.first); + base::TrimWhitespaceASCII(pair.second, base::TRIM_ALL, &pair.second); + } + + return pair; +} + +std::string Join(char delimiter, const std::vector<std::string>& strings) { + return JoinString(strings, delimiter); +} + +std::string Join(const std::string& delimiter, + const std::vector<std::string>& strings) { + return JoinString(strings, delimiter); +} + +std::string Join(char delimiter, + const std::string& str1, const std::string& str2) { + return str1 + delimiter + str2; +} + +std::string Join(const std::string& delimiter, + const std::string& str1, const std::string& str2) { + return str1 + delimiter + str2; +} + +} // namespace string_utils +} // namespace buffet
diff --git a/buffet/string_utils.h b/buffet/string_utils.h new file mode 100644 index 0000000..95da72d --- /dev/null +++ b/buffet/string_utils.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_STRING_UTILS_H_ +#define BUFFET_STRING_UTILS_H_ + +#include <string> +#include <utility> +#include <vector> + +namespace buffet { +namespace string_utils { + +// Treats the string as a delimited list of substrings and returns the array +// of original elements of the list. +// By default, empty elements from the original string are omitted and +// each element has all whitespaces trimmed off. +std::vector<std::string> Split(const std::string& str, + char delimiter, + bool trim_whitespaces = true, + bool purge_empty_strings = true); + +// Splits the string into two pieces at the first position of the specified +// delimiter. By default, each part has all whitespaces trimmed off. +std::pair<std::string, std::string> SplitAtFirst(const std::string& str, + char delimiter, + bool trim_whitespaces = true); + +// Joins an array of strings into a single string separated by |delimiter|. +std::string Join(char delimiter, const std::vector<std::string>& strings); +std::string Join(const std::string& delimiter, + const std::vector<std::string>& strings); +std::string Join(char delimiter, + const std::string& str1, const std::string& str2); +std::string Join(const std::string& delimiter, + const std::string& str1, const std::string& str2); + +} // namespace string_utils +} // namespace buffet + +#endif // BUFFET_STRING_UTILS_H_
diff --git a/buffet/string_utils_unittest.cc b/buffet/string_utils_unittest.cc new file mode 100644 index 0000000..4a021f4 --- /dev/null +++ b/buffet/string_utils_unittest.cc
@@ -0,0 +1,109 @@ +// Copyright (c) 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/string_utils.h" + +#include <gtest/gtest.h> + +using namespace buffet; // NOLINT(build/namespaces) + +TEST(StringUtils, Split) { + std::vector<std::string> parts; + + parts = string_utils::Split(",a,bc , d,,e,", ',', true, true); + EXPECT_EQ(4, parts.size()); + EXPECT_EQ("a", parts[0]); + EXPECT_EQ("bc", parts[1]); + EXPECT_EQ("d", parts[2]); + EXPECT_EQ("e", parts[3]); + + parts = string_utils::Split(",a,bc , d,,e,", ',', false, true); + EXPECT_EQ(4, parts.size()); + EXPECT_EQ("a", parts[0]); + EXPECT_EQ("bc ", parts[1]); + EXPECT_EQ(" d", parts[2]); + EXPECT_EQ("e", parts[3]); + + parts = string_utils::Split(",a,bc , d,,e,", ',', true, false); + EXPECT_EQ(7, parts.size()); + EXPECT_EQ("", parts[0]); + EXPECT_EQ("a", parts[1]); + EXPECT_EQ("bc", parts[2]); + EXPECT_EQ("d", parts[3]); + EXPECT_EQ("", parts[4]); + EXPECT_EQ("e", parts[5]); + EXPECT_EQ("", parts[6]); + + parts = string_utils::Split(",a,bc , d,,e,", ',', false, false); + EXPECT_EQ(7, parts.size()); + EXPECT_EQ("", parts[0]); + EXPECT_EQ("a", parts[1]); + EXPECT_EQ("bc ", parts[2]); + EXPECT_EQ(" d", parts[3]); + EXPECT_EQ("", parts[4]); + EXPECT_EQ("e", parts[5]); + EXPECT_EQ("", parts[6]); +} + +TEST(StringUtils, SplitAtFirst) { + std::pair<std::string, std::string> pair; + + pair = string_utils::SplitAtFirst(" 123 : 4 : 56 : 789 ", ':', true); + EXPECT_EQ("123", pair.first); + EXPECT_EQ("4 : 56 : 789", pair.second); + + pair = string_utils::SplitAtFirst(" 123 : 4 : 56 : 789 ", ':', false); + EXPECT_EQ(" 123 ", pair.first); + EXPECT_EQ(" 4 : 56 : 789 ", pair.second); + + pair = string_utils::SplitAtFirst("", '='); + EXPECT_EQ("", pair.first); + EXPECT_EQ("", pair.second); + + pair = string_utils::SplitAtFirst("=", '='); + EXPECT_EQ("", pair.first); + EXPECT_EQ("", pair.second); + + pair = string_utils::SplitAtFirst("a=", '='); + EXPECT_EQ("a", pair.first); + EXPECT_EQ("", pair.second); + + pair = string_utils::SplitAtFirst("abc=", '='); + EXPECT_EQ("abc", pair.first); + EXPECT_EQ("", pair.second); + + pair = string_utils::SplitAtFirst("=a", '='); + EXPECT_EQ("", pair.first); + EXPECT_EQ("a", pair.second); + + pair = string_utils::SplitAtFirst("=abc=", '='); + EXPECT_EQ("", pair.first); + EXPECT_EQ("abc=", pair.second); + + pair = string_utils::SplitAtFirst("abc", '='); + EXPECT_EQ("abc", pair.first); + EXPECT_EQ("", pair.second); +} + +TEST(StringUtils, Join_Char) { + EXPECT_EQ("", string_utils::Join(',', {})); + EXPECT_EQ("abc", string_utils::Join(',', {"abc"})); + EXPECT_EQ("abc,defg", string_utils::Join(',', {"abc", "defg"})); + EXPECT_EQ("1:2:3", string_utils::Join(':', {"1", "2", "3"})); + EXPECT_EQ("192.168.0.1", string_utils::Join('.', {"192", "168", "0", "1"})); + EXPECT_EQ("ff02::1", string_utils::Join(':', {"ff02", "", "1"})); +} + +TEST(StringUtils, Join_String) { + EXPECT_EQ("", string_utils::Join(",", {})); + EXPECT_EQ("abc", string_utils::Join(",", {"abc"})); + EXPECT_EQ("abc,defg", string_utils::Join(",", {"abc", "defg"})); + EXPECT_EQ("1 : 2 : 3", string_utils::Join(" : ", {"1", "2", "3"})); + EXPECT_EQ("123", string_utils::Join("", {"1", "2", "3"})); +} + +TEST(StringUtils, Join_Pair) { + EXPECT_EQ("ab,cd", string_utils::Join(',', "ab", "cd")); + EXPECT_EQ("key = value", string_utils::Join(" = ", "key", "value")); +}
diff --git a/buffet/url_utils.cc b/buffet/url_utils.cc new file mode 100644 index 0000000..0395094 --- /dev/null +++ b/buffet/url_utils.cc
@@ -0,0 +1,167 @@ +// 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/url_utils.h" + +#include <algorithm> + +namespace { +// Given a URL string, determine where the query string starts and ends. +// URLs have schema, domain and path (along with possible user name, password +// and port number which are of no interest for us here) which could optionally +// have a query string that is separated from the path by '?'. Finally, the URL +// could also have a '#'-separated URL fragment which is usually used by the +// browser as a bookmark element. So, for example: +// http://server.com/path/to/object?k=v&foo=bar#fragment +// Here: +// http://server.com/path/to/object - is the URL of the object, +// ?k=v&foo=bar - URL query string +// #fragment - URL fragment string +// If |exclude_fragment| is true, the function returns the start character and +// the length of the query string alone. If it is false, the query string length +// will include both the query string and the fragment. +bool GetQueryStringPos(const std::string& url, bool exclude_fragment, + size_t* query_pos, size_t* query_len) { + size_t query_start = url.find_first_of("?#"); + if (query_start == std::string::npos) { + *query_pos = url.size(); + if (query_len) + *query_len = 0; + return false; + } + + *query_pos = query_start; + if (query_len) { + size_t query_end = url.size(); + + if (exclude_fragment) { + if (url[query_start] == '?') { + size_t pos_fragment = url.find('#', query_start); + if (pos_fragment != std::string::npos) + query_end = pos_fragment; + } else { + query_end = query_start; + } + } + *query_len = query_end - query_start; + } + return true; +} +} // anonymous namespace + +namespace buffet { + +std::string url::TrimOffQueryString(std::string* url) { + size_t query_pos; + if (!GetQueryStringPos(*url, false, &query_pos, nullptr)) + return std::string(); + std::string query_string = url->substr(query_pos); + url->resize(query_pos); + return query_string; +} + +std::string url::Combine( + const std::string& url, const std::string& subpath) { + return CombineMultiple(url, {subpath}); +} + +std::string url::CombineMultiple( + const std::string& url, const std::vector<std::string>& parts) { + std::string result = url; + if (!parts.empty()) { + std::string query_string = TrimOffQueryString(&result); + for (auto&& part : parts) { + if (!part.empty()) { + if (!result.empty() && result.back() != '/') + result += '/'; + size_t non_slash_pos = part.find_first_not_of('/'); + if (non_slash_pos != std::string::npos) + result += part.substr(non_slash_pos); + } + } + result += query_string; + } + return result; +} + +std::string url::GetQueryString( + const std::string& url, bool remove_fragment) { + std::string query_string; + size_t query_pos, query_len; + if (GetQueryStringPos(url, remove_fragment, &query_pos, &query_len)) { + query_string = url.substr(query_pos, query_len); + } + return query_string; +} + +data_encoding::WebParamList url::GetQueryStringParameters( + const std::string& url) { + // Extract the query string and remove the leading '?'. + std::string query_string = GetQueryString(url, true); + if (!query_string.empty() && query_string.front() == '?') + query_string.erase(query_string.begin()); + return data_encoding::WebParamsDecode(query_string); +} + +std::string url::GetQueryStringValue( + const std::string& url, const std::string& name) { + return GetQueryStringValue(GetQueryStringParameters(url), name); +} + +std::string url::GetQueryStringValue( + const data_encoding::WebParamList& params, + const std::string& name) { + for (auto&& pair : params) { + if (name.compare(pair.first) == 0) + return pair.second; + } + return std::string(); +} + +std::string url::RemoveQueryString( + const std::string& url, bool remove_fragment_too) { + size_t query_pos, query_len; + if (!GetQueryStringPos(url, !remove_fragment_too, &query_pos, &query_len)) + return url; + std::string result = url.substr(0, query_pos); + size_t fragment_pos = query_pos + query_len; + if (fragment_pos < url.size()) { + result += url.substr(fragment_pos); + } + return result; +} + +std::string url::AppendQueryParam( + const std::string& url, const std::string& name, const std::string& value) { + return AppendQueryParams(url, {{name, value}}); +} + +std::string url::AppendQueryParams( + const std::string& url, + const data_encoding::WebParamList& params) { + if (params.empty()) + return url; + size_t query_pos, query_len; + GetQueryStringPos(url, true, &query_pos, &query_len); + size_t fragment_pos = query_pos + query_len; + std::string result = url.substr(0, fragment_pos); + if (query_len == 0) { + result += '?'; + } else if (query_len > 1) { + result += '&'; + } + result += data_encoding::WebParamsEncode(params); + if (fragment_pos < url.size()) { + result += url.substr(fragment_pos); + } + return result; +} + +bool url::HasQueryString(const std::string& url) { + size_t query_pos, query_len; + GetQueryStringPos(url, true, &query_pos, &query_len); + return (query_len > 0); +} + +} // namespace buffet
diff --git a/buffet/url_utils.h b/buffet/url_utils.h new file mode 100644 index 0000000..d9bce25 --- /dev/null +++ b/buffet/url_utils.h
@@ -0,0 +1,77 @@ +// 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_URL_UTILS_H_ +#define BUFFET_URL_UTILS_H_ + +#include <string> +#include <vector> + +#include <base/basictypes.h> + +#include "buffet/data_encoding.h" + +namespace buffet { + +namespace url { + +// Appends a subpath to url and delimiting then with '/' if the path doesn't +// end with it already. Also handles URLs with query parameters/fragment. +std::string Combine(const std::string& url, + const std::string& subpath) WARN_UNUSED_RESULT; +std::string CombineMultiple( + const std::string& url, + const std::vector<std::string>& parts) WARN_UNUSED_RESULT; + +// Removes the query string/fragment from |url| and returns the query string. +// This method actually modifies |url|. So, if you call it on this: +// http://www.test.org/?foo=bar +// it will modify |url| to "http://www.test.org/" and return "?foo=bar" +std::string TrimOffQueryString(std::string* url); + +// Returns the query string, if available. +// For example, for the following URL: +// http://server.com/path/to/object?k=v&foo=bar#fragment +// Here: +// http://server.com/path/to/object - is the URL of the object, +// ?k=v&foo=bar - URL query string +// #fragment - URL fragment string +// If |remove_fragment| is true, the function returns the query string without +// the fragment. Otherwise the fragment is included as part of the result. +std::string GetQueryString(const std::string& url, bool remove_fragment); + +// Parses the query string into a set of key-value pairs. +data_encoding::WebParamList GetQueryStringParameters(const std::string& url); + +// Returns a value of the specified query parameter, or empty string if missing. +std::string GetQueryStringValue(const std::string& url, + const std::string& name); +std::string GetQueryStringValue(const data_encoding::WebParamList& params, + const std::string& name); + +// Removes the query string and/or a fragment part from URL. +// If |remove_fragment| is specified, the fragment is also removed. +// For example: +// http://server.com/path/to/object?k=v&foo=bar#fragment +// true -> http://server.com/path/to/object +// false -> http://server.com/path/to/object#fragment +std::string RemoveQueryString(const std::string& url, + bool remove_fragment) WARN_UNUSED_RESULT; + +// Appends a single query parameter to the URL. +std::string AppendQueryParam(const std::string& url, + const std::string& name, + const std::string& value) WARN_UNUSED_RESULT; +// Appends a list of query parameters to the URL. +std::string AppendQueryParams( + const std::string& url, + const data_encoding::WebParamList& params) WARN_UNUSED_RESULT; + +// Checks if the URL has query parameters. +bool HasQueryString(const std::string& url); + +} // namespace url +} // namespace buffet + +#endif // BUFFET_URL_UTILS_H_
diff --git a/buffet/url_utils_unittest.cc b/buffet/url_utils_unittest.cc new file mode 100644 index 0000000..7f4826d --- /dev/null +++ b/buffet/url_utils_unittest.cc
@@ -0,0 +1,152 @@ +// Copyright (c) 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/url_utils.h" + +#include <gtest/gtest.h> + +using namespace buffet; // NOLINT(build/namespaces) + +TEST(UrlUtils, Combine) { + EXPECT_EQ("http://sample.org/path", + url::Combine("http://sample.org", "path")); + EXPECT_EQ("http://sample.org/path", + url::Combine("http://sample.org/", "path")); + EXPECT_EQ("path1/path2", url::Combine("", "path1/path2")); + EXPECT_EQ("path1/path2", url::Combine("path1", "path2")); + EXPECT_EQ("http://sample.org", + url::Combine("http://sample.org", "")); + EXPECT_EQ("http://sample.org/path", + url::Combine("http://sample.org/", "/path")); + EXPECT_EQ("http://sample.org/path", + url::Combine("http://sample.org", "//////path")); + EXPECT_EQ("http://sample.org/", + url::Combine("http://sample.org", "///")); + EXPECT_EQ("http://sample.org/obj/path1/path2", + url::Combine("http://sample.org/obj", "path1/path2")); + EXPECT_EQ("http://sample.org/obj/path1/path2#tag", + url::Combine("http://sample.org/obj#tag", "path1/path2")); + EXPECT_EQ("http://sample.org/obj/path1/path2?k1=v1&k2=v2", + url::Combine("http://sample.org/obj?k1=v1&k2=v2", "path1/path2")); + EXPECT_EQ("http://sample.org/obj/path1/path2?k1=v1#k2=v2", + url::Combine("http://sample.org/obj/?k1=v1#k2=v2", "path1/path2")); + EXPECT_EQ("http://sample.org/obj/path1/path2#tag?", + url::Combine("http://sample.org/obj#tag?", "path1/path2")); + EXPECT_EQ("path1/path2", url::CombineMultiple("", {"path1", "path2"})); + EXPECT_EQ("http://sample.org/obj/part1/part2", + url::CombineMultiple("http://sample.org", + {"obj", "", "/part1/", "part2"})); +} + +TEST(UrlUtils, GetQueryString) { + EXPECT_EQ("", url::GetQueryString("http://sample.org", false)); + EXPECT_EQ("", url::GetQueryString("http://sample.org", true)); + EXPECT_EQ("", url::GetQueryString("", false)); + EXPECT_EQ("", url::GetQueryString("", true)); + + EXPECT_EQ("?q=v&b=2#tag?2", + url::GetQueryString("http://s.com/?q=v&b=2#tag?2", false)); + EXPECT_EQ("?q=v&b=2", + url::GetQueryString("http://s.com/?q=v&b=2#tag?2", true)); + + EXPECT_EQ("#tag?a=2", + url::GetQueryString("http://s.com/#tag?a=2", false)); + EXPECT_EQ("", + url::GetQueryString("http://s.com/#tag?a=2", true)); + + EXPECT_EQ("?a=2&b=2", + url::GetQueryString("?a=2&b=2", false)); + EXPECT_EQ("?a=2&b=2", + url::GetQueryString("?a=2&b=2", true)); + + EXPECT_EQ("#s#?d#?f?#s?#d", + url::GetQueryString("#s#?d#?f?#s?#d", false)); + EXPECT_EQ("", + url::GetQueryString("#s#?d#?f?#s?#d", true)); +} + +TEST(UrlUtils, GetQueryStringParameters) { + auto params = url::GetQueryStringParameters( + "http://sample.org/path?k=v&&%3Dkey%3D=val%26&r#blah"); + + EXPECT_EQ(3, params.size()); + EXPECT_EQ("k", params[0].first); + EXPECT_EQ("v", params[0].second); + EXPECT_EQ("=key=", params[1].first); + EXPECT_EQ("val&", params[1].second); + EXPECT_EQ("r", params[2].first); + EXPECT_EQ("", params[2].second); +} + +TEST(UrlUtils, GetQueryStringValue) { + std::string url = "http://url?key1=val1&&key2=val2"; + EXPECT_EQ("val1", url::GetQueryStringValue(url, "key1")); + EXPECT_EQ("val2", url::GetQueryStringValue(url, "key2")); + EXPECT_EQ("", url::GetQueryStringValue(url, "key3")); + + auto params = url::GetQueryStringParameters(url); + EXPECT_EQ("val1", url::GetQueryStringValue(params, "key1")); + EXPECT_EQ("val2", url::GetQueryStringValue(params, "key2")); + EXPECT_EQ("", url::GetQueryStringValue(params, "key3")); +} + +TEST(UrlUtils, TrimOffQueryString) { + std::string url = "http://url?key1=val1&key2=val2#fragment"; + std::string query = url::TrimOffQueryString(&url); + EXPECT_EQ("http://url", url); + EXPECT_EQ("?key1=val1&key2=val2#fragment", query); + + url = "http://url#fragment"; + query = url::TrimOffQueryString(&url); + EXPECT_EQ("http://url", url); + EXPECT_EQ("#fragment", query); + + url = "http://url"; + query = url::TrimOffQueryString(&url); + EXPECT_EQ("http://url", url); + EXPECT_EQ("", query); +} + +TEST(UrlUtils, RemoveQueryString) { + std::string url = "http://url?key1=val1&key2=val2#fragment"; + EXPECT_EQ("http://url", url::RemoveQueryString(url, true)); + EXPECT_EQ("http://url#fragment", url::RemoveQueryString(url, false)); +} + +TEST(UrlUtils, AppendQueryParam) { + std::string url = "http://server.com/path"; + url = url::AppendQueryParam(url, "param", "value"); + EXPECT_EQ("http://server.com/path?param=value", url); + url = url::AppendQueryParam(url, "param2", "v"); + EXPECT_EQ("http://server.com/path?param=value¶m2=v", url); + + url = "http://server.com/path#fragment"; + url = url::AppendQueryParam(url, "param", "value"); + EXPECT_EQ("http://server.com/path?param=value#fragment", url); + url = url::AppendQueryParam(url, "param2", "v"); + EXPECT_EQ("http://server.com/path?param=value¶m2=v#fragment", url); + + url = url::AppendQueryParam("http://server.com/path?", "param", "value"); + EXPECT_EQ("http://server.com/path?param=value", url); +} + +TEST(UrlUtils, AppendQueryParams) { + std::string url = "http://server.com/path"; + url = url::AppendQueryParams(url, {}); + EXPECT_EQ("http://server.com/path", url); + url = url::AppendQueryParams(url, {{"param", "value"}, {"q", "="}}); + EXPECT_EQ("http://server.com/path?param=value&q=%3D", url); + url += "#fr?"; + url = url::AppendQueryParams(url, {{"p", "1"}, {"s&", "\n"}}); + EXPECT_EQ("http://server.com/path?param=value&q=%3D&p=1&s%26=%0A#fr?", url); +} + +TEST(UrlUtils, HasQueryString) { + EXPECT_FALSE(url::HasQueryString("http://server.com/path")); + EXPECT_FALSE(url::HasQueryString("http://server.com/path#blah?v=1")); + EXPECT_TRUE(url::HasQueryString("http://server.com/path?v=1#blah")); + EXPECT_TRUE(url::HasQueryString("http://server.com/path?v=1")); + EXPECT_FALSE(url::HasQueryString("")); + EXPECT_TRUE(url::HasQueryString("?ss")); +}