blob: a79a79353370a1ef55e58dff2f10b3b80eac805e [file] [log] [blame]
// 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/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/stl_util.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/values.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 kTypeRegisteredForInvalidationsDeprecated[] =
"invalidation.registered_for_invalidation";
const char kTypeRegisteredForInvalidations[] =
"invalidation.per_sender_registered_for_invalidation";
const char kActiveRegistrationTokenDeprecated[] =
"invalidation.active_registration_token";
const char kActiveRegistrationTokens[] =
"invalidation.per_sender_active_registration_tokens";
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,
};
class PerProjectDictionaryPrefUpdate {
public:
explicit PerProjectDictionaryPrefUpdate(PrefService* prefs,
const std::string& project_id)
: update_(prefs, kTypeRegisteredForInvalidations) {
per_sender_pref_ = update_->FindDictKey(project_id);
if (!per_sender_pref_) {
update_->SetDictionary(project_id,
std::make_unique<base::DictionaryValue>());
per_sender_pref_ = update_->FindDictKey(project_id);
}
DCHECK(per_sender_pref_);
}
base::Value& operator*() { return *per_sender_pref_; }
base::Value* operator->() { return per_sender_pref_; }
private:
DictionaryPrefUpdate update_;
base::Value* per_sender_pref_;
};
// Added in M76.
void MigratePrefs(PrefService* prefs, const std::string& project_id) {
if (!prefs->HasPrefPath(kActiveRegistrationTokenDeprecated)) {
return;
}
{
DictionaryPrefUpdate token_update(prefs, kActiveRegistrationTokens);
token_update->SetString(
project_id, prefs->GetString(kActiveRegistrationTokenDeprecated));
}
auto* old_registrations =
prefs->GetDictionary(kTypeRegisteredForInvalidationsDeprecated);
{
PerProjectDictionaryPrefUpdate update(prefs, project_id);
*update = old_registrations->Clone();
}
prefs->ClearPref(kActiveRegistrationTokenDeprecated);
prefs->ClearPref(kTypeRegisteredForInvalidationsDeprecated);
}
} // namespace
// static
void PerUserTopicRegistrationManager::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(kTypeRegisteredForInvalidationsDeprecated);
registry->RegisterStringPref(kActiveRegistrationTokenDeprecated,
std::string());
registry->RegisterDictionaryPref(kTypeRegisteredForInvalidations);
registry->RegisterDictionaryPref(kActiveRegistrationTokens);
}
// static
void PerUserTopicRegistrationManager::RegisterPrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(kTypeRegisteredForInvalidationsDeprecated);
registry->RegisterStringPref(kActiveRegistrationTokenDeprecated,
std::string());
registry->RegisterDictionaryPref(kTypeRegisteredForInvalidations);
registry->RegisterDictionaryPref(kActiveRegistrationTokens);
}
// State of the instance ID token when registration is requested.
// Used by UMA histogram, so entries shouldn't be reordered or removed.
enum class PerUserTopicRegistrationManager::TokenStateOnRegistrationRequest {
kTokenWasEmpty = 0,
kUnchangedToken = 1,
kTokenChanged = 2,
kMaxValue = kTokenChanged,
};
struct PerUserTopicRegistrationManager::RegistrationEntry {
RegistrationEntry(const Topic& topic,
SubscriptionFinishedCallback completion_callback,
PerUserTopicRegistrationRequest::RequestType type,
bool topic_is_public = false);
~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;
const bool topic_is_public;
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,
bool topic_is_public)
: topic(topic),
topic_is_public(topic_is_public),
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 std::string& project_id,
bool migrate_prefs)
: local_state_(local_state),
identity_provider_(identity_provider),
request_access_token_backoff_(&kBackoffPolicy),
url_loader_factory_(url_loader_factory),
project_id_(project_id),
migrate_prefs_(migrate_prefs) {}
PerUserTopicRegistrationManager::~PerUserTopicRegistrationManager() {}
// static
std::unique_ptr<PerUserTopicRegistrationManager>
PerUserTopicRegistrationManager::Create(
invalidation::IdentityProvider* identity_provider,
PrefService* local_state,
network::mojom::URLLoaderFactory* url_loader_factory,
const std::string& project_id,
bool migrate_prefs) {
return std::make_unique<PerUserTopicRegistrationManager>(
identity_provider, local_state, url_loader_factory, project_id,
migrate_prefs);
}
void PerUserTopicRegistrationManager::Init() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (migrate_prefs_) {
MigratePrefs(local_state_, project_id_);
}
PerProjectDictionaryPrefUpdate update(local_state_, project_id_);
if (update->DictEmpty()) {
return;
}
std::vector<std::string> keys_to_remove;
// Load registered ids from prefs.
for (const auto& it : update->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;
private_topic_to_topic_[private_topic_name] = topic;
continue;
}
// Remove saved pref.
keys_to_remove.push_back(topic);
}
// Delete prefs, which weren't decoded successfully.
for (const std::string& key : keys_to_remove) {
update->RemoveKey(key);
}
}
void PerUserTopicRegistrationManager::UpdateRegisteredTopics(
const Topics& topics,
const std::string& instance_id_token) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
instance_id_token_ = instance_id_token;
UMA_HISTOGRAM_ENUMERATION("FCMInvalidations.TokenStateOnRegistrationRequest",
DropAllSavedRegistrationsOnTokenChange());
for (const auto& topic : topics) {
// If the topic isn't registered yet, schedule the registration.
if (topic_to_private_topic_.find(topic.first) ==
topic_to_private_topic_.end()) {
auto it = registration_statuses_.find(topic.first);
if (it != registration_statuses_.end())
it->second->Cancel();
registration_statuses_[topic.first] = std::make_unique<RegistrationEntry>(
topic.first,
base::BindOnce(
&PerUserTopicRegistrationManager::RegistrationFinishedForTopic,
base::Unretained(this)),
PerUserTopicRegistrationRequest::SUBSCRIBE, topic.second.is_public);
}
}
// There may be registered topics which need to be unregistered.
// Schedule unregistration and immediately remove from
// |topic_to_private_topic_| and |private_topic_to_topic_|.
for (auto it = topic_to_private_topic_.begin();
it != topic_to_private_topic_.end();) {
Topic 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);
private_topic_to_topic_.erase(it->second);
it = topic_to_private_topic_.erase(it);
// The decision to unregister from the invalidations for the |topic| was
// made, the preferences should be cleaned up immediately.
PerProjectDictionaryPrefUpdate update(local_state_, project_id_);
update->RemoveKey(topic);
} else {
// Topic is still wanted, nothing to do.
++it;
}
}
// Kick off the process of actually processing the (un)registrations we just
// scheduled.
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.SetInstanceIdToken(instance_id_token_)
.SetScope(kInvalidationRegistrationScope)
.SetPublicTopicName(topic)
.SetAuthenticationHeader(base::StringPrintf(
"Bearer %s", access_token_.c_str()))
.SetProjectId(project_id_)
.SetType(it->second->type)
.SetTopicIsPublic(it->second->topic_is_public)
.Build();
it->second->request->Start(
base::BindOnce(&PerUserTopicRegistrationManager::RegistrationEntry::
RegistrationFinished,
base::Unretained(it->second.get())),
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) {
{
PerProjectDictionaryPrefUpdate update(local_state_, project_id_);
update->SetKey(topic, base::Value(private_topic_name));
topic_to_private_topic_[topic] = private_topic_name;
private_topic_to_topic_[private_topic_name] = topic;
}
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) {
registration_statuses_[topic]->completion_callback = base::BindOnce(
&PerUserTopicRegistrationManager::RegistrationFinishedForTopic,
base::Unretained(this));
// TODO(treib): We already called InformOfRequest(false) before in
// RegistrationFinishedForTopic(), should probably not call it again here?
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();
OAuth2AccessTokenManager::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)));
}
PerUserTopicRegistrationManager::TokenStateOnRegistrationRequest
PerUserTopicRegistrationManager::DropAllSavedRegistrationsOnTokenChange() {
{
DictionaryPrefUpdate token_update(local_state_, kActiveRegistrationTokens);
std::string current_token;
token_update->GetString(project_id_, &current_token);
if (current_token.empty()) {
token_update->SetString(project_id_, instance_id_token_);
return TokenStateOnRegistrationRequest::kTokenWasEmpty;
}
if (current_token == instance_id_token_) {
return TokenStateOnRegistrationRequest::kUnchangedToken;
}
token_update->SetString(project_id_, instance_id_token_);
}
PerProjectDictionaryPrefUpdate update(local_state_, project_id_);
*update = base::Value(base::Value::Type::DICTIONARY);
topic_to_private_topic_.clear();
private_topic_to_topic_.clear();
return TokenStateOnRegistrationRequest::kTokenChanged;
// 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", instance_id_token_);
return status;
}
base::Optional<Topic>
PerUserTopicRegistrationManager::LookupRegisteredPublicTopicByPrivateTopic(
const std::string& private_topic) const {
auto it = private_topic_to_topic_.find(private_topic);
if (it == private_topic_to_topic_.end()) {
return base::nullopt;
}
return it->second;
}
} // namespace syncer