Merge commit '03cd192eceb46f936ad89e6ae04dac130202e18c' into HEAD * commit '03cd192eceb46f936ad89e6ae04dac130202e18c': Add support for the custom colorXY trait Persist kInvalidCredentials state Fixed headers parsing in CurlHttpClient Replace clouddevices with weave in documentation and tests Removed OAuth scope parameter as optional. Change-Id: If6f63c188d28af515714633d7058fc34c5f006f0
diff --git a/examples/daemon/light/light.cc b/examples/daemon/light/light.cc index 484b9e1..d54de93 100644 --- a/examples/daemon/light/light.cc +++ b/examples/daemon/light/light.cc
@@ -19,12 +19,44 @@ device->AddStateDefinitionsFromJson(R"({ "onOff": {"state": ["on", "standby"]}, - "brightness": {"brightness": "integer"} + "brightness": {"brightness": "integer"}, + "colorXY": { + "colorSetting": { + "properties": { + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} + } + }, + "colorCapRed": { + "properties": { + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} + } + }, + "colorCapGreen": { + "properties": { + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} + } + }, + "colorCapBlue": { + "properties": { + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} + } + } + } })"); device->SetStatePropertiesFromJson(R"({ "onOff":{"state": "standby"}, - "brightness":{"brightness": 0} + "brightness":{"brightness": 0}, + "colorXY": { + "colorSetting": {"colorX": 0, "colorY": 0}, + "colorCapRed": {"colorX": 0.674, "colorY": 0.322}, + "colorCapGreen":{"colorX": 0.408, "colorY": 0.517}, + "colorCapBlue": {"colorX": 0.168, "colorY": 0.041} + } })", nullptr); @@ -46,6 +78,28 @@ } } } + }, + "_colorXY": { + "_setConfig": { + "minimalRole": "user", + "parameters": { + "_colorSetting": { + "type": "object", + "properties": { + "_colorX": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "_colorY": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + } + } + } } })"); device->AddCommandHandler("onOff.setConfig", @@ -54,6 +108,9 @@ device->AddCommandHandler("brightness.setConfig", base::Bind(&LightHandler::OnBrightnessSetConfig, weak_ptr_factory_.GetWeakPtr())); + device->AddCommandHandler("_colorXY._setConfig", + base::Bind(&LightHandler::OnColorXYSetConfig, + weak_ptr_factory_.GetWeakPtr())); } private: @@ -105,11 +162,51 @@ cmd->Abort(error.get(), nullptr); } + void OnColorXYSetConfig(const std::weak_ptr<weave::Command>& command) { + auto cmd = command.lock(); + if (!cmd) + return; + LOG(INFO) << "received command: " << cmd->GetName(); + auto params = cmd->GetParameters(); + base::DictionaryValue* colorXY = nullptr; + if (params->GetDictionary("_colorSetting", &colorXY)) { + bool updateState = false; + double X = 0.0; + double Y = 0.0; + if (colorXY->GetDouble("_colorX", &X)) { + color_X_ = X; + updateState = true; + } + + if (colorXY->GetDouble("_colorY", &Y)) { + color_Y_ = Y; + updateState = true; + } + + if (updateState) + UpdateLightState(); + + cmd->Complete({}, nullptr); + return; + } + + weave::ErrorPtr error; + weave::Error::AddTo(&error, FROM_HERE, "example", "invalid_parameter_value", + "Invalid parameters"); + cmd->Abort(error.get(), nullptr); + } + void UpdateLightState() { base::DictionaryValue state; state.SetString("onOff.state", light_status_ ? "on" : "standby"); state.SetInteger("brightness.brightness", brightness_state_); + + std::unique_ptr<base::DictionaryValue> colorXY(new base::DictionaryValue()); + colorXY->SetDouble("colorX", color_X_); + colorXY->SetDouble("colorY", color_Y_); + state.Set("colorXY.colorSetting", colorXY.get()); device_->SetStateProperties(state, nullptr); + colorXY.release(); } weave::Device* device_{nullptr}; @@ -117,6 +214,8 @@ // Simulate the state of the light. bool light_status_; int32_t brightness_state_; + double color_X_{0.0}; + double color_Y_{0.0}; base::WeakPtrFactory<LightHandler> weak_ptr_factory_{this}; };
diff --git a/examples/provider/curl_http_client.cc b/examples/provider/curl_http_client.cc index 32aa4af..774c07b 100644 --- a/examples/provider/curl_http_client.cc +++ b/examples/provider/curl_http_client.cc
@@ -34,6 +34,27 @@ return size * nmemb; } +size_t HeaderFunction(void* contents, size_t size, size_t nmemb, void* userp) { + std::string header(static_cast<const char*>(contents), size * nmemb); + auto pos = header.find(':'); + if (pos != std::string::npos) { + std::pair<std::string, std::string> header_pair; + + static const char kSpaces[] = " \t\r\n"; + header_pair.first = header.substr(0, pos); + pos = header.find_first_not_of(kSpaces, pos + 1); + if (pos != std::string::npos) { + auto last_non_space = header.find_last_not_of(kSpaces); + if (last_non_space >= pos) + header_pair.second = header.substr(pos, last_non_space - pos + 1); + } + + static_cast<provider::HttpClient::Headers*>(userp) + ->emplace_back(std::move(header_pair)); + } + return size * nmemb; +} + std::pair<std::unique_ptr<CurlHttpClient::Response>, ErrorPtr> SendRequestBlocking(CurlHttpClient::Method method, const std::string& url, @@ -76,9 +97,10 @@ CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response->data)); CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, - &WriteFunction)); - CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, - &response->content_type)); + &HeaderFunction)); + provider::HttpClient::Headers response_headers; + CHECK_EQ(CURLE_OK, + curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &response_headers)); CURLcode res = curl_easy_perform(curl.get()); if (chunk) @@ -91,20 +113,16 @@ return {nullptr, std::move(error)}; } - const std::string kContentType = "\r\nContent-Type:"; - auto pos = response->content_type.find(kContentType); - if (pos == std::string::npos) { + for (const auto& header : response_headers) { + if (header.first == "Content-Type") + response->content_type = header.second; + } + + if (response->content_type.empty()) { Error::AddTo(&error, FROM_HERE, "curl", "no_content_header", "Content-Type header is missing"); return {nullptr, std::move(error)}; } - pos += kContentType.size(); - auto pos_end = response->content_type.find("\r\n", pos); - if (pos_end == std::string::npos) { - pos_end = response->content_type.size(); - } - - response->content_type = response->content_type.substr(pos, pos_end); CHECK_EQ(CURLE_OK, curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response->status));
diff --git a/include/weave/device.h b/include/weave/device.h index e761e0c..19012b5 100644 --- a/include/weave/device.h +++ b/include/weave/device.h
@@ -24,7 +24,7 @@ // States of Gcd connection. enum class GcdState { - kUnconfigured, // We have no credentials. + kUnconfigured, // Device was not registered. kConnecting, // We have credentials but not yet connected. kConnected, // We're registered and connected to the cloud. kInvalidCredentials, // Our registration has been revoked.
diff --git a/src/device_registration_info.cc b/src/device_registration_info.cc index d2b10a9..751e530 100644 --- a/src/device_registration_info.cc +++ b/src/device_registration_info.cc
@@ -201,9 +201,9 @@ SplitAtFirst(response.GetContentType(), ";", true).first; if (content_type != http::kJson && content_type != http::kPlain) { - Error::AddTo(error, FROM_HERE, errors::json::kDomain, - "non_json_content_type", - "Unexpected response content type: " + content_type); + Error::AddTo( + error, FROM_HERE, errors::json::kDomain, "non_json_content_type", + "Unexpected content type: \'" + response.GetContentType() + "\'"); return std::unique_ptr<base::DictionaryValue>(); } @@ -262,6 +262,11 @@ cloud_backoff_entry_.reset(new BackoffEntry{cloud_backoff_policy_.get()}); oauth2_backoff_entry_.reset(new BackoffEntry{cloud_backoff_policy_.get()}); + bool revoked = + !GetSettings().cloud_id.empty() && !HaveRegistrationCredentials(); + gcd_state_ = + revoked ? GcdState::kInvalidCredentials : GcdState::kUnconfigured; + command_manager_->AddCommandDefChanged( base::Bind(&DeviceRegistrationInfo::OnCommandDefsChanged, weak_factory_.GetWeakPtr())); @@ -511,9 +516,8 @@ void DeviceRegistrationInfo::GetDeviceInfo( const CloudRequestDoneCallback& callback) { ErrorPtr error; - if (!VerifyRegistrationCredentials(&error)) { + if (!VerifyRegistrationCredentials(&error)) return callback.Run({}, std::move(error)); - } DoCloudRequest(HttpClient::Method::kGet, GetDeviceURL(), nullptr, callback); } @@ -675,9 +679,8 @@ // TODO(antonm): Add support for device removal. ErrorPtr error; - if (!VerifyRegistrationCredentials(&error)) { + if (!VerifyRegistrationCredentials(&error)) return data->callback.Run({}, std::move(error)); - } if (cloud_backoff_entry_->ShouldRejectRequest()) { VLOG(1) << "Cloud request delayed for " @@ -762,13 +765,13 @@ void DeviceRegistrationInfo::CheckAccessTokenError(ErrorPtr error) { if (error && error->HasError(kErrorDomainOAuth2, "invalid_grant")) - MarkDeviceUnregistered(); + RemoveCredentials(); } void DeviceRegistrationInfo::ConnectToCloud(ErrorPtr error) { if (error) { if (error->HasError(kErrorDomainOAuth2, "invalid_grant")) - MarkDeviceUnregistered(); + RemoveCredentials(); return; } @@ -1270,10 +1273,10 @@ << cloud_id << "'"; return; } - MarkDeviceUnregistered(); + RemoveCredentials(); } -void DeviceRegistrationInfo::MarkDeviceUnregistered() { +void DeviceRegistrationInfo::RemoveCredentials() { if (!HaveRegistrationCredentials()) return; @@ -1281,7 +1284,7 @@ LOG(INFO) << "Device is unregistered from the cloud. Deleting credentials"; Config::Transaction change{config_.get()}; - change.set_cloud_id(""); + // Keep cloud_id to switch to detect kInvalidCredentials after restart. change.set_robot_account(""); change.set_refresh_token(""); change.Commit();
diff --git a/src/device_registration_info.h b/src/device_registration_info.h index 9d9d21f..1399b37 100644 --- a/src/device_registration_info.h +++ b/src/device_registration_info.h
@@ -110,12 +110,6 @@ // Starts GCD device if credentials available. void Start(); - // Checks whether we have credentials generated during registration. - bool HaveRegistrationCredentials() const; - // Calls HaveRegistrationCredentials() and logs an error if no credentials - // are available. - bool VerifyRegistrationCredentials(ErrorPtr* error) const; - // Updates a command (override from CloudCommandUpdateInterface). void UpdateCommand(const std::string& command_id, const base::DictionaryValue& command_patch, @@ -134,6 +128,12 @@ return weak_factory_.GetWeakPtr(); } + // Checks whether we have credentials generated during registration. + bool HaveRegistrationCredentials() const; + // Calls HaveRegistrationCredentials() and logs an error if no credentials + // are available. + bool VerifyRegistrationCredentials(ErrorPtr* error) const; + // Cause DeviceRegistrationInfo to attempt to connect to cloud server on // its own later. void ScheduleCloudConnection(const base::TimeDelta& delay); @@ -266,7 +266,7 @@ void OnDeviceDeleted(const std::string& cloud_id) override; // Wipes out the device registration information and stops server connections. - void MarkDeviceUnregistered(); + void RemoveCredentials(); void RegisterDeviceError(const DoneCallback& callback, ErrorPtr error); void RegisterDeviceOnTicketSent(
diff --git a/src/device_registration_info_unittest.cc b/src/device_registration_info_unittest.cc index 9edd396..df4a438 100644 --- a/src/device_registration_info_unittest.cc +++ b/src/device_registration_info_unittest.cc
@@ -186,6 +186,10 @@ GcdState GetGcdState() const { return dev_reg_->GetGcdState(); } + bool HaveRegistrationCredentials() const { + return dev_reg_->HaveRegistrationCredentials(); + } + provider::test::FakeTaskRunner task_runner_; provider::test::MockConfigStore config_store_; StrictMock<MockHttpClient> http_client_; @@ -222,14 +226,13 @@ url += "client_id="; url += test_data::kClientId; EXPECT_EQ(url, dev_reg_->GetOAuthURL( - "auth", - {{"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, - {"response_type", "code"}, - {"client_id", test_data::kClientId}})); + "auth", {{"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, + {"response_type", "code"}, + {"client_id", test_data::kClientId}})); } TEST_F(DeviceRegistrationInfoTest, HaveRegistrationCredentials) { - EXPECT_FALSE(dev_reg_->HaveRegistrationCredentials()); + EXPECT_FALSE(HaveRegistrationCredentials()); ReloadSettings(); EXPECT_CALL( @@ -254,7 +257,7 @@ }))); EXPECT_TRUE(RefreshAccessToken(nullptr)); - EXPECT_TRUE(dev_reg_->HaveRegistrationCredentials()); + EXPECT_TRUE(HaveRegistrationCredentials()); } TEST_F(DeviceRegistrationInfoTest, CheckAuthenticationFailure) { @@ -313,6 +316,7 @@ EXPECT_FALSE(RefreshAccessToken(&error)); EXPECT_TRUE(error->HasError(kErrorDomainOAuth2, "invalid_grant")); EXPECT_EQ(GcdState::kInvalidCredentials, GetGcdState()); + EXPECT_EQ(test_data::kDeviceId, dev_reg_->GetSettings().cloud_id); } TEST_F(DeviceRegistrationInfoTest, GetDeviceInfo) {
diff --git a/src/privet/cloud_delegate.cc b/src/privet/cloud_delegate.cc index b0eea41..d612668 100644 --- a/src/privet/cloud_delegate.cc +++ b/src/privet/cloud_delegate.cc
@@ -223,7 +223,8 @@ void OnConfigChanged(const Settings&) { NotifyOnDeviceInfoChanged(); } void OnRegistrationChanged(GcdState status) { - if (status == GcdState::kUnconfigured) { + if (status == GcdState::kUnconfigured || + status == GcdState::kInvalidCredentials) { connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; } else if (status == GcdState::kConnecting) { // TODO(vitalybuka): Find conditions for kOffline.