blob: 15d3dc4e4b9170530902a6a22f30363cf6d93355 [file] [log] [blame]
Alex Vakulenko3cb466c2014-04-15 11:36:32 -07001// Copyright 2014 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "buffet/device_registration_info.h"
6
Christopher Wiley006e94e2014-05-02 13:44:48 -07007#include <memory>
Alex Vakulenko9ea5a322015-04-17 15:35:34 -07008#include <set>
Alex Vakulenkob3aac252014-05-07 17:35:24 -07009#include <utility>
10#include <vector>
Christopher Wiley006e94e2014-05-02 13:44:48 -070011
Christopher Wileycd419662015-02-06 17:51:43 -080012#include <base/bind.h>
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070013#include <base/json/json_writer.h>
Anton Muhinac661ab2014-10-03 20:29:48 +040014#include <base/message_loop/message_loop.h>
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070015#include <base/values.h>
Anton Muhinac661ab2014-10-03 20:29:48 +040016#include <chromeos/bind_lambda.h>
Alex Vakulenkoe4879a22014-08-20 15:47:36 -070017#include <chromeos/data_encoding.h>
Anton Muhin233d2ee2014-10-22 15:16:24 +040018#include <chromeos/errors/error_codes.h>
Alex Vakulenkoa8b95bc2014-08-27 11:00:57 -070019#include <chromeos/http/http_utils.h>
Anton Muhin332df192014-11-22 05:59:14 +040020#include <chromeos/key_value_store.h>
Alex Vakulenko3aeea1c2014-08-20 16:33:12 -070021#include <chromeos/mime_utils.h>
Alex Vakulenkoa8b95bc2014-08-27 11:00:57 -070022#include <chromeos/strings/string_utils.h>
Alex Vakulenkobd5b5442014-08-20 16:16:34 -070023#include <chromeos/url_utils.h>
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070024
Anton Muhin59755522014-11-05 21:30:12 +040025#include "buffet/commands/cloud_command_proxy.h"
Alex Vakulenko45109442014-07-29 11:07:10 -070026#include "buffet/commands/command_definition.h"
27#include "buffet/commands/command_manager.h"
Alex Vakulenkof784e212015-04-20 12:33:52 -070028#include "buffet/commands/schema_constants.h"
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -070029#include "buffet/notification/xmpp_channel.h"
Alex Vakulenko07216fe2014-09-19 15:31:09 -070030#include "buffet/states/state_manager.h"
Alex Vakulenkob04936f2014-09-19 14:53:58 -070031#include "buffet/utils.h"
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070032
Alex Vakulenkob3aac252014-05-07 17:35:24 -070033const char buffet::kErrorDomainOAuth2[] = "oauth2";
34const char buffet::kErrorDomainGCD[] = "gcd";
35const char buffet::kErrorDomainGCDServer[] = "gcd_server";
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070036
Alex Vakulenko8e34d392014-04-29 11:02:56 -070037namespace {
38
Christopher Wileyba983c82015-03-05 16:32:23 -080039const int kMaxStartDeviceRetryDelayMinutes{1};
40const int64_t kMinStartDeviceRetryDelaySeconds{5};
Alex Vakulenkod1978d32015-04-29 17:33:26 -070041const int64_t kAbortCommandRetryDelaySeconds{5};
Christopher Wileyba983c82015-03-05 16:32:23 -080042
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070043std::pair<std::string, std::string> BuildAuthHeader(
44 const std::string& access_token_type,
45 const std::string& access_token) {
Alex Vakulenkoaf23b322014-05-08 16:25:45 -070046 std::string authorization =
Vitaly Bukadb770e72015-03-10 19:33:33 -070047 chromeos::string_utils::Join(" ", access_token_type, access_token);
Alex Vakulenkocca20932014-08-20 17:35:12 -070048 return {chromeos::http::request_header::kAuthorization, authorization};
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070049}
50
Alex Vakulenko5f472062014-08-14 17:54:04 -070051inline void SetUnexpectedError(chromeos::ErrorPtr* error) {
Alex Vakulenkoac8037d2014-11-11 11:42:05 -080052 chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainGCD,
53 "unexpected_response", "Unexpected GCD error");
Alex Vakulenkob3aac252014-05-07 17:35:24 -070054}
55
Alex Vakulenko5f472062014-08-14 17:54:04 -070056void ParseGCDError(const base::DictionaryValue* json,
57 chromeos::ErrorPtr* error) {
Alex Vakulenkob3aac252014-05-07 17:35:24 -070058 const base::Value* list_value = nullptr;
59 const base::ListValue* error_list = nullptr;
60 if (!json->Get("error.errors", &list_value) ||
61 !list_value->GetAsList(&error_list)) {
62 SetUnexpectedError(error);
63 return;
64 }
65
66 for (size_t i = 0; i < error_list->GetSize(); i++) {
67 const base::Value* error_value = nullptr;
68 const base::DictionaryValue* error_object = nullptr;
69 if (!error_list->Get(i, &error_value) ||
70 !error_value->GetAsDictionary(&error_object)) {
71 SetUnexpectedError(error);
72 continue;
73 }
74 std::string error_code, error_message;
75 if (error_object->GetString("reason", &error_code) &&
76 error_object->GetString("message", &error_message)) {
Alex Vakulenkoac8037d2014-11-11 11:42:05 -080077 chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainGCDServer,
Alex Vakulenko5f472062014-08-14 17:54:04 -070078 error_code, error_message);
Alex Vakulenkob3aac252014-05-07 17:35:24 -070079 } else {
80 SetUnexpectedError(error);
81 }
82 }
83}
84
Alex Vakulenkobda220a2014-04-18 15:25:44 -070085std::string BuildURL(const std::string& url,
86 const std::vector<std::string>& subpaths,
Alex Vakulenkoe4879a22014-08-20 15:47:36 -070087 const chromeos::data_encoding::WebParamList& params) {
Alex Vakulenkobd5b5442014-08-20 16:16:34 -070088 std::string result = chromeos::url::CombineMultiple(url, subpaths);
89 return chromeos::url::AppendQueryParams(result, params);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -070090}
91
Alex Vakulenko0357c032015-01-06 16:32:31 -080092void IgnoreCloudError(const chromeos::Error*) {
Anton Muhin5191e812014-10-30 17:49:48 +040093}
94
Christopher Wileyba983c82015-03-05 16:32:23 -080095void IgnoreCloudErrorWithCallback(const base::Closure& cb,
96 const chromeos::Error*) {
97 cb.Run();
98}
99
Anton Muhin5191e812014-10-30 17:49:48 +0400100void IgnoreCloudResult(const base::DictionaryValue&) {
101}
Anton Muhin6d2569e2014-10-30 12:32:27 +0400102
Christopher Wileyba983c82015-03-05 16:32:23 -0800103void IgnoreCloudResultWithCallback(const base::Closure& cb,
104 const base::DictionaryValue&) {
105 cb.Run();
106}
107
Alex Vakulenko8e34d392014-04-29 11:02:56 -0700108} // anonymous namespace
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700109
110namespace buffet {
Alex Vakulenko8e34d392014-04-29 11:02:56 -0700111
Alex Vakulenko1f30a622014-07-23 11:13:15 -0700112DeviceRegistrationInfo::DeviceRegistrationInfo(
Alex Vakulenko07216fe2014-09-19 15:31:09 -0700113 const std::shared_ptr<CommandManager>& command_manager,
Anton Muhin332df192014-11-22 05:59:14 +0400114 const std::shared_ptr<StateManager>& state_manager,
Vitaly Buka867b0882015-04-16 10:03:26 -0700115 std::unique_ptr<BuffetConfig> config,
Alex Vakulenkocca20932014-08-20 17:35:12 -0700116 const std::shared_ptr<chromeos::http::Transport>& transport,
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700117 bool notifications_enabled)
Alex Vakulenko07216fe2014-09-19 15:31:09 -0700118 : transport_{transport},
Alex Vakulenko07216fe2014-09-19 15:31:09 -0700119 command_manager_{command_manager},
Anton Muhin332df192014-11-22 05:59:14 +0400120 state_manager_{state_manager},
Christopher Wiley583d64b2015-03-24 14:30:17 -0700121 config_{std::move(config)},
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700122 notifications_enabled_{notifications_enabled} {
Vitaly Buka5e6ff6c2015-05-11 15:41:33 -0700123 command_manager_->AddOnCommandDefChanged(
Alex Vakulenko9ea5a322015-04-17 15:35:34 -0700124 base::Bind(&DeviceRegistrationInfo::OnCommandDefsChanged,
125 weak_factory_.GetWeakPtr()));
Vitaly Bukac903d282015-05-26 17:03:08 -0700126 state_manager_->AddOnChangedCallback(
127 base::Bind(&DeviceRegistrationInfo::OnStateChanged,
128 weak_factory_.GetWeakPtr()));
Alex Vakulenkoa3062c52014-04-21 17:05:51 -0700129}
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700130
Anton Muhin332df192014-11-22 05:59:14 +0400131DeviceRegistrationInfo::~DeviceRegistrationInfo() = default;
132
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700133std::pair<std::string, std::string>
134 DeviceRegistrationInfo::GetAuthorizationHeader() const {
Alex Vakulenko8e34d392014-04-29 11:02:56 -0700135 return BuildAuthHeader("Bearer", access_token_);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700136}
137
138std::string DeviceRegistrationInfo::GetServiceURL(
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700139 const std::string& subpath,
Alex Vakulenkoe4879a22014-08-20 15:47:36 -0700140 const chromeos::data_encoding::WebParamList& params) const {
Christopher Wiley583d64b2015-03-24 14:30:17 -0700141 return BuildURL(config_->service_url(), {subpath}, params);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700142}
143
144std::string DeviceRegistrationInfo::GetDeviceURL(
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700145 const std::string& subpath,
Alex Vakulenkoe4879a22014-08-20 15:47:36 -0700146 const chromeos::data_encoding::WebParamList& params) const {
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700147 CHECK(!config_->device_id().empty()) << "Must have a valid device ID";
Christopher Wiley583d64b2015-03-24 14:30:17 -0700148 return BuildURL(config_->service_url(),
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700149 {"devices", config_->device_id(), subpath}, params);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700150}
151
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700152std::string DeviceRegistrationInfo::GetOAuthURL(
153 const std::string& subpath,
Alex Vakulenkoe4879a22014-08-20 15:47:36 -0700154 const chromeos::data_encoding::WebParamList& params) const {
Christopher Wiley583d64b2015-03-24 14:30:17 -0700155 return BuildURL(config_->oauth_url(), {subpath}, params);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700156}
157
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700158void DeviceRegistrationInfo::Start() {
Christopher Wileyba983c82015-03-05 16:32:23 -0800159 if (HaveRegistrationCredentials(nullptr)) {
Christopher Wileyba983c82015-03-05 16:32:23 -0800160 // Wait a significant amount of time for local daemons to publish their
161 // state to Buffet before publishing it to the cloud.
162 // TODO(wiley) We could do a lot of things here to either expose this
163 // timeout as a configurable knob or allow local
164 // daemons to signal that their state is up to date so that
165 // we need not wait for them.
166 ScheduleStartDevice(base::TimeDelta::FromSeconds(5));
167 }
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700168}
169
Christopher Wileycd419662015-02-06 17:51:43 -0800170void DeviceRegistrationInfo::ScheduleStartDevice(const base::TimeDelta& later) {
Vitaly Bukab055f152015-03-12 13:41:43 -0700171 SetRegistrationStatus(RegistrationStatus::kConnecting);
Christopher Wileycd419662015-02-06 17:51:43 -0800172 base::MessageLoop* current = base::MessageLoop::current();
173 if (!current)
174 return; // Assume we're in unittests
Christopher Wileyba983c82015-03-05 16:32:23 -0800175 base::TimeDelta max_delay =
176 base::TimeDelta::FromMinutes(kMaxStartDeviceRetryDelayMinutes);
177 base::TimeDelta min_delay =
178 base::TimeDelta::FromSeconds(kMinStartDeviceRetryDelaySeconds);
179 base::TimeDelta retry_delay = later * 2;
180 if (retry_delay > max_delay) { retry_delay = max_delay; }
181 if (retry_delay < min_delay) { retry_delay = min_delay; }
Christopher Wileycd419662015-02-06 17:51:43 -0800182 current->PostDelayedTask(
183 FROM_HERE,
184 base::Bind(&DeviceRegistrationInfo::StartDevice,
Christopher Wileyba983c82015-03-05 16:32:23 -0800185 weak_factory_.GetWeakPtr(), nullptr,
186 retry_delay),
Christopher Wileycd419662015-02-06 17:51:43 -0800187 later);
188}
189
Alex Vakulenko5f472062014-08-14 17:54:04 -0700190bool DeviceRegistrationInfo::CheckRegistration(chromeos::ErrorPtr* error) {
Christopher Wileyc900e482015-02-15 15:42:04 -0800191 return HaveRegistrationCredentials(error) &&
David Zeuthen390d1912015-03-03 14:54:48 -0500192 MaybeRefreshAccessToken(error);
Christopher Wileyc900e482015-02-15 15:42:04 -0800193}
194
195bool DeviceRegistrationInfo::HaveRegistrationCredentials(
196 chromeos::ErrorPtr* error) {
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700197 const bool have_credentials = !config_->refresh_token().empty() &&
198 !config_->device_id().empty() &&
199 !config_->robot_account().empty();
Christopher Wileyc900e482015-02-15 15:42:04 -0800200
201 VLOG(1) << "Device registration record "
202 << ((have_credentials) ? "found" : "not found.");
203 if (!have_credentials)
Alex Vakulenkoac8037d2014-11-11 11:42:05 -0800204 chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainGCD,
205 "device_not_registered",
Alex Vakulenko5f472062014-08-14 17:54:04 -0700206 "No valid device registration record found");
Christopher Wileyc900e482015-02-15 15:42:04 -0800207 return have_credentials;
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700208}
209
Nathan Bullock24d189f2015-02-26 13:09:18 -0500210std::unique_ptr<base::DictionaryValue>
Alex Vakulenkof71cca62015-04-09 15:17:17 -0700211DeviceRegistrationInfo::ParseOAuthResponse(chromeos::http::Response* response,
212 chromeos::ErrorPtr* error) {
Nathan Bullock24d189f2015-02-26 13:09:18 -0500213 int code = 0;
214 auto resp = chromeos::http::ParseJsonResponse(response, &code, error);
215 if (resp && code >= chromeos::http::status_code::BadRequest) {
216 std::string error_code, error_message;
217 if (!resp->GetString("error", &error_code)) {
218 error_code = "unexpected_response";
219 }
220 if (error_code == "invalid_grant") {
221 LOG(INFO) << "The device's registration has been revoked.";
222 SetRegistrationStatus(RegistrationStatus::kInvalidCredentials);
223 }
224 // I have never actually seen an error_description returned.
225 if (!resp->GetString("error_description", &error_message)) {
226 error_message = "Unexpected OAuth error";
227 }
228 chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainOAuth2,
229 error_code, error_message);
230 return std::unique_ptr<base::DictionaryValue>();
231 }
232 return resp;
233}
234
David Zeuthen390d1912015-03-03 14:54:48 -0500235bool DeviceRegistrationInfo::MaybeRefreshAccessToken(
Alex Vakulenko5f472062014-08-14 17:54:04 -0700236 chromeos::ErrorPtr* error) {
Alex Vakulenko8e34d392014-04-29 11:02:56 -0700237 LOG(INFO) << "Checking access token expiration.";
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700238 if (!access_token_.empty() &&
239 !access_token_expiration_.is_null() &&
240 access_token_expiration_ > base::Time::Now()) {
241 LOG(INFO) << "Access token is still valid.";
242 return true;
243 }
David Zeuthen390d1912015-03-03 14:54:48 -0500244 return RefreshAccessToken(error);
245}
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700246
David Zeuthen390d1912015-03-03 14:54:48 -0500247bool DeviceRegistrationInfo::RefreshAccessToken(
248 chromeos::ErrorPtr* error) {
249 LOG(INFO) << "Refreshing access token.";
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700250 auto response = chromeos::http::PostFormDataAndBlock(
251 GetOAuthURL("token"),
252 {
253 {"refresh_token", config_->refresh_token()},
254 {"client_id", config_->client_id()},
255 {"client_secret", config_->client_secret()},
256 {"grant_type", "refresh_token"},
257 },
258 {}, transport_, error);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700259 if (!response)
260 return false;
261
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700262 auto json = ParseOAuthResponse(response.get(), error);
263 if (!json)
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700264 return false;
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700265
266 int expires_in = 0;
267 if (!json->GetString("access_token", &access_token_) ||
268 !json->GetInteger("expires_in", &expires_in) ||
269 access_token_.empty() ||
270 expires_in <= 0) {
271 LOG(ERROR) << "Access token unavailable.";
Alex Vakulenkoac8037d2014-11-11 11:42:05 -0800272 chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainOAuth2,
Alex Vakulenko5f472062014-08-14 17:54:04 -0700273 "unexpected_server_response",
274 "Access token unavailable");
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700275 return false;
276 }
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700277 access_token_expiration_ = base::Time::Now() +
278 base::TimeDelta::FromSeconds(expires_in);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700279 LOG(INFO) << "Access token is refreshed for additional " << expires_in
280 << " seconds.";
Nathan Bullockd9e0bcd2015-02-11 11:36:39 -0500281
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700282 StartNotificationChannel();
Nathan Bullockd9e0bcd2015-02-11 11:36:39 -0500283
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700284 return true;
285}
286
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700287void DeviceRegistrationInfo::StartNotificationChannel() {
288 if (!notifications_enabled_) {
289 LOG(WARNING) << "Notification support disabled by flag.";
Christopher Wileyd732bd02015-04-07 11:11:18 -0700290 return;
291 }
Nathan Bullockbea91132015-02-19 09:13:33 -0500292 // If no MessageLoop assume we're in unittests.
293 if (!base::MessageLoop::current()) {
294 LOG(INFO) << "No MessageLoop, not starting XMPP";
295 return;
296 }
297
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700298 // TODO(avakulenko): Move this into a notification channel factory and out of
299 // this class completely. Also to be added the secondary (poll) notification
300 // channel.
Alex Vakulenko26f557b2015-05-26 16:47:40 -0700301 if (primary_notification_channel_)
302 primary_notification_channel_->Stop();
303
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700304 primary_notification_channel_.reset(
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700305 new XmppChannel{config_->robot_account(),
306 access_token_,
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700307 base::MessageLoop::current()->task_runner()});
308 primary_notification_channel_->Start(this);
Nathan Bullockf12f7f02015-02-20 14:46:53 -0500309}
310
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700311void DeviceRegistrationInfo::AddOnRegistrationChangedCallback(
312 const OnRegistrationChangedCallback& callback) {
313 on_registration_changed_.push_back(callback);
314 callback.Run(registration_status_);
315}
316
Anton Muhind8d32162014-10-02 20:37:00 +0400317std::unique_ptr<base::DictionaryValue>
318DeviceRegistrationInfo::BuildDeviceResource(chromeos::ErrorPtr* error) {
Alex Vakulenko9ea5a322015-04-17 15:35:34 -0700319 // Limit only to commands that are visible to the cloud.
320 auto commands = command_manager_->GetCommandDictionary().GetCommandsAsJson(
321 [](const CommandDefinition* def) { return def->GetVisibility().cloud; },
322 true, error);
Anton Muhind8d32162014-10-02 20:37:00 +0400323 if (!commands)
324 return nullptr;
325
326 std::unique_ptr<base::DictionaryValue> state =
327 state_manager_->GetStateValuesAsJson(error);
328 if (!state)
329 return nullptr;
330
331 std::unique_ptr<base::DictionaryValue> resource{new base::DictionaryValue};
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700332 if (!config_->device_id().empty())
333 resource->SetString("id", config_->device_id());
Vitaly Buka867b0882015-04-16 10:03:26 -0700334 resource->SetString("name", config_->name());
335 if (!config_->description().empty())
336 resource->SetString("description", config_->description());
337 if (!config_->location().empty())
338 resource->SetString("location", config_->location());
339 resource->SetString("modelManifestId", config_->model_id());
Christopher Wiley583d64b2015-03-24 14:30:17 -0700340 resource->SetString("deviceKind", config_->device_kind());
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700341 std::unique_ptr<base::DictionaryValue> channel{new base::DictionaryValue};
342 if (primary_notification_channel_) {
343 channel->SetString("supportedType",
344 primary_notification_channel_->GetName());
345 primary_notification_channel_->AddChannelParameters(channel.get());
346 } else {
347 // TODO(avakulenko): Currently GCD server doesn't support changing supported
348 // channel, so here we cannot use "pull" as supported channel type until
349 // this is fixed. See b/20895223
350 channel->SetString("supportedType", "xmpp");
351 }
352 resource->Set("channel", channel.release());
Anton Muhind8d32162014-10-02 20:37:00 +0400353 resource->Set("commandDefs", commands.release());
354 resource->Set("state", state.release());
355
356 return resource;
357}
358
Vitaly Buka6ca3ad62015-03-11 17:03:23 -0700359std::unique_ptr<base::DictionaryValue> DeviceRegistrationInfo::GetDeviceInfo(
Alex Vakulenko5f472062014-08-14 17:54:04 -0700360 chromeos::ErrorPtr* error) {
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700361 if (!CheckRegistration(error))
Vitaly Buka6ca3ad62015-03-11 17:03:23 -0700362 return std::unique_ptr<base::DictionaryValue>();
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700363
Anton Muhinac661ab2014-10-03 20:29:48 +0400364 // TODO(antonm): Switch to DoCloudRequest later.
Alex Vakulenko89dde2f2015-01-07 12:05:12 -0800365 auto response = chromeos::http::GetAndBlock(
Alex Vakulenkocca20932014-08-20 17:35:12 -0700366 GetDeviceURL(), {GetAuthorizationHeader()}, transport_, error);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700367 int status_code = 0;
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700368 std::unique_ptr<base::DictionaryValue> json =
Alex Vakulenkocca20932014-08-20 17:35:12 -0700369 chromeos::http::ParseJsonResponse(response.get(), &status_code, error);
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700370 if (json) {
Alex Vakulenkocca20932014-08-20 17:35:12 -0700371 if (status_code >= chromeos::http::status_code::BadRequest) {
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700372 LOG(WARNING) << "Failed to retrieve the device info. Response code = "
373 << status_code;
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700374 ParseGCDError(json.get(), error);
Vitaly Buka6ca3ad62015-03-11 17:03:23 -0700375 return std::unique_ptr<base::DictionaryValue>();
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700376 }
377 }
Vitaly Buka6ca3ad62015-03-11 17:03:23 -0700378 return json;
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700379}
380
Vitaly Bukacad2e332015-05-14 23:33:32 -0700381std::string DeviceRegistrationInfo::RegisterDevice(const std::string& ticket_id,
382 chromeos::ErrorPtr* error) {
Anton Muhind8d32162014-10-02 20:37:00 +0400383 std::unique_ptr<base::DictionaryValue> device_draft =
384 BuildDeviceResource(error);
385 if (!device_draft)
Alex Vakulenko07216fe2014-09-19 15:31:09 -0700386 return std::string();
387
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700388 base::DictionaryValue req_json;
Nathan Bullocke4408482015-02-19 11:13:21 -0500389 req_json.SetString("id", ticket_id);
Christopher Wiley583d64b2015-03-24 14:30:17 -0700390 req_json.SetString("oauthClientId", config_->client_id());
Anton Muhind8d32162014-10-02 20:37:00 +0400391 req_json.Set("deviceDraft", device_draft.release());
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700392
Nathan Bullocke4408482015-02-19 11:13:21 -0500393 auto url = GetServiceURL("registrationTickets/" + ticket_id,
Christopher Wiley583d64b2015-03-24 14:30:17 -0700394 {{"key", config_->api_key()}});
Anton Muhin532ff7e2014-09-29 23:21:21 +0400395 std::unique_ptr<chromeos::http::Response> response =
Alex Vakulenko89dde2f2015-01-07 12:05:12 -0800396 chromeos::http::PatchJsonAndBlock(url, &req_json, {}, transport_, error);
Anton Muhin532ff7e2014-09-29 23:21:21 +0400397 auto json_resp = chromeos::http::ParseJsonResponse(response.get(), nullptr,
398 error);
399 if (!json_resp)
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700400 return std::string();
David Zeuthen1dbad472015-02-12 15:24:21 -0500401 if (!response->IsSuccessful()) {
402 ParseGCDError(json_resp.get(), error);
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700403 return std::string();
David Zeuthen1dbad472015-02-12 15:24:21 -0500404 }
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700405
Nathan Bullocke4408482015-02-19 11:13:21 -0500406 url = GetServiceURL("registrationTickets/" + ticket_id +
Christopher Wiley583d64b2015-03-24 14:30:17 -0700407 "/finalize?key=" + config_->api_key());
Alex Vakulenko89dde2f2015-01-07 12:05:12 -0800408 response = chromeos::http::SendRequestWithNoDataAndBlock(
409 chromeos::http::request_type::kPost, url, {}, transport_, error);
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700410 if (!response)
Anton Muhinbeb1c5b2014-10-16 18:59:57 +0400411 return std::string();
412 json_resp = chromeos::http::ParseJsonResponse(response.get(), nullptr, error);
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700413 if (!json_resp)
Anton Muhinbeb1c5b2014-10-16 18:59:57 +0400414 return std::string();
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700415 if (!response->IsSuccessful()) {
416 ParseGCDError(json_resp.get(), error);
Anton Muhinbeb1c5b2014-10-16 18:59:57 +0400417 return std::string();
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700418 }
Anton Muhin532ff7e2014-09-29 23:21:21 +0400419
420 std::string auth_code;
Vitaly Buka620bd7e2015-03-16 01:07:01 -0700421 std::string device_id;
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700422 std::string robot_account;
423 if (!json_resp->GetString("robotAccountEmail", &robot_account) ||
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700424 !json_resp->GetString("robotAccountAuthorizationCode", &auth_code) ||
Vitaly Buka620bd7e2015-03-16 01:07:01 -0700425 !json_resp->GetString("deviceDraft.id", &device_id)) {
Alex Vakulenkoac8037d2014-11-11 11:42:05 -0800426 chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainGCD,
427 "unexpected_response",
Alex Vakulenko5f472062014-08-14 17:54:04 -0700428 "Device account missing in response");
Anton Muhinbeb1c5b2014-10-16 18:59:57 +0400429 return std::string();
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700430 }
431
432 // Now get access_token and refresh_token
Alex Vakulenko89dde2f2015-01-07 12:05:12 -0800433 response = chromeos::http::PostFormDataAndBlock(GetOAuthURL("token"), {
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700434 {"code", auth_code},
Christopher Wiley583d64b2015-03-24 14:30:17 -0700435 {"client_id", config_->client_id()},
436 {"client_secret", config_->client_secret()},
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700437 {"redirect_uri", "oob"},
438 {"scope", "https://www.googleapis.com/auth/clouddevices"},
439 {"grant_type", "authorization_code"}
Alex Vakulenko89dde2f2015-01-07 12:05:12 -0800440 }, {}, transport_, error);
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700441 if (!response)
Anton Muhinbeb1c5b2014-10-16 18:59:57 +0400442 return std::string();
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700443
444 json_resp = ParseOAuthResponse(response.get(), error);
445 int expires_in = 0;
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700446 std::string refresh_token;
447 if (!json_resp || !json_resp->GetString("access_token", &access_token_) ||
448 !json_resp->GetString("refresh_token", &refresh_token) ||
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700449 !json_resp->GetInteger("expires_in", &expires_in) ||
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700450 access_token_.empty() || refresh_token.empty() || expires_in <= 0) {
Alex Vakulenkoac8037d2014-11-11 11:42:05 -0800451 chromeos::Error::AddTo(error, FROM_HERE,
452 kErrorDomainGCD, "unexpected_response",
Alex Vakulenko5f472062014-08-14 17:54:04 -0700453 "Device access_token missing in response");
Anton Muhinbeb1c5b2014-10-16 18:59:57 +0400454 return std::string();
Alex Vakulenkob3aac252014-05-07 17:35:24 -0700455 }
456
457 access_token_expiration_ = base::Time::Now() +
458 base::TimeDelta::FromSeconds(expires_in);
459
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700460 BuffetConfig::Transaction change{config_.get()};
461 change.set_device_id(device_id);
462 change.set_robot_account(robot_account);
463 change.set_refresh_token(refresh_token);
464 change.Commit();
465
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700466 StartNotificationChannel();
Christopher Wileycd419662015-02-06 17:51:43 -0800467
468 // We're going to respond with our success immediately and we'll StartDevice
469 // shortly after.
470 ScheduleStartDevice(base::TimeDelta::FromSeconds(0));
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700471 return device_id;
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700472}
473
Anton Muhin633eded2014-10-03 20:40:10 +0400474namespace {
475
476template <class T>
477void PostToCallback(base::Callback<void(const T&)> callback,
478 std::unique_ptr<T> value) {
479 auto cb = [callback] (T* result) {
480 callback.Run(*result);
481 };
482 base::MessageLoop::current()->PostTask(
483 FROM_HERE, base::Bind(cb, base::Owned(value.release())));
484}
485
Alex Vakulenkoab5f27b2015-04-24 16:42:50 -0700486using ResponsePtr = std::unique_ptr<chromeos::http::Response>;
Anton Muhin633eded2014-10-03 20:40:10 +0400487
Alex Vakulenko0357c032015-01-06 16:32:31 -0800488void SendRequestWithRetries(
Anton Muhin633eded2014-10-03 20:40:10 +0400489 const std::string& method,
490 const std::string& url,
491 const std::string& data,
492 const std::string& mime_type,
493 const chromeos::http::HeaderList& headers,
494 std::shared_ptr<chromeos::http::Transport> transport,
495 int num_retries,
Alex Vakulenko0357c032015-01-06 16:32:31 -0800496 const chromeos::http::SuccessCallback& success_callback,
497 const chromeos::http::ErrorCallback& error_callback) {
498 auto on_failure =
499 [method, url, data, mime_type, headers, transport, num_retries,
Alex Vakulenko6401d012015-01-16 07:40:43 -0800500 success_callback, error_callback](int request_id,
501 const chromeos::Error* error) {
Anton Muhin633eded2014-10-03 20:40:10 +0400502 if (num_retries > 0) {
Alex Vakulenko0357c032015-01-06 16:32:31 -0800503 SendRequestWithRetries(method, url, data, mime_type,
504 headers, transport, num_retries - 1,
505 success_callback, error_callback);
Anton Muhin633eded2014-10-03 20:40:10 +0400506 } else {
Alex Vakulenko6401d012015-01-16 07:40:43 -0800507 error_callback.Run(request_id, error);
Anton Muhin633eded2014-10-03 20:40:10 +0400508 }
509 };
510
Alex Vakulenko0357c032015-01-06 16:32:31 -0800511 auto on_success =
Alex Vakulenko6401d012015-01-16 07:40:43 -0800512 [on_failure, success_callback, error_callback](int request_id,
513 ResponsePtr response) {
Alex Vakulenko0357c032015-01-06 16:32:31 -0800514 int status_code = response->GetStatusCode();
515 if (status_code >= chromeos::http::status_code::Continue &&
516 status_code < chromeos::http::status_code::BadRequest) {
Alex Vakulenkoab5f27b2015-04-24 16:42:50 -0700517 success_callback.Run(request_id, std::move(response));
Anton Muhin633eded2014-10-03 20:40:10 +0400518 return;
519 }
Anton Muhin633eded2014-10-03 20:40:10 +0400520
Alex Vakulenko0357c032015-01-06 16:32:31 -0800521 // TODO(antonm): Should add some useful information to error.
522 LOG(WARNING) << "Request failed. Response code = " << status_code;
Anton Muhin633eded2014-10-03 20:40:10 +0400523
Alex Vakulenko0357c032015-01-06 16:32:31 -0800524 chromeos::ErrorPtr error;
Alex Vakulenkoac8037d2014-11-11 11:42:05 -0800525 chromeos::Error::AddTo(&error, FROM_HERE, chromeos::errors::http::kDomain,
Anton Muhin233d2ee2014-10-22 15:16:24 +0400526 std::to_string(status_code),
527 response->GetStatusText());
Alex Vakulenko0357c032015-01-06 16:32:31 -0800528 if (status_code >= chromeos::http::status_code::InternalServerError &&
529 status_code < 600) {
530 // Request was valid, but server failed, retry.
531 // TODO(antonm): Implement exponential backoff.
532 // TODO(antonm): Reconsider status codes, maybe only some require
533 // retry.
534 // TODO(antonm): Support Retry-After header.
Alex Vakulenko6401d012015-01-16 07:40:43 -0800535 on_failure(request_id, error.get());
Alex Vakulenko0357c032015-01-06 16:32:31 -0800536 } else {
Alex Vakulenko6401d012015-01-16 07:40:43 -0800537 error_callback.Run(request_id, error.get());
Alex Vakulenko0357c032015-01-06 16:32:31 -0800538 }
539 };
540
541 chromeos::http::SendRequest(method, url, data.c_str(), data.size(),
542 mime_type, headers, transport,
543 base::Bind(on_success),
544 base::Bind(on_failure));
Anton Muhin633eded2014-10-03 20:40:10 +0400545}
546
547} // namespace
548
Anton Muhinac661ab2014-10-03 20:29:48 +0400549void DeviceRegistrationInfo::DoCloudRequest(
Anton Muhin233d2ee2014-10-22 15:16:24 +0400550 const std::string& method,
Anton Muhinac661ab2014-10-03 20:29:48 +0400551 const std::string& url,
552 const base::DictionaryValue* body,
Alex Vakulenko0357c032015-01-06 16:32:31 -0800553 const CloudRequestCallback& success_callback,
554 const CloudRequestErrorCallback& error_callback) {
555 // TODO(antonm): Add reauthorization on access token expiration (do not
Anton Muhinac661ab2014-10-03 20:29:48 +0400556 // forget about 5xx when fetching new access token).
557 // TODO(antonm): Add support for device removal.
558
Anton Muhinac661ab2014-10-03 20:29:48 +0400559 std::string data;
560 if (body)
561 base::JSONWriter::Write(body, &data);
562
563 const std::string mime_type{chromeos::mime::AppendParameter(
564 chromeos::mime::application::kJson,
565 chromeos::mime::parameters::kCharset,
566 "utf-8")};
567
Vitaly Bukab055f152015-03-12 13:41:43 -0700568 auto status_cb = base::Bind(&DeviceRegistrationInfo::SetRegistrationStatus,
569 weak_factory_.GetWeakPtr());
570
571 auto request_cb = [success_callback, error_callback, status_cb](
572 int request_id, ResponsePtr response) {
573 status_cb.Run(RegistrationStatus::kConnected);
Anton Muhin633eded2014-10-03 20:40:10 +0400574 chromeos::ErrorPtr error;
Anton Muhinac661ab2014-10-03 20:29:48 +0400575
Anton Muhin633eded2014-10-03 20:40:10 +0400576 std::unique_ptr<base::DictionaryValue> json_resp{
Alex Vakulenko0357c032015-01-06 16:32:31 -0800577 chromeos::http::ParseJsonResponse(response.get(), nullptr, &error)};
Anton Muhin633eded2014-10-03 20:40:10 +0400578 if (!json_resp) {
Alex Vakulenko0357c032015-01-06 16:32:31 -0800579 error_callback.Run(error.get());
Anton Muhin633eded2014-10-03 20:40:10 +0400580 return;
581 }
Anton Muhinac661ab2014-10-03 20:29:48 +0400582
Alex Vakulenko0357c032015-01-06 16:32:31 -0800583 success_callback.Run(*json_resp);
Anton Muhinac661ab2014-10-03 20:29:48 +0400584 };
Anton Muhin633eded2014-10-03 20:40:10 +0400585
Alex Vakulenko6401d012015-01-16 07:40:43 -0800586 auto error_cb =
587 [error_callback](int request_id, const chromeos::Error* error) {
588 error_callback.Run(error);
589 };
590
Anton Muhin233d2ee2014-10-22 15:16:24 +0400591 auto transport = transport_;
Vitaly Bukab055f152015-03-12 13:41:43 -0700592 auto error_callackback_with_reauthorization = base::Bind(
593 [method, url, data, mime_type, transport, request_cb, error_cb,
594 status_cb](DeviceRegistrationInfo* self, int request_id,
595 const chromeos::Error* error) {
596 status_cb.Run(RegistrationStatus::kConnecting);
597 if (error->HasError(
598 chromeos::errors::http::kDomain,
599 std::to_string(chromeos::http::status_code::Denied))) {
600 chromeos::ErrorPtr reauthorization_error;
601 // Forcibly refresh the access token.
602 if (!self->RefreshAccessToken(&reauthorization_error)) {
603 // TODO(antonm): Check if the device has been actually removed.
604 error_cb(request_id, reauthorization_error.get());
605 return;
606 }
607 SendRequestWithRetries(method, url, data, mime_type,
608 {self->GetAuthorizationHeader()}, transport, 7,
609 base::Bind(request_cb), base::Bind(error_cb));
610 } else {
611 error_cb(request_id, error);
612 }
613 },
614 base::Unretained(this));
Anton Muhin233d2ee2014-10-22 15:16:24 +0400615
Alex Vakulenko0357c032015-01-06 16:32:31 -0800616 SendRequestWithRetries(method, url,
617 data, mime_type,
618 {GetAuthorizationHeader()},
619 transport,
620 7,
621 base::Bind(request_cb),
622 error_callackback_with_reauthorization);
Anton Muhinac661ab2014-10-03 20:29:48 +0400623}
624
Christopher Wileyba983c82015-03-05 16:32:23 -0800625void DeviceRegistrationInfo::StartDevice(
626 chromeos::ErrorPtr* error,
627 const base::TimeDelta& retry_delay) {
Nathan Bullock8fb6de72015-02-24 11:58:39 -0500628 if (!HaveRegistrationCredentials(error))
Anton Muhind8d32162014-10-02 20:37:00 +0400629 return;
Christopher Wileyba983c82015-03-05 16:32:23 -0800630 auto handle_start_device_failure_cb = base::Bind(
631 &IgnoreCloudErrorWithCallback,
632 base::Bind(&DeviceRegistrationInfo::ScheduleStartDevice,
633 weak_factory_.GetWeakPtr(),
634 retry_delay));
635 // "Starting" a device just means that we:
636 // 1) push an updated device resource
637 // 2) fetch an initial set of outstanding commands
638 // 3) abort any commands that we've previously marked as "in progress"
639 // or as being in an error state.
640 // 4) Initiate periodic polling for commands.
641 auto periodically_poll_commands_cb = base::Bind(
642 &DeviceRegistrationInfo::PeriodicallyPollCommands,
643 weak_factory_.GetWeakPtr());
644 auto abort_commands_cb = base::Bind(
645 &DeviceRegistrationInfo::AbortLimboCommands,
646 weak_factory_.GetWeakPtr(),
647 periodically_poll_commands_cb);
648 auto fetch_commands_cb = base::Bind(
649 &DeviceRegistrationInfo::FetchCommands,
650 weak_factory_.GetWeakPtr(),
651 abort_commands_cb,
652 handle_start_device_failure_cb);
653 UpdateDeviceResource(fetch_commands_cb, handle_start_device_failure_cb);
Anton Muhinc635c592014-10-28 21:48:08 +0400654}
655
Vitaly Bukafa947062015-04-17 00:41:31 -0700656bool DeviceRegistrationInfo::UpdateDeviceInfo(const std::string& name,
657 const std::string& description,
658 const std::string& location,
659 chromeos::ErrorPtr* error) {
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700660 BuffetConfig::Transaction change{config_.get()};
661 if (!change.set_name(name)) {
662 chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainBuffet,
663 "invalid_parameter", "Empty device name");
Vitaly Bukafa947062015-04-17 00:41:31 -0700664 return false;
665 }
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700666 change.set_description(description);
667 change.set_location(location);
Vitaly Bukaff81db62015-05-14 21:25:45 -0700668 change.Commit();
Vitaly Bukafa947062015-04-17 00:41:31 -0700669
670 if (HaveRegistrationCredentials(nullptr)) {
671 UpdateDeviceResource(base::Bind(&base::DoNothing),
672 base::Bind(&IgnoreCloudError));
673 }
674
675 return true;
676}
677
Vitaly Bukaff81db62015-05-14 21:25:45 -0700678bool DeviceRegistrationInfo::UpdateServiceConfig(
679 const std::string& client_id,
680 const std::string& client_secret,
681 const std::string& api_key,
682 const std::string& oauth_url,
683 const std::string& service_url,
684 chromeos::ErrorPtr* error) {
685 if (HaveRegistrationCredentials(nullptr)) {
686 chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainBuffet,
687 "already_registered",
688 "Unable to change config for registered device");
689 return false;
690 }
691 BuffetConfig::Transaction change{config_.get()};
692 change.set_client_id(client_id);
693 change.set_client_secret(client_secret);
694 change.set_api_key(api_key);
695 change.set_oauth_url(oauth_url);
696 change.set_service_url(service_url);
697 return true;
698}
699
Anton Muhin59755522014-11-05 21:30:12 +0400700void DeviceRegistrationInfo::UpdateCommand(
701 const std::string& command_id,
Alex Vakulenkob211c102015-04-21 11:43:23 -0700702 const base::DictionaryValue& command_patch,
703 const base::Closure& on_success,
704 const base::Closure& on_error) {
Anton Muhin59755522014-11-05 21:30:12 +0400705 DoCloudRequest(
706 chromeos::http::request_type::kPatch,
707 GetServiceURL("commands/" + command_id),
708 &command_patch,
Alex Vakulenkob211c102015-04-21 11:43:23 -0700709 base::Bind(&IgnoreCloudResultWithCallback, on_success),
710 base::Bind(&IgnoreCloudErrorWithCallback, on_error));
Anton Muhin59755522014-11-05 21:30:12 +0400711}
712
Alex Vakulenkod1978d32015-04-29 17:33:26 -0700713void DeviceRegistrationInfo::NotifyCommandAborted(
714 const std::string& command_id,
715 chromeos::ErrorPtr error) {
716 base::DictionaryValue command_patch;
717 command_patch.SetString(commands::attributes::kCommand_State,
718 CommandInstance::kStatusAborted);
719 if (error) {
720 command_patch.SetString(commands::attributes::kCommand_ErrorCode,
721 chromeos::string_utils::Join(":",
722 error->GetDomain(),
723 error->GetCode()));
724 std::vector<std::string> messages;
725 const chromeos::Error* current_error = error.get();
726 while (current_error) {
727 messages.push_back(current_error->GetMessage());
728 current_error = current_error->GetInnerError();
729 }
730 command_patch.SetString(commands::attributes::kCommand_ErrorMessage,
731 chromeos::string_utils::Join(";", messages));
732 }
733 UpdateCommand(command_id,
734 command_patch,
735 base::Bind(&base::DoNothing),
736 base::Bind(&DeviceRegistrationInfo::RetryNotifyCommandAborted,
737 weak_factory_.GetWeakPtr(),
738 command_id, base::Passed(std::move(error))));
739}
740
741void DeviceRegistrationInfo::RetryNotifyCommandAborted(
742 const std::string& command_id,
743 chromeos::ErrorPtr error) {
744 base::MessageLoop::current()->PostDelayedTask(
745 FROM_HERE,
746 base::Bind(&DeviceRegistrationInfo::NotifyCommandAborted,
747 weak_factory_.GetWeakPtr(),
748 command_id, base::Passed(std::move(error))),
749 base::TimeDelta::FromSeconds(kAbortCommandRetryDelaySeconds));
750}
751
Christopher Wileyba983c82015-03-05 16:32:23 -0800752void DeviceRegistrationInfo::UpdateDeviceResource(
753 const base::Closure& on_success,
754 const CloudRequestErrorCallback& on_failure) {
Alex Vakulenko9ea5a322015-04-17 15:35:34 -0700755 VLOG(1) << "Updating GCD server with CDD...";
Anton Muhind8d32162014-10-02 20:37:00 +0400756 std::unique_ptr<base::DictionaryValue> device_resource =
Anton Muhinc635c592014-10-28 21:48:08 +0400757 BuildDeviceResource(nullptr);
Anton Muhind8d32162014-10-02 20:37:00 +0400758 if (!device_resource)
759 return;
760
Anton Muhinc635c592014-10-28 21:48:08 +0400761 DoCloudRequest(
762 chromeos::http::request_type::kPut,
763 GetDeviceURL(),
764 device_resource.get(),
Christopher Wileyba983c82015-03-05 16:32:23 -0800765 base::Bind(&IgnoreCloudResultWithCallback, on_success),
766 on_failure);
Anton Muhinc635c592014-10-28 21:48:08 +0400767}
Anton Muhina34f0d92014-10-03 21:09:40 +0400768
Christopher Wileyba983c82015-03-05 16:32:23 -0800769namespace {
770
771void HandleFetchCommandsResult(
772 const base::Callback<void(const base::ListValue&)>& callback,
773 const base::DictionaryValue& json) {
774 const base::ListValue* commands{nullptr};
775 if (!json.GetList("commands", &commands)) {
776 VLOG(1) << "No commands in the response.";
777 }
778 const base::ListValue empty;
779 callback.Run(commands ? *commands : empty);
780}
781
782} // namespace
783
Anton Muhinc635c592014-10-28 21:48:08 +0400784void DeviceRegistrationInfo::FetchCommands(
Christopher Wileyba983c82015-03-05 16:32:23 -0800785 const base::Callback<void(const base::ListValue&)>& on_success,
786 const CloudRequestErrorCallback& on_failure) {
Anton Muhinc635c592014-10-28 21:48:08 +0400787 DoCloudRequest(
788 chromeos::http::request_type::kGet,
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700789 GetServiceURL("commands/queue", {{"deviceId", config_->device_id()}}),
790 nullptr, base::Bind(&HandleFetchCommandsResult, on_success), on_failure);
Anton Muhinc635c592014-10-28 21:48:08 +0400791}
Anton Muhina34f0d92014-10-03 21:09:40 +0400792
Anton Muhinc635c592014-10-28 21:48:08 +0400793void DeviceRegistrationInfo::AbortLimboCommands(
Christopher Wileyba983c82015-03-05 16:32:23 -0800794 const base::Closure& callback, const base::ListValue& commands) {
Anton Muhinc635c592014-10-28 21:48:08 +0400795 const size_t size{commands.GetSize()};
796 for (size_t i = 0; i < size; ++i) {
797 const base::DictionaryValue* command{nullptr};
798 if (!commands.GetDictionary(i, &command)) {
799 LOG(WARNING) << "No command resource at " << i;
800 continue;
Anton Muhina34f0d92014-10-03 21:09:40 +0400801 }
Anton Muhinc635c592014-10-28 21:48:08 +0400802 std::string command_state;
803 if (!command->GetString("state", &command_state)) {
804 LOG(WARNING) << "Command with no state at " << i;
805 continue;
806 }
807 if (command_state != "error" &&
808 command_state != "inProgress" &&
809 command_state != "paused") {
810 // It's not a limbo command, ignore.
811 continue;
812 }
813 std::string command_id;
814 if (!command->GetString("id", &command_id)) {
815 LOG(WARNING) << "Command with no ID at " << i;
816 continue;
817 }
Anton Muhin6d2569e2014-10-30 12:32:27 +0400818
819 std::unique_ptr<base::DictionaryValue> command_copy{command->DeepCopy()};
820 command_copy->SetString("state", "aborted");
Christopher Wileyba983c82015-03-05 16:32:23 -0800821 // TODO(wiley) We could consider handling this error case more gracefully.
Anton Muhin6d2569e2014-10-30 12:32:27 +0400822 DoCloudRequest(
823 chromeos::http::request_type::kPut,
824 GetServiceURL("commands/" + command_id),
825 command_copy.get(),
Anton Muhin5191e812014-10-30 17:49:48 +0400826 base::Bind(&IgnoreCloudResult), base::Bind(&IgnoreCloudError));
Anton Muhinc635c592014-10-28 21:48:08 +0400827 }
Anton Muhin0ae1fba2014-10-22 19:30:40 +0400828
Anton Muhinc635c592014-10-28 21:48:08 +0400829 base::MessageLoop::current()->PostTask(FROM_HERE, callback);
830}
831
832void DeviceRegistrationInfo::PeriodicallyPollCommands() {
Anton Muhind07e2062014-10-27 10:53:29 +0400833 VLOG(1) << "Poll commands";
Christopher Wiley34eae042015-03-18 10:25:08 -0700834 command_poll_timer_.Start(
Anton Muhind07e2062014-10-27 10:53:29 +0400835 FROM_HERE,
Christopher Wiley34eae042015-03-18 10:25:08 -0700836 base::TimeDelta::FromMilliseconds(config_->polling_period_ms()),
837 base::Bind(&DeviceRegistrationInfo::FetchCommands,
838 base::Unretained(this),
839 base::Bind(&DeviceRegistrationInfo::PublishCommands,
840 base::Unretained(this)),
841 base::Bind(&IgnoreCloudError)));
Anton Muhind07e2062014-10-27 10:53:29 +0400842}
843
844void DeviceRegistrationInfo::PublishCommands(const base::ListValue& commands) {
Alex Vakulenko6e3c30e2015-05-21 17:39:25 -0700845 for (const base::Value* command : commands) {
846 const base::DictionaryValue* command_dict{nullptr};
847 if (!command->GetAsDictionary(&command_dict)) {
848 LOG(WARNING) << "Not a command dictionary: " << *command;
Anton Muhind07e2062014-10-27 10:53:29 +0400849 continue;
850 }
Alex Vakulenko6e3c30e2015-05-21 17:39:25 -0700851 PublishCommand(*command_dict);
852 }
853}
Anton Muhind07e2062014-10-27 10:53:29 +0400854
Alex Vakulenko6e3c30e2015-05-21 17:39:25 -0700855void DeviceRegistrationInfo::PublishCommand(
856 const base::DictionaryValue& command) {
857 std::string command_id;
858 chromeos::ErrorPtr error;
859 auto command_instance = CommandInstance::FromJson(
860 &command, commands::attributes::kCommand_Visibility_Cloud,
861 command_manager_->GetCommandDictionary(), &command_id, &error);
862 if (!command_instance) {
863 LOG(WARNING) << "Failed to parse a command instance: " << command;
864 if (!command_id.empty())
865 NotifyCommandAborted(command_id, std::move(error));
866 return;
867 }
Anton Muhind07e2062014-10-27 10:53:29 +0400868
Alex Vakulenko6e3c30e2015-05-21 17:39:25 -0700869 // TODO(antonm): Properly process cancellation of commands.
870 if (!command_manager_->FindCommand(command_instance->GetID())) {
871 LOG(INFO) << "New command '" << command_instance->GetName()
872 << "' arrived, ID: " << command_instance->GetID();
873 std::unique_ptr<CommandProxyInterface> cloud_proxy{
874 new CloudCommandProxy(command_instance.get(), this)};
875 command_instance->AddProxy(std::move(cloud_proxy));
876 command_manager_->AddCommand(std::move(command_instance));
Anton Muhind07e2062014-10-27 10:53:29 +0400877 }
Anton Muhind8d32162014-10-02 20:37:00 +0400878}
879
Anton Muhinb8315622014-11-20 03:17:05 +0400880void DeviceRegistrationInfo::PublishStateUpdates() {
881 VLOG(1) << "PublishStateUpdates";
882 const std::vector<StateChange> state_changes{
883 state_manager_->GetAndClearRecordedStateChanges()};
884 if (state_changes.empty())
885 return;
886
887 std::unique_ptr<base::ListValue> patches{new base::ListValue};
888 for (const auto& state_change : state_changes) {
889 std::unique_ptr<base::DictionaryValue> patch{new base::DictionaryValue};
Anton Muhin76933fd2014-11-21 21:25:18 +0400890 patch->SetString("timeMs",
891 std::to_string(state_change.timestamp.ToJavaTime()));
Anton Muhinb8315622014-11-20 03:17:05 +0400892
893 std::unique_ptr<base::DictionaryValue> changes{new base::DictionaryValue};
894 for (const auto& pair : state_change.changed_properties) {
895 auto value = pair.second->ToJson(nullptr);
896 if (!value) {
897 return;
898 }
Alex Vakulenko61ad4db2015-01-20 10:50:04 -0800899 // The key in |pair.first| is the full property name in format
900 // "package.property_name", so must use DictionaryValue::Set() instead of
901 // DictionaryValue::SetWithoutPathExpansion to recreate the JSON
902 // property tree properly.
903 changes->Set(pair.first, value.release());
Anton Muhinb8315622014-11-20 03:17:05 +0400904 }
905 patch->Set("patch", changes.release());
906
907 patches->Append(patch.release());
908 }
909
910 base::DictionaryValue body;
Anton Muhin76933fd2014-11-21 21:25:18 +0400911 body.SetString("requestTimeMs",
912 std::to_string(base::Time::Now().ToJavaTime()));
Anton Muhinb8315622014-11-20 03:17:05 +0400913 body.Set("patches", patches.release());
914
915 DoCloudRequest(
916 chromeos::http::request_type::kPost,
917 GetDeviceURL("patchState"),
918 &body,
919 base::Bind(&IgnoreCloudResult), base::Bind(&IgnoreCloudError));
920}
921
Christopher Wileyc900e482015-02-15 15:42:04 -0800922void DeviceRegistrationInfo::SetRegistrationStatus(
923 RegistrationStatus new_status) {
Vitaly Bukafa947062015-04-17 00:41:31 -0700924 VLOG_IF(1, new_status != registration_status_)
925 << "Changing registration status to " << StatusToString(new_status);
Vitaly Bukaee7a3af2015-05-14 16:57:23 -0700926 registration_status_ = new_status;
927 for (const auto& cb : on_registration_changed_)
928 cb.Run(registration_status_);
Christopher Wileyc900e482015-02-15 15:42:04 -0800929}
930
Alex Vakulenko9ea5a322015-04-17 15:35:34 -0700931void DeviceRegistrationInfo::OnCommandDefsChanged() {
932 VLOG(1) << "CommandDefinitionChanged notification received";
933 if (!HaveRegistrationCredentials(nullptr))
934 return;
935
936 UpdateDeviceResource(base::Bind(&base::DoNothing),
937 base::Bind(&IgnoreCloudError));
938}
939
Vitaly Bukac903d282015-05-26 17:03:08 -0700940void DeviceRegistrationInfo::OnStateChanged() {
941 VLOG(1) << "StateChanged notification received";
942 if (!HaveRegistrationCredentials(nullptr))
943 return;
944
945 // TODO(vitalybuka): Integrate BackoffEntry.
946 PublishStateUpdates();
947}
948
Alex Vakulenkoeedf3be2015-05-13 17:52:02 -0700949void DeviceRegistrationInfo::OnConnected(const std::string& channel_name) {
950 LOG(INFO) << "Notification channel successfully established over "
951 << channel_name;
952 // TODO(avakulenko): Notify GCD server of changed supported channel.
953}
954
955void DeviceRegistrationInfo::OnDisconnected() {
956 LOG(INFO) << "Notification channel disconnected";
957 // TODO(avakulenko): Notify GCD server of changed supported channel.
958}
959
960void DeviceRegistrationInfo::OnPermanentFailure() {
961 LOG(ERROR) << "Failed to establish notification channel.";
962}
963
Alex Vakulenko6e3c30e2015-05-21 17:39:25 -0700964void DeviceRegistrationInfo::OnCommandCreated(
965 const base::DictionaryValue& command) {
966 if (!command.empty()) {
967 // GCD spec indicates that the command parameter in notification object
968 // "may be empty if command size is too big".
969 PublishCommand(command);
970 return;
971 }
972 // TODO(avakulenko): If the command was too big to be delivered over a
973 // notification channel, perform a manual poll from the server here.
974}
975
976
Alex Vakulenko3cb466c2014-04-15 11:36:32 -0700977} // namespace buffet