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