| // Copyright 2018 The Chromium 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 "components/invalidation/impl/per_user_topic_registration_manager.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <iterator> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/rand_util.h" |
| #include "base/stl_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "components/gcm_driver/instance_id/instance_id_driver.h" |
| #include "components/invalidation/impl/invalidation_switches.h" |
| #include "components/invalidation/public/identity_provider.h" |
| #include "components/invalidation/public/invalidation_util.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| |
| namespace syncer { |
| |
| namespace { |
| |
| const char kTypeRegisteredForInvalidation[] = |
| "invalidation.registered_for_invalidation"; |
| |
| const char kActiveRegistrationToken[] = |
| "invalidation.active_registration_token"; |
| |
| const char kInvalidationRegistrationScope[] = |
| "https://firebaseperusertopics-pa.googleapis.com"; |
| |
| const char kFCMOAuthScope[] = |
| "https://www.googleapis.com/auth/firebase.messaging"; |
| |
| using SubscriptionFinishedCallback = |
| base::OnceCallback<void(Topic topic, |
| Status code, |
| std::string private_topic_name, |
| PerUserTopicRegistrationRequest::RequestType type)>; |
| |
| static const net::BackoffEntry::Policy kBackoffPolicy = { |
| // Number of initial errors (in sequence) to ignore before applying |
| // exponential back-off rules. |
| 0, |
| |
| // Initial delay for exponential back-off in ms. |
| 2000, |
| |
| // Factor by which the waiting time will be multiplied. |
| 2, |
| |
| // Fuzzing percentage. ex: 10% will spread requests randomly |
| // between 90%-100% of the calculated time. |
| 0.2, // 20% |
| |
| // Maximum amount of time we are willing to delay our request in ms. |
| 1000 * 3600 * 4, // 4 hours. |
| |
| // Time to keep an entry from being discarded even when it |
| // has no significant state, -1 to never discard. |
| -1, |
| |
| // Don't use initial delay unless the last request was an error. |
| false, |
| }; |
| |
| } // namespace |
| |
| // static |
| void PerUserTopicRegistrationManager::RegisterProfilePrefs( |
| PrefRegistrySimple* registry) { |
| registry->RegisterDictionaryPref(kTypeRegisteredForInvalidation); |
| registry->RegisterStringPref(kActiveRegistrationToken, std::string()); |
| } |
| |
| struct PerUserTopicRegistrationManager::RegistrationEntry { |
| RegistrationEntry(const Topic& id, |
| SubscriptionFinishedCallback completion_callback, |
| PerUserTopicRegistrationRequest::RequestType type); |
| ~RegistrationEntry(); |
| |
| void RegistrationFinished(const Status& code, |
| const std::string& private_topic_name); |
| void Cancel(); |
| |
| // The object for which this is the status. |
| const Topic topic; |
| SubscriptionFinishedCallback completion_callback; |
| PerUserTopicRegistrationRequest::RequestType type; |
| |
| base::OneShotTimer request_retry_timer_; |
| net::BackoffEntry request_backoff_; |
| |
| std::unique_ptr<PerUserTopicRegistrationRequest> request; |
| |
| DISALLOW_COPY_AND_ASSIGN(RegistrationEntry); |
| }; |
| |
| PerUserTopicRegistrationManager::RegistrationEntry::RegistrationEntry( |
| const Topic& topic, |
| SubscriptionFinishedCallback completion_callback, |
| PerUserTopicRegistrationRequest::RequestType type) |
| : topic(topic), |
| completion_callback(std::move(completion_callback)), |
| type(type), |
| request_backoff_(&kBackoffPolicy) {} |
| |
| PerUserTopicRegistrationManager::RegistrationEntry::~RegistrationEntry() {} |
| |
| void PerUserTopicRegistrationManager::RegistrationEntry::RegistrationFinished( |
| const Status& code, |
| const std::string& topic_name) { |
| if (completion_callback) |
| std::move(completion_callback).Run(topic, code, topic_name, type); |
| } |
| |
| void PerUserTopicRegistrationManager::RegistrationEntry::Cancel() { |
| request_retry_timer_.Stop(); |
| request.reset(); |
| } |
| |
| PerUserTopicRegistrationManager::PerUserTopicRegistrationManager( |
| invalidation::IdentityProvider* identity_provider, |
| PrefService* local_state, |
| network::mojom::URLLoaderFactory* url_loader_factory, |
| const ParseJSONCallback& parse_json, |
| const std::string& project_id) |
| : local_state_(local_state), |
| identity_provider_(identity_provider), |
| request_access_token_backoff_(&kBackoffPolicy), |
| parse_json_(parse_json), |
| url_loader_factory_(url_loader_factory), |
| project_id_(project_id) {} |
| |
| PerUserTopicRegistrationManager::~PerUserTopicRegistrationManager() {} |
| |
| void PerUserTopicRegistrationManager::Init() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| const base::Value* pref_data = |
| local_state_->Get(kTypeRegisteredForInvalidation); |
| std::vector<std::string> keys_to_remove; |
| // Load registered ids from prefs. |
| for (const auto& it : pref_data->DictItems()) { |
| Topic topic = it.first; |
| std::string private_topic_name; |
| if (it.second.GetAsString(&private_topic_name) && |
| !private_topic_name.empty()) { |
| topic_to_private_topic_[topic] = private_topic_name; |
| continue; |
| } |
| // Remove saved pref. |
| keys_to_remove.push_back(topic); |
| } |
| |
| // Delete prefs, which weren't decoded successfully. |
| DictionaryPrefUpdate update(local_state_, kTypeRegisteredForInvalidation); |
| base::DictionaryValue* pref_update = update.Get(); |
| for (const std::string& key : keys_to_remove) { |
| pref_update->RemoveKey(key); |
| } |
| } |
| |
| void PerUserTopicRegistrationManager::UpdateRegisteredTopics( |
| const TopicSet& topics, |
| const std::string& instance_id_token) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| token_ = instance_id_token; |
| DropAllSavedRegistrationsOnTokenChange(instance_id_token); |
| for (const auto& topic : topics) { |
| // If id isn't registered, schedule the registration. |
| if (topic_to_private_topic_.find(topic) == topic_to_private_topic_.end()) { |
| auto it = registration_statuses_.find(topic); |
| if (it != registration_statuses_.end()) |
| it->second->Cancel(); |
| registration_statuses_[topic] = std::make_unique<RegistrationEntry>( |
| topic, |
| base::BindOnce( |
| &PerUserTopicRegistrationManager::RegistrationFinishedForTopic, |
| base::Unretained(this)), |
| PerUserTopicRegistrationRequest::SUBSCRIBE); |
| } |
| } |
| |
| // There is registered topic, which need to be unregistered. |
| // Schedule unregistration and immediately remove from |
| // |topic_to_private_topic_| |
| for (auto it = topic_to_private_topic_.begin(); |
| it != topic_to_private_topic_.end();) { |
| auto topic = it->first; |
| if (topics.find(topic) == topics.end()) { |
| registration_statuses_[topic] = std::make_unique<RegistrationEntry>( |
| topic, |
| base::BindOnce( |
| &PerUserTopicRegistrationManager::RegistrationFinishedForTopic, |
| base::Unretained(this)), |
| PerUserTopicRegistrationRequest::UNSUBSCRIBE); |
| it = topic_to_private_topic_.erase(it); |
| // The descision to unregister from the invalidations for the |topic| was |
| // made, the preferences should be cleaned up immediatelly. |
| DictionaryPrefUpdate update(local_state_, kTypeRegisteredForInvalidation); |
| update->RemoveKey(topic); |
| } else { |
| ++it; |
| } |
| } |
| RequestAccessToken(); |
| } |
| |
| void PerUserTopicRegistrationManager::DoRegistrationUpdate() { |
| for (const auto& registration_status : registration_statuses_) { |
| StartRegistrationRequest(registration_status.first); |
| } |
| } |
| |
| void PerUserTopicRegistrationManager::StartRegistrationRequest( |
| const Topic& topic) { |
| auto it = registration_statuses_.find(topic); |
| if (it == registration_statuses_.end()) { |
| NOTREACHED() << "StartRegistrationRequest called on " << topic |
| << " which is not in the registration map"; |
| return; |
| } |
| PerUserTopicRegistrationRequest::Builder builder; |
| it->second->request.reset(); // Resetting request in case it's running. |
| it->second->request = builder.SetToken(token_) |
| .SetScope(kInvalidationRegistrationScope) |
| .SetPublicTopicName(topic) |
| .SetAuthenticationHeader(base::StringPrintf( |
| "Bearer %s", access_token_.c_str())) |
| .SetProjectId(project_id_) |
| .SetType(it->second->type) |
| .Build(); |
| it->second->request->Start( |
| base::BindOnce(&PerUserTopicRegistrationManager::RegistrationEntry:: |
| RegistrationFinished, |
| base::Unretained(it->second.get())), |
| parse_json_, url_loader_factory_); |
| } |
| |
| void PerUserTopicRegistrationManager::ActOnSuccesfullRegistration( |
| const Topic& topic, |
| const std::string& private_topic_name, |
| PerUserTopicRegistrationRequest::RequestType type) { |
| auto it = registration_statuses_.find(topic); |
| it->second->request_backoff_.InformOfRequest(true); |
| registration_statuses_.erase(it); |
| if (type == PerUserTopicRegistrationRequest::SUBSCRIBE) { |
| DictionaryPrefUpdate update(local_state_, kTypeRegisteredForInvalidation); |
| update->SetKey(topic, base::Value(private_topic_name)); |
| topic_to_private_topic_[topic] = private_topic_name; |
| local_state_->CommitPendingWrite(); |
| } |
| bool all_subscription_completed = true; |
| for (const auto& entry : registration_statuses_) { |
| if (entry.second->type == PerUserTopicRegistrationRequest::SUBSCRIBE) { |
| all_subscription_completed = false; |
| } |
| } |
| // Emit ENABLED once we recovered from failed request. |
| if (all_subscription_completed && |
| base::FeatureList::IsEnabled( |
| invalidation::switches::kFCMInvalidationsConservativeEnabling)) { |
| NotifySubscriptionChannelStateChange(SubscriptionChannelState::ENABLED); |
| } |
| } |
| |
| void PerUserTopicRegistrationManager::ScheduleRequestForRepetition( |
| const Topic& topic) { |
| auto completition_callback = base::BindOnce( |
| &PerUserTopicRegistrationManager::RegistrationFinishedForTopic, |
| base::Unretained(this)); |
| registration_statuses_[topic]->completion_callback = |
| std::move(completition_callback); |
| registration_statuses_[topic]->request_backoff_.InformOfRequest(false); |
| registration_statuses_[topic]->request_retry_timer_.Start( |
| FROM_HERE, |
| registration_statuses_[topic]->request_backoff_.GetTimeUntilRelease(), |
| base::BindRepeating( |
| &PerUserTopicRegistrationManager::StartRegistrationRequest, |
| base::Unretained(this), topic)); |
| } |
| |
| void PerUserTopicRegistrationManager::RegistrationFinishedForTopic( |
| Topic topic, |
| Status code, |
| std::string private_topic_name, |
| PerUserTopicRegistrationRequest::RequestType type) { |
| if (code.IsSuccess()) { |
| ActOnSuccesfullRegistration(topic, private_topic_name, type); |
| } else { |
| auto it = registration_statuses_.find(topic); |
| it->second->request_backoff_.InformOfRequest(false); |
| if (code.IsAuthFailure()) { |
| // Re-request access token and fire registrations again. |
| RequestAccessToken(); |
| } else { |
| // If one of the registration requests failed, emit SUBSCRIPTION_FAILURE. |
| if (type == PerUserTopicRegistrationRequest::SUBSCRIBE && |
| base::FeatureList::IsEnabled( |
| invalidation::switches::kFCMInvalidationsConservativeEnabling)) { |
| NotifySubscriptionChannelStateChange( |
| SubscriptionChannelState::SUBSCRIPTION_FAILURE); |
| } |
| if (!code.ShouldRetry()) { |
| registration_statuses_.erase(it); |
| return; |
| } |
| ScheduleRequestForRepetition(topic); |
| } |
| } |
| } |
| |
| TopicSet PerUserTopicRegistrationManager::GetRegisteredIds() const { |
| TopicSet topics; |
| for (const auto& t : topic_to_private_topic_) |
| topics.insert(t.first); |
| |
| return topics; |
| } |
| |
| void PerUserTopicRegistrationManager::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void PerUserTopicRegistrationManager::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void PerUserTopicRegistrationManager::RequestAccessToken() { |
| // TODO(melandory): Implement traffic optimisation. |
| // * Before sending request to server ask for access token from identity |
| // provider (don't invalidate previous token). |
| // Identity provider will take care of retrieving/caching. |
| // * Only invalidate access token when server didn't accept it. |
| |
| // Only one active request at a time. |
| if (access_token_fetcher_ != nullptr) |
| return; |
| request_access_token_retry_timer_.Stop(); |
| OAuth2TokenService::ScopeSet oauth2_scopes = {kFCMOAuthScope}; |
| // Invalidate previous token, otherwise the identity provider will return the |
| // same token again. |
| identity_provider_->InvalidateAccessToken(oauth2_scopes, access_token_); |
| access_token_.clear(); |
| access_token_fetcher_ = identity_provider_->FetchAccessToken( |
| "fcm_invalidation", oauth2_scopes, |
| base::BindOnce( |
| &PerUserTopicRegistrationManager::OnAccessTokenRequestCompleted, |
| base::Unretained(this))); |
| } |
| |
| void PerUserTopicRegistrationManager::OnAccessTokenRequestCompleted( |
| GoogleServiceAuthError error, |
| std::string access_token) { |
| access_token_fetcher_.reset(); |
| if (error.state() == GoogleServiceAuthError::NONE) |
| OnAccessTokenRequestSucceeded(access_token); |
| else |
| OnAccessTokenRequestFailed(error); |
| } |
| |
| void PerUserTopicRegistrationManager::OnAccessTokenRequestSucceeded( |
| std::string access_token) { |
| // Reset backoff time after successful response. |
| request_access_token_backoff_.Reset(); |
| access_token_ = access_token; |
| // Emit ENABLED when successfully got the token. |
| NotifySubscriptionChannelStateChange(SubscriptionChannelState::ENABLED); |
| DoRegistrationUpdate(); |
| } |
| |
| void PerUserTopicRegistrationManager::OnAccessTokenRequestFailed( |
| GoogleServiceAuthError error) { |
| DCHECK_NE(error.state(), GoogleServiceAuthError::NONE); |
| NotifySubscriptionChannelStateChange( |
| SubscriptionChannelState::ACCESS_TOKEN_FAILURE); |
| request_access_token_backoff_.InformOfRequest(false); |
| request_access_token_retry_timer_.Start( |
| FROM_HERE, request_access_token_backoff_.GetTimeUntilRelease(), |
| base::BindRepeating(&PerUserTopicRegistrationManager::RequestAccessToken, |
| base::Unretained(this))); |
| } |
| |
| void PerUserTopicRegistrationManager::DropAllSavedRegistrationsOnTokenChange( |
| const std::string& instance_id_token) { |
| std::string current_token = local_state_->GetString(kActiveRegistrationToken); |
| if (current_token.empty()) { |
| local_state_->SetString(kActiveRegistrationToken, instance_id_token); |
| return; |
| } |
| if (current_token == instance_id_token) { |
| return; |
| } |
| local_state_->SetString(kActiveRegistrationToken, instance_id_token); |
| DictionaryPrefUpdate update(local_state_, kTypeRegisteredForInvalidation); |
| for (const auto& topic : topic_to_private_topic_) { |
| update->RemoveKey(topic.first); |
| } |
| topic_to_private_topic_.clear(); |
| // TODO(melandory): Figure out if the unsubscribe request should be |
| // sent with the old token. |
| } |
| |
| void PerUserTopicRegistrationManager::NotifySubscriptionChannelStateChange( |
| SubscriptionChannelState state) { |
| // NOT_STARTED is the default state of the subscription |
| // channel and shouldn't explicitly issued. |
| DCHECK(state != SubscriptionChannelState::NOT_STARTED); |
| if (last_issued_state_ == state) { |
| // Notify only on state change. |
| return; |
| } |
| |
| last_issued_state_ = state; |
| for (auto& observer : observers_) { |
| observer.OnSubscriptionChannelStateChanged(state); |
| } |
| } |
| |
| base::DictionaryValue PerUserTopicRegistrationManager::CollectDebugData() |
| const { |
| base::DictionaryValue status; |
| for (const auto& topic_to_private_topic : topic_to_private_topic_) { |
| status.SetString(topic_to_private_topic.first, |
| topic_to_private_topic.second); |
| } |
| status.SetString("Instance id token", token_); |
| return status; |
| } |
| |
| } // namespace syncer |