| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/invalidation/impl/fcm_network_handler.h" |
| |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| |
| #include "base/base64url.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "build/build_config.h" |
| #include "components/gcm_driver/gcm_driver.h" |
| #include "components/gcm_driver/gcm_profile_service.h" |
| #include "components/gcm_driver/instance_id/instance_id.h" |
| #include "components/gcm_driver/instance_id/instance_id_driver.h" |
| #include "components/invalidation/impl/invalidation_switches.h" |
| #include "components/invalidation/impl/status.h" |
| #include "components/invalidation/public/invalidator_state.h" |
| |
| using instance_id::InstanceID; |
| |
| namespace invalidation { |
| |
| namespace { |
| |
| const char kPayloadKey[] = "payload"; |
| const char kPublicTopic[] = "external_name"; |
| const char kVersionKey[] = "version"; |
| |
| // OAuth2 Scope passed to getToken to obtain GCM registration tokens. |
| // Must match Java GoogleCloudMessaging.INSTANCE_ID_SCOPE. |
| const char kGCMScope[] = "GCM"; |
| |
| // Lower bound time between two token validations when listening. |
| const int kTokenValidationPeriodMinutesDefault = 60 * 24; |
| |
| // Returns the TTL (time-to-live) for the Instance ID token, or 0 if no TTL |
| // should be specified. |
| base::TimeDelta GetTimeToLive(const std::string& sender_id) { |
| // This magic value is identical to kPolicyFCMInvalidationSenderID, i.e. the |
| // value that ChromeOS policy uses for its invalidations. |
| if (sender_id == "1013309121859") { |
| if (!base::FeatureList::IsEnabled(switches::kPolicyInstanceIDTokenTTL)) { |
| return base::TimeDelta(); |
| } |
| |
| return base::Seconds(switches::kPolicyInstanceIDTokenTTLSeconds.Get()); |
| } |
| |
| // The default for all other FCM clients is no TTL. |
| return base::TimeDelta(); |
| } |
| |
| std::string GetValueFromMessage(const gcm::IncomingMessage& message, |
| const std::string& key) { |
| std::string value; |
| auto it = message.data.find(key); |
| if (it != message.data.end()) |
| value = it->second; |
| return value; |
| } |
| |
| // Unpacks the private topic included in messages to the form returned for |
| // subscription requests. |
| // |
| // Subscriptions for private topics generate a private topic from the public |
| // topic of the form "/private/${public_topic}-${something}. Messages include |
| // this as the sender in the form |
| // "/topics/private/${public_topic}-${something}". For such messages, strip the |
| // "/topics" prefix. |
| // |
| // Subscriptions for public topics pass-through the public topic unchanged: |
| // "${public_topic}". Messages include the sender in the form |
| // "/topics/${public_topic}". For these messages, strip the "/topics/" prefix. |
| // |
| // If the provided sender does not match either pattern, return it unchanged. |
| std::string UnpackPrivateTopic(std::string_view private_topic) { |
| if (base::StartsWith(private_topic, "/topics/private/")) { |
| return std::string(private_topic.substr(strlen("/topics"))); |
| } else if (base::StartsWith(private_topic, "/topics/")) { |
| return std::string(private_topic.substr(strlen("/topics/"))); |
| } else { |
| return std::string(private_topic); |
| } |
| } |
| |
| InvalidationParsingStatus ParseIncomingMessage( |
| const gcm::IncomingMessage& message, |
| std::string* payload, |
| std::string* private_topic, |
| std::string* public_topic, |
| int64_t* version) { |
| *payload = GetValueFromMessage(message, kPayloadKey); |
| std::string version_str = GetValueFromMessage(message, kVersionKey); |
| |
| // Version must always be there, and be an integer. |
| if (version_str.empty()) |
| return InvalidationParsingStatus::kVersionEmpty; |
| if (!base::StringToInt64(version_str, version)) |
| return InvalidationParsingStatus::kVersionInvalid; |
| |
| *public_topic = GetValueFromMessage(message, kPublicTopic); |
| |
| *private_topic = UnpackPrivateTopic(message.sender_id); |
| if (private_topic->empty()) |
| return InvalidationParsingStatus::kPrivateTopicEmpty; |
| |
| return InvalidationParsingStatus::kSuccess; |
| } |
| |
| void RecordFCMMessageStatus(InvalidationParsingStatus status, |
| const std::string& sender_id) { |
| // These histograms are recorded quite frequently, so use the macros rather |
| // than the functions. |
| UMA_HISTOGRAM_ENUMERATION("FCMInvalidations.FCMMessageStatus", status); |
| // Also split the histogram by a few well-known senders. The actual constants |
| // aren't accessible here (they're defined in higher layers), so we simply |
| // duplicate them here, strictly only for the purpose of metrics. |
| constexpr char kDriveFcmSenderId[] = "947318989803"; |
| constexpr char kPolicyFCMInvalidationSenderID[] = "1013309121859"; |
| if (sender_id == kDriveFcmSenderId) { |
| UMA_HISTOGRAM_ENUMERATION("FCMInvalidations.FCMMessageStatus.Drive", |
| status); |
| } else if (sender_id == kPolicyFCMInvalidationSenderID) { |
| UMA_HISTOGRAM_ENUMERATION("FCMInvalidations.FCMMessageStatus.Policy", |
| status); |
| } |
| } |
| |
| } // namespace |
| |
| FCMNetworkHandler::FCMNetworkHandler( |
| gcm::GCMDriver* gcm_driver, |
| instance_id::InstanceIDDriver* instance_id_driver, |
| const std::string& sender_id, |
| const std::string& app_id) |
| : gcm_driver_(gcm_driver), |
| instance_id_driver_(instance_id_driver), |
| token_validation_timer_(std::make_unique<base::OneShotTimer>()), |
| sender_id_(sender_id), |
| app_id_(app_id) {} |
| |
| FCMNetworkHandler::~FCMNetworkHandler() { |
| StopListening(); |
| } |
| |
| // static |
| std::unique_ptr<FCMNetworkHandler> FCMNetworkHandler::Create( |
| gcm::GCMDriver* gcm_driver, |
| instance_id::InstanceIDDriver* instance_id_driver, |
| const std::string& sender_id, |
| const std::string& app_id) { |
| return std::make_unique<FCMNetworkHandler>(gcm_driver, instance_id_driver, |
| sender_id, app_id); |
| } |
| |
| void FCMNetworkHandler::StartListening() { |
| if (IsListening()) { |
| StopListening(); |
| } |
| // Adding ourselves as Handler means start listening. |
| // Being the listener is pre-requirement for token operations. |
| gcm_driver_->AddAppHandler(app_id_, this); |
| |
| instance_id_driver_->GetInstanceID(app_id_)->GetToken( |
| sender_id_, kGCMScope, GetTimeToLive(sender_id_), |
| /*flags=*/{InstanceID::Flags::kIsLazy}, |
| base::BindRepeating(&FCMNetworkHandler::DidRetrieveToken, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void FCMNetworkHandler::StopListening() { |
| if (IsListening()) |
| gcm_driver_->RemoveAppHandler(app_id_); |
| } |
| |
| bool FCMNetworkHandler::IsListening() const { |
| return gcm_driver_->GetAppHandler(app_id_); |
| } |
| |
| void FCMNetworkHandler::DidRetrieveToken(const std::string& subscription_token, |
| InstanceID::Result result) { |
| base::UmaHistogramEnumeration("FCMInvalidations.InitialTokenRetrievalStatus", |
| result); |
| switch (result) { |
| case InstanceID::SUCCESS: |
| // The received token is assumed to be valid, therefore, we reschedule |
| // validation. |
| DeliverToken(subscription_token); |
| token_ = subscription_token; |
| UpdateChannelState(FcmChannelState::ENABLED); |
| break; |
| case InstanceID::INVALID_PARAMETER: |
| case InstanceID::DISABLED: |
| case InstanceID::ASYNC_OPERATION_PENDING: |
| case InstanceID::SERVER_ERROR: |
| case InstanceID::UNKNOWN_ERROR: |
| case InstanceID::NETWORK_ERROR: |
| DLOG(WARNING) << "Messaging subscription failed; InstanceID::Result = " |
| << result; |
| UpdateChannelState(FcmChannelState::NO_INSTANCE_ID_TOKEN); |
| break; |
| } |
| ScheduleNextTokenValidation(); |
| } |
| |
| void FCMNetworkHandler::ScheduleNextTokenValidation() { |
| DCHECK(IsListening()); |
| |
| token_validation_timer_->Start( |
| FROM_HERE, base::Minutes(kTokenValidationPeriodMinutesDefault), |
| base::BindOnce(&FCMNetworkHandler::StartTokenValidation, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void FCMNetworkHandler::StartTokenValidation() { |
| DCHECK(IsListening()); |
| |
| instance_id_driver_->GetInstanceID(app_id_)->GetToken( |
| sender_id_, kGCMScope, GetTimeToLive(sender_id_), |
| /*flags=*/{InstanceID::Flags::kIsLazy}, |
| base::BindOnce(&FCMNetworkHandler::DidReceiveTokenForValidation, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void FCMNetworkHandler::DidReceiveTokenForValidation( |
| const std::string& new_token, |
| InstanceID::Result result) { |
| if (!IsListening()) { |
| // After we requested the token, |StopListening| has been called. Thus, |
| // ignore the token. |
| return; |
| } |
| |
| if (result == InstanceID::SUCCESS) { |
| UpdateChannelState(FcmChannelState::ENABLED); |
| if (token_ != new_token) { |
| token_ = new_token; |
| DeliverToken(new_token); |
| } |
| } |
| |
| ScheduleNextTokenValidation(); |
| } |
| |
| void FCMNetworkHandler::UpdateChannelState(FcmChannelState state) { |
| if (channel_state_ == state) |
| return; |
| channel_state_ = state; |
| NotifyChannelStateChange(channel_state_); |
| } |
| |
| void FCMNetworkHandler::ShutdownHandler() {} |
| |
| void FCMNetworkHandler::OnStoreReset() {} |
| |
| void FCMNetworkHandler::OnMessage(const std::string& app_id, |
| const gcm::IncomingMessage& message) { |
| DCHECK_EQ(app_id, app_id_); |
| |
| std::string payload; |
| std::string private_topic; |
| std::string public_topic; |
| int64_t version = 0; |
| InvalidationParsingStatus status = ParseIncomingMessage( |
| message, &payload, &private_topic, &public_topic, &version); |
| |
| RecordFCMMessageStatus(status, sender_id_); |
| |
| if (status == InvalidationParsingStatus::kSuccess) |
| DeliverIncomingMessage(payload, private_topic, public_topic, version); |
| } |
| |
| void FCMNetworkHandler::OnMessagesDeleted(const std::string& app_id) { |
| DCHECK_EQ(app_id, app_id_); |
| // Note: As of 2020-02, this doesn't actually happen in practice. |
| } |
| |
| void FCMNetworkHandler::OnSendError( |
| const std::string& app_id, |
| const gcm::GCMClient::SendErrorDetails& details) { |
| // Should never be called because we don't send GCM messages to |
| // the server. |
| NOTREACHED() << "FCMNetworkHandler doesn't send GCM messages."; |
| } |
| |
| void FCMNetworkHandler::OnSendAcknowledged(const std::string& app_id, |
| const std::string& message_id) { |
| // Should never be called because we don't send GCM messages to |
| // the server. |
| NOTREACHED() << "FCMNetworkHandler doesn't send GCM messages."; |
| } |
| |
| void FCMNetworkHandler::SetTokenValidationTimerForTesting( |
| std::unique_ptr<base::OneShotTimer> token_validation_timer) { |
| token_validation_timer_ = std::move(token_validation_timer); |
| } |
| |
| } // namespace invalidation |