Buffet: Phase 1 of GCD device registration workflow
Implemented the basic device registration workflow in buffet daemon.
Updated buffet_client to perform/test device registration:
Check for device registration:
buffet_client cr
Getting registered device information:
buffet_client di
Begin registration (with all default values):
buffet_client sr
Begin registration with custom parameters:
buffet_client sr "service_url=http://localhost/buffet&device_kind=coffeePot"
Finalize registration:
buffet_client fr 4/FsXprlpVsmPw6z7ro7aqU156Eh6V.0ktCYeVc3DwYEnp6UAPFm0GAey3PigI
BUG=chromium:363348
TEST=unit tests passed.
Change-Id: Id8a90b66fbdc366eaa9f62caa82a7cb0abc2e638
Reviewed-on: https://chromium-review.googlesource.com/195082
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Chris Sosa <sosa@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc
new file mode 100644
index 0000000..c2b7780
--- /dev/null
+++ b/buffet/device_registration_info.cc
@@ -0,0 +1,471 @@
+// 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 <base/json/json_reader.h>
+#include <base/json/json_writer.h>
+#include <base/values.h>
+#include <base/file_util.h>
+#include <memory>
+
+#include "buffet/http_utils.h"
+#include "buffet/mime_utils.h"
+#include "buffet/string_utils.h"
+#include "buffet/data_encoding.h"
+
+using namespace chromeos::http;
+using namespace chromeos::data_encoding;
+
+namespace {
+// 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";
+
+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 = chromeos::string_utils::Join(' ',
+ access_token_type,
+ access_token);
+ return {request_header::kAuthorization, authorization};
+}
+
+std::unique_ptr<base::DictionaryValue> ParseOAuthResponse(
+ const Response* response, std::string* error_message) {
+ int code = 0;
+ auto resp = ParseJsonResponse(response, &code, error_message);
+ if (resp && code >= status_code::BadRequest) {
+ if (error_message) {
+ error_message->clear();
+ std::string error_code, error;
+ if (resp->GetString("error", &error_code) &&
+ resp->GetString("error_description", &error)) {
+ *error_message = error_code + " (" + error + ")";
+ } else {
+ *error_message = "Unexpected OAuth error";
+ }
+ }
+ return std::unique_ptr<base::DictionaryValue>();
+ }
+ return resp;
+}
+
+std::string BuildURL(std::string url,
+ const std::string& subpath,
+ const WebParamList& params) {
+ if (!subpath.empty()) {
+ if (!url.empty() && url.back() != '/')
+ url += '/';
+ url += subpath;
+ }
+
+ if (!params.empty()) {
+ url += '?';
+ url += WebParamsEncode(params);
+ }
+ return url;
+}
+
+
+} // anonymous namespace
+
+namespace buffet {
+
+std::pair<std::string, std::string>
+ DeviceRegistrationInfo::GetAuthorizationHeader() const {
+ return BuildAuthHeader(/*"Bearer"*/"OAuth", access_token_);
+}
+
+std::string DeviceRegistrationInfo::GetServiceURL(
+ const std::string& subpath, const WebParamList& params) const {
+ return BuildURL(service_url_, subpath, params);
+}
+
+std::string DeviceRegistrationInfo::GetDeviceURL(
+ const std::string& subpath, const WebParamList& params) const {
+ CHECK(!device_id_.empty()) << "Must have a valid device ID";
+ std::string path = "devices/" + device_id_;
+ if (!subpath.empty()) {
+ path += '/' + subpath;
+ }
+ return GetServiceURL(path, params);
+}
+
+std::string DeviceRegistrationInfo::GetOAuthURL(const std::string& subpath,
+ const WebParamList& params) const {
+ return BuildURL(oauth_url_, subpath, params);
+}
+
+std::string DeviceRegistrationInfo::GetDeviceId() {
+ return CheckRegistration() ? device_id_ : std::string();
+}
+
+bool DeviceRegistrationInfo::Load() {
+ // TODO(avakulenko): Figure out security implications of storing
+ // this data unencrypted.
+ std::string json;
+ if (!base::ReadFileToString(base::FilePath(kDeviceInfoFilePath), &json))
+ return false;
+
+ auto value = std::unique_ptr<base::Value>(base::JSONReader::Read(json));
+ 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(kClientId, &client_id))
+ return false;
+ std::string client_secret;
+ if (!dict->GetString(kClientSecret, &client_secret))
+ return false;
+ std::string api_key;
+ if (!dict->GetString(kApiKey, &api_key))
+ return false;
+ std::string refresh_token;
+ if (!dict->GetString(kRefreshToken, &refresh_token))
+ return false;
+ std::string device_id;
+ if (!dict->GetString(kDeviceId, &device_id))
+ return false;
+ std::string oauth_url;
+ if (!dict->GetString(kOAuthURL, &oauth_url))
+ return false;
+ std::string service_url;
+ if (!dict->GetString(kServiceURL, &service_url))
+ return false;
+ std::string device_robot_account;
+ if (!dict->GetString(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 {
+ // TODO(avakulenko): Figure out security implications of storing
+ // this data unencrypted.
+ base::DictionaryValue dict;
+ dict.SetString(kClientId, client_id_);
+ dict.SetString(kClientSecret, client_secret_);
+ dict.SetString(kApiKey, api_key_);
+ dict.SetString(kRefreshToken, refresh_token_);
+ dict.SetString(kDeviceId, device_id_);
+ dict.SetString(kOAuthURL, oauth_url_);
+ dict.SetString(kServiceURL, service_url_);
+ dict.SetString(kRobotAccount, device_robot_account_);
+
+ std::string json;
+ base::JSONWriter::WriteWithOptions(&dict,
+ base::JSONWriter::OPTIONS_PRETTY_PRINT,
+ &json);
+ int count = file_util::WriteFile(base::FilePath(kDeviceInfoFilePath),
+ json.data(), static_cast<int>(json.size()));
+
+ return (count > 0);
+}
+
+bool DeviceRegistrationInfo::CheckRegistration() {
+ 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.";
+ return false;
+ }
+
+ LOG(INFO) << "Device registration record found.";
+ return ValidateAndRefreshAccessToken();
+}
+
+bool DeviceRegistrationInfo::ValidateAndRefreshAccessToken() {
+ 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 = PostFormData(GetOAuthURL("token"), {
+ {"refresh_token", refresh_token_},
+ {"client_id", client_id_},
+ {"client_secret", client_secret_},
+ {"grant_type", "refresh_token"},
+ });
+ if (!response)
+ return false;
+
+ std::string error;
+ auto json = ParseOAuthResponse(response.get(), &error);
+ if (!json) {
+ LOG(ERROR) << "Unable to refresh access token: " << error;
+ 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.";
+ 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() {
+ if (!CheckRegistration())
+ return std::unique_ptr<base::Value>();
+
+ auto response = Get(GetDeviceURL(), {GetAuthorizationHeader()});
+ int status_code = 0;
+ std::unique_ptr<base::Value> device_info =
+ ParseJsonResponse(response.get(), &status_code, nullptr);
+
+ if (device_info) {
+ if (status_code >= status_code::BadRequest) {
+ LOG(WARNING) << "Failed to retrieve the device info. Response code = "
+ << status_code;
+ return std::unique_ptr<base::Value>();
+ }
+ }
+ return device_info;
+}
+
+bool CheckParam(const std::string& param_name,
+ const std::string& param_value,
+ std::string* error_msg) {
+ if (!param_value.empty())
+ return true;
+
+ if (error_msg)
+ *error_msg = "Parameter " + param_name + " not specified";
+ return false;
+}
+
+std::string DeviceRegistrationInfo::StartRegistration(
+ const std::map<std::string, std::shared_ptr<base::Value>>& params,
+ std::string* error_msg) {
+ GetParamValue(params, kClientId, &client_id_);
+ GetParamValue(params, kClientSecret, &client_secret_);
+ GetParamValue(params, kApiKey, &api_key_);
+ GetParamValue(params, kDeviceId, &device_id_);
+ GetParamValue(params, kDeviceKind, &device_kind_);
+ GetParamValue(params, kSystemName, &system_name_);
+ GetParamValue(params, kDisplayName, &display_name_);
+ GetParamValue(params, kOAuthURL, &oauth_url_);
+ GetParamValue(params, kServiceURL, &service_url_);
+
+ if (!CheckParam(kClientId, client_id_, error_msg))
+ return std::string();
+ if (!CheckParam(kClientSecret, client_secret_, error_msg))
+ return std::string();
+ if (!CheckParam(kApiKey, api_key_, error_msg))
+ return std::string();
+ if (!CheckParam(kDeviceKind, device_kind_, error_msg))
+ return std::string();
+ if (!CheckParam(kSystemName, system_name_, error_msg))
+ return std::string();
+ if (!CheckParam(kOAuthURL, oauth_url_, error_msg))
+ return std::string();
+ if (!CheckParam(kServiceURL, service_url_, error_msg))
+ 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 = ParseJsonResponse(PostJson(url, &req_json).get(),
+ nullptr, error_msg);
+ if (!resp_json)
+ return std::string();
+
+ const base::DictionaryValue* resp_dict = nullptr;
+ if (!resp_json->GetAsDictionary(&resp_dict)) {
+ if (error_msg)
+ *error_msg = "Invalid response received";
+ return std::string();
+ }
+
+ if (!resp_dict->GetString("id", &ticket_id_))
+ 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) {
+ if (ticket_id_.empty()) {
+ LOG(ERROR) << "Finish registration without ticket ID";
+ return false;
+ }
+
+ std::string url = GetServiceURL("registrationTickets/" + ticket_id_);
+ std::unique_ptr<Response> response;
+ if (!user_auth_code.empty()) {
+ std::string user_access_token;
+ response = 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"}
+ });
+ if (!response)
+ return false;
+
+ auto json_resp = ParseOAuthResponse(response.get(), nullptr);
+ if (!json_resp ||
+ !json_resp->GetString("access_token", &user_access_token)) {
+ return false;
+ }
+
+ base::DictionaryValue user_info;
+ user_info.SetString("userEmail", "me");
+ response = PatchJson(url, &user_info,
+ {BuildAuthHeader("Bearer", user_access_token)});
+
+ std::string error;
+ auto json = ParseJsonResponse(response.get(), nullptr, &error);
+ if (!json) {
+ LOG(ERROR) << "Error populating user info: " << error;
+ return false;
+ }
+ }
+
+ std::string auth_code;
+ url += "/finalize?key=" + api_key_;
+ do {
+ LOG(INFO) << "Sending request to: " << url;
+ response = PostBinary(url, nullptr, 0);
+ if (response) {
+ if (response->GetStatusCode() == status_code::BadRequest)
+ sleep(1);
+ }
+ }
+ while (response &&
+ response->GetStatusCode() == status_code::BadRequest);
+ if (response &&
+ response->GetStatusCode() == status_code::Ok) {
+ auto json_resp = ParseJsonResponse(response.get(), nullptr, nullptr);
+ if (json_resp &&
+ json_resp->GetString("robotAccountEmail", &device_robot_account_) &&
+ json_resp->GetString("robotAccountAuthorizationCode", &auth_code) &&
+ json_resp->GetString("deviceDraft.id", &device_id_)) {
+ // Now get access_token and refresh_token
+ response = 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"}
+ });
+ if (!response)
+ return false;
+
+ json_resp = ParseOAuthResponse(response.get(), nullptr);
+ 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) {
+ LOG(ERROR) << "Access token unavailable";
+ return false;
+ }
+
+ access_token_expiration_ = base::Time::Now() +
+ base::TimeDelta::FromSeconds(expires_in);
+
+ Save();
+ }
+ }
+ return true;
+}
+
+} // namespace buffet