| // Copyright (c) 2015 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 "chrome/browser/chromeos/policy/heartbeat_scheduler.h" |
| |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/command_line.h" |
| #include "base/location.h" |
| #include "base/macros.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "components/gcm_driver/gcm_driver.h" |
| |
| namespace { |
| const int kMinHeartbeatIntervalMs = 30 * 1000; // 30 seconds |
| const int kMaxHeartbeatIntervalMs = 24 * 60 * 60 * 1000; // 24 hours |
| |
| // Our sender ID we send up with all of our GCM messages. |
| const char* kHeartbeatGCMAppID = "com.google.chromeos.monitoring"; |
| |
| // The default destination we send our GCM messages to. |
| const char* kHeartbeatGCMDestinationID = "1013309121859"; |
| const char* kHeartbeatGCMSenderSuffix = "@google.com"; |
| |
| // Destination of upstream notification sign up message. |
| const char* kUpstreamNotificationSignUpDestinationID = |
| "https://gcm.googleapis.com/gcm/gcm.event_tracker"; |
| |
| // A bit mask, listening events of upstream notification. |
| const char* kUpstreamNotificationSignUpListeningEvents = |
| "7"; // START | DISCONNECTED | HEARTBEAT |
| |
| const char* kGcmMessageTypeKey = "type"; |
| const char* kHeartbeatTimestampKey = "timestamp"; |
| const char* kHeartbeatDomainNameKey = "domain_name"; |
| const char* kHeartbeatDeviceIDKey = "device_id"; |
| const char* kHeartbeatTypeValue = "hb"; |
| const char* kUpstreamNotificationNotifyKey = "notify"; |
| const char* kUpstreamNotificationRegIdKey = "registration_id"; |
| |
| // If we get an error registering with GCM, try again in two minutes. |
| const int64_t kRegistrationRetryDelayMs = 2 * 60 * 1000; |
| |
| const char* kHeartbeatSchedulerScope = |
| "policy.heartbeat_scheduler.upstream_notification"; |
| |
| // Returns the destination ID for GCM heartbeats. |
| std::string GetDestinationID() { |
| std::string receiver_id = kHeartbeatGCMDestinationID; |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kMonitoringDestinationID)) { |
| receiver_id = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kMonitoringDestinationID); |
| } |
| return receiver_id; |
| } |
| |
| } // namespace |
| |
| namespace policy { |
| |
| const int64_t HeartbeatScheduler::kDefaultHeartbeatIntervalMs = |
| 2 * 60 * 1000; // 2 minutes |
| |
| // Helper class used to manage GCM registration (handles retrying after |
| // errors, etc). |
| class HeartbeatRegistrationHelper { |
| public: |
| typedef base::Callback<void(const std::string& registration_id)> |
| RegistrationHelperCallback; |
| |
| HeartbeatRegistrationHelper( |
| gcm::GCMDriver* gcm_driver, |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner); |
| |
| void Register(const RegistrationHelperCallback& callback); |
| |
| private: |
| void AttemptRegistration(); |
| |
| // Callback invoked once a registration attempt has finished. |
| void OnRegisterAttemptComplete(const std::string& registration_id, |
| gcm::GCMClient::Result result); |
| |
| // GCMDriver to use to register. |
| gcm::GCMDriver* const gcm_driver_; |
| |
| // Callback to invoke when we have completed GCM registration. |
| RegistrationHelperCallback callback_; |
| |
| // TaskRunner used for scheduling retry attempts. |
| const scoped_refptr<base::SequencedTaskRunner> task_runner_; |
| |
| // Should remain the last member so it will be destroyed first and |
| // invalidate all weak pointers. |
| base::WeakPtrFactory<HeartbeatRegistrationHelper> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(HeartbeatRegistrationHelper); |
| }; |
| |
| HeartbeatRegistrationHelper::HeartbeatRegistrationHelper( |
| gcm::GCMDriver* gcm_driver, |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner) |
| : gcm_driver_(gcm_driver), |
| task_runner_(task_runner), |
| weak_factory_(this) { |
| } |
| |
| void HeartbeatRegistrationHelper::Register( |
| const RegistrationHelperCallback& callback) { |
| // Should only call Register() once. |
| DCHECK(callback_.is_null()); |
| callback_ = callback; |
| AttemptRegistration(); |
| } |
| |
| void HeartbeatRegistrationHelper::AttemptRegistration() { |
| std::vector<std::string> destinations; |
| destinations.push_back(GetDestinationID()); |
| gcm_driver_->Register( |
| kHeartbeatGCMAppID, |
| destinations, |
| base::Bind(&HeartbeatRegistrationHelper::OnRegisterAttemptComplete, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void HeartbeatRegistrationHelper::OnRegisterAttemptComplete( |
| const std::string& registration_id, gcm::GCMClient::Result result) { |
| DVLOG(1) << "Received Register() response: " << result; |
| // TODO(atwilson): Track GCM errors via UMA (http://crbug.com/459238). |
| switch (result) { |
| case gcm::GCMClient::SUCCESS: |
| { |
| // Copy the callback, because the callback may free this object and |
| // we don't want to free the callback object and any bound variables |
| // until the callback exits. |
| RegistrationHelperCallback callback = callback_; |
| callback.Run(registration_id); |
| // This helper may be freed now, so do not access any member variables |
| // after this point. |
| return; |
| } |
| |
| case gcm::GCMClient::NETWORK_ERROR: |
| case gcm::GCMClient::SERVER_ERROR: |
| // Transient error - try again after a delay. |
| task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&HeartbeatRegistrationHelper::AttemptRegistration, |
| weak_factory_.GetWeakPtr()), |
| base::TimeDelta::FromMilliseconds(kRegistrationRetryDelayMs)); |
| break; |
| |
| case gcm::GCMClient::INVALID_PARAMETER: |
| case gcm::GCMClient::UNKNOWN_ERROR: |
| case gcm::GCMClient::GCM_DISABLED: |
| // No need to bother retrying in the case of one of these fatal errors. |
| // This means that heartbeats will remain disabled until the next |
| // restart. |
| DLOG(ERROR) << "Fatal GCM Registration error: " << result; |
| break; |
| |
| case gcm::GCMClient::ASYNC_OPERATION_PENDING: |
| case gcm::GCMClient::TTL_EXCEEDED: |
| default: |
| NOTREACHED() << "Unexpected GCMDriver::Register() result: " << result; |
| break; |
| } |
| } |
| |
| HeartbeatScheduler::HeartbeatScheduler( |
| gcm::GCMDriver* driver, |
| policy::CloudPolicyClient* cloud_policy_client, |
| const std::string& enrollment_domain, |
| const std::string& device_id, |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner) |
| : task_runner_(task_runner), |
| enrollment_domain_(enrollment_domain), |
| device_id_(device_id), |
| heartbeat_enabled_(false), |
| heartbeat_interval_( |
| base::TimeDelta::FromMilliseconds(kDefaultHeartbeatIntervalMs)), |
| cloud_policy_client_(cloud_policy_client), |
| gcm_driver_(driver), |
| weak_factory_(this) { |
| // If no GCMDriver (e.g. this is loaded as part of an unrelated unit test) |
| // do nothing as no heartbeats can be sent. |
| if (!gcm_driver_) |
| return; |
| |
| heartbeat_frequency_observer_ = |
| chromeos::CrosSettings::Get()->AddSettingsObserver( |
| chromeos::kHeartbeatFrequency, |
| base::Bind(&HeartbeatScheduler::RefreshHeartbeatSettings, |
| base::Unretained(this))); |
| |
| heartbeat_enabled_observer_ = |
| chromeos::CrosSettings::Get()->AddSettingsObserver( |
| chromeos::kHeartbeatEnabled, |
| base::Bind(&HeartbeatScheduler::RefreshHeartbeatSettings, |
| base::Unretained(this))); |
| |
| // Update the heartbeat frequency from settings. This will trigger a |
| // heartbeat as appropriate once the settings have been refreshed. |
| RefreshHeartbeatSettings(); |
| |
| // Initialize the default heartbeats interval for GCM driver. |
| gcm_driver_->AddHeartbeatInterval(kHeartbeatSchedulerScope, |
| heartbeat_interval_.InMilliseconds()); |
| } |
| |
| void HeartbeatScheduler::RefreshHeartbeatSettings() { |
| // Attempt to fetch the current value of the reporting settings. |
| // If trusted values are not available, register this function to be called |
| // back when they are available. |
| chromeos::CrosSettings* settings = chromeos::CrosSettings::Get(); |
| if (chromeos::CrosSettingsProvider::TRUSTED != settings->PrepareTrustedValues( |
| base::Bind(&HeartbeatScheduler::RefreshHeartbeatSettings, |
| weak_factory_.GetWeakPtr()))) { |
| return; |
| } |
| |
| // CrosSettings are trusted - update our cached settings (we cache the |
| // value because CrosSettings can become untrusted at arbitrary times and we |
| // want to use the last trusted value). |
| int frequency; |
| if (settings->GetInteger(chromeos::kHeartbeatFrequency, &frequency)) { |
| heartbeat_interval_ = EnsureValidHeartbeatInterval( |
| base::TimeDelta::FromMilliseconds(frequency)); |
| } |
| |
| gcm_driver_->AddHeartbeatInterval(kHeartbeatSchedulerScope, |
| heartbeat_interval_.InMilliseconds()); |
| |
| bool enabled; |
| if (settings->GetBoolean(chromeos::kHeartbeatEnabled, &enabled)) |
| heartbeat_enabled_ = enabled; |
| |
| if (!heartbeat_enabled_) { |
| // Heartbeats are no longer enabled - cancel our callback and any |
| // outstanding registration attempts and disconnect from GCM so the |
| // connection can be shut down. If heartbeats are re-enabled later, we |
| // will re-register with GCM. |
| ShutdownGCM(); |
| } else { |
| // Schedule a new upload with the new frequency. |
| ScheduleNextHeartbeat(); |
| } |
| |
| DVLOG(1) << "heartbeat enabled: " << heartbeat_enabled_; |
| DVLOG(1) << "heartbeat frequency: " << heartbeat_interval_; |
| } |
| |
| void HeartbeatScheduler::ShutdownGCM() { |
| heartbeat_callback_.Cancel(); |
| registration_helper_.reset(); |
| registration_id_.clear(); |
| if (registered_app_handler_) { |
| registered_app_handler_ = false; |
| gcm_driver_->RemoveHeartbeatInterval(kHeartbeatSchedulerScope); |
| gcm_driver_->RemoveAppHandler(kHeartbeatGCMAppID); |
| gcm_driver_->RemoveConnectionObserver(this); |
| } |
| } |
| |
| base::TimeDelta HeartbeatScheduler::EnsureValidHeartbeatInterval( |
| const base::TimeDelta& interval) { |
| const base::TimeDelta min = base::TimeDelta::FromMilliseconds( |
| kMinHeartbeatIntervalMs); |
| const base::TimeDelta max = base::TimeDelta::FromMilliseconds( |
| kMaxHeartbeatIntervalMs); |
| if (interval < min) { |
| DLOG(WARNING) << "Invalid heartbeat interval: " << interval; |
| return min; |
| } |
| if (interval > max) { |
| DLOG(WARNING) << "Invalid heartbeat interval: " << interval; |
| return max; |
| } |
| return interval; |
| } |
| |
| void HeartbeatScheduler::ScheduleNextHeartbeat() { |
| // Do nothing if heartbeats are disabled. |
| if (!heartbeat_enabled_) |
| return; |
| |
| if (registration_id_.empty()) { |
| // We are not registered with the GCM service yet, so kick off registration. |
| if (!registration_helper_) { |
| // Add ourselves as an AppHandler - this is required in order to setup |
| // a GCM connection. |
| registered_app_handler_ = true; |
| gcm_driver_->AddAppHandler(kHeartbeatGCMAppID, this); |
| gcm_driver_->AddConnectionObserver(this); |
| registration_helper_.reset(new HeartbeatRegistrationHelper( |
| gcm_driver_, task_runner_)); |
| registration_helper_->Register( |
| base::Bind(&HeartbeatScheduler::OnRegistrationComplete, |
| weak_factory_.GetWeakPtr())); |
| } |
| return; |
| } |
| |
| // Calculate when to fire off the next update (if it should have already |
| // happened, this yields a TimeDelta of 0). |
| base::TimeDelta delay = std::max( |
| last_heartbeat_ + heartbeat_interval_ - base::Time::NowFromSystemTime(), |
| base::TimeDelta()); |
| |
| heartbeat_callback_.Reset(base::Bind(&HeartbeatScheduler::SendHeartbeat, |
| base::Unretained(this))); |
| task_runner_->PostDelayedTask( |
| FROM_HERE, heartbeat_callback_.callback(), delay); |
| } |
| |
| void HeartbeatScheduler::OnRegistrationComplete( |
| const std::string& registration_id) { |
| DCHECK(!registration_id.empty()); |
| registration_helper_.reset(); |
| registration_id_ = registration_id; |
| |
| if (cloud_policy_client_) { |
| // TODO(binjin): Avoid sending the same GCM id to the server. |
| // See http://crbug.com/516375 |
| cloud_policy_client_->UpdateGcmId( |
| registration_id, |
| base::Bind(&HeartbeatScheduler::OnGcmIdUpdateRequestSent, |
| weak_factory_.GetWeakPtr())); |
| SignUpUpstreamNotification(); |
| } |
| |
| // Now that GCM registration is complete, start sending heartbeats. |
| ScheduleNextHeartbeat(); |
| } |
| |
| void HeartbeatScheduler::SendHeartbeat() { |
| DCHECK(!registration_id_.empty()); |
| if (!gcm_driver_ || !heartbeat_enabled_) |
| return; |
| |
| gcm::OutgoingMessage message; |
| message.time_to_live = heartbeat_interval_.InSeconds(); |
| // Just use the current timestamp as the message ID - if the user changes the |
| // time and we send a message with the same ID that we previously used, no |
| // big deal (the new message will replace the old, which is the behavior we |
| // want anyway, per: |
| // https://developer.chrome.com/apps/cloudMessaging#send_messages |
| message.id = base::Int64ToString( |
| base::Time::NowFromSystemTime().ToInternalValue()); |
| message.data[kGcmMessageTypeKey] = kHeartbeatTypeValue; |
| message.data[kHeartbeatTimestampKey] = base::Int64ToString( |
| base::Time::NowFromSystemTime().ToJavaTime()); |
| message.data[kHeartbeatDomainNameKey] = enrollment_domain_; |
| message.data[kHeartbeatDeviceIDKey] = device_id_; |
| gcm_driver_->Send(kHeartbeatGCMAppID, |
| GetDestinationID() + kHeartbeatGCMSenderSuffix, |
| message, |
| base::Bind(&HeartbeatScheduler::OnHeartbeatSent, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void HeartbeatScheduler::SignUpUpstreamNotification() { |
| DCHECK(gcm_driver_); |
| |
| // Registration ID is a hard requirement for upstream notification signup, |
| // so we need to send the sign up message after registration is completed, |
| // as well as registration ID changes. |
| // We also listen to GCM driver connected events, so that once we |
| // reconnected to GCM server, we can resend the signup message immediately. |
| // Having both conditional checks here ensures that during the start up, the |
| // sign up message will be sent at most once. |
| if (registration_id_.empty() || !gcm_driver_->IsConnected()) |
| return; |
| |
| gcm::OutgoingMessage message; |
| message.id = |
| base::Int64ToString(base::Time::NowFromSystemTime().ToInternalValue()); |
| message.data[kGcmMessageTypeKey] = kUpstreamNotificationSignUpListeningEvents; |
| message.data[kUpstreamNotificationNotifyKey] = |
| GetDestinationID() + kHeartbeatGCMSenderSuffix; |
| message.data[kUpstreamNotificationRegIdKey] = registration_id_; |
| gcm_driver_->Send(kHeartbeatGCMAppID, |
| kUpstreamNotificationSignUpDestinationID, message, |
| base::Bind(&HeartbeatScheduler::OnUpstreamNotificationSent, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void HeartbeatScheduler::OnHeartbeatSent(const std::string& message_id, |
| gcm::GCMClient::Result result) { |
| DVLOG(1) << "Monitoring heartbeat sent - result = " << result; |
| // Don't care if the result was successful or not - just schedule the next |
| // heartbeat. |
| DLOG_IF(ERROR, result != gcm::GCMClient::SUCCESS) << |
| "Error sending monitoring heartbeat: " << result; |
| last_heartbeat_ = base::Time::NowFromSystemTime(); |
| ScheduleNextHeartbeat(); |
| } |
| |
| void HeartbeatScheduler::OnUpstreamNotificationSent( |
| const std::string& message_id, |
| gcm::GCMClient::Result result) { |
| DVLOG(1) << "Upstream notification signup message sent - result = " << result; |
| DLOG_IF(ERROR, result != gcm::GCMClient::SUCCESS) |
| << "Error sending upstream notification signup message: " << result; |
| } |
| |
| HeartbeatScheduler::~HeartbeatScheduler() { |
| ShutdownGCM(); |
| } |
| |
| void HeartbeatScheduler::ShutdownHandler() { |
| // This should never be called, because BrowserProcessImpl::StartTearDown() |
| // should shutdown the BrowserPolicyConnector (which destroys this object) |
| // before the GCMDriver. Our goal is to make sure that this object is always |
| // shutdown before GCMDriver is shut down, rather than trying to handle the |
| // case when GCMDriver goes away. |
| NOTREACHED() << "HeartbeatScheduler should be destroyed before GCMDriver"; |
| } |
| |
| void HeartbeatScheduler::OnStoreReset() { |
| // TODO(crbug.com/661660): Tell server that |registration_id_| is no longer |
| // valid. See also crbug.com/516375. |
| if (!registration_helper_) { |
| ShutdownGCM(); |
| RefreshHeartbeatSettings(); |
| } // Otherwise let the pending registration complete normally. |
| } |
| |
| void HeartbeatScheduler::OnMessage(const std::string& app_id, |
| const gcm::IncomingMessage& message) { |
| // Should never be called because we don't get any incoming messages |
| // for our app ID. |
| NOTREACHED() << "Received incoming message for " << app_id; |
| } |
| |
| void HeartbeatScheduler::OnMessagesDeleted(const std::string& app_id) { |
| } |
| |
| void HeartbeatScheduler::OnSendError( |
| const std::string& app_id, |
| const gcm::GCMClient::SendErrorDetails& details) { |
| // Ignore send errors - we already are notified above in OnHeartbeatSent(). |
| } |
| |
| void HeartbeatScheduler::OnSendAcknowledged(const std::string& app_id, |
| const std::string& message_id) { |
| DVLOG(1) << "Heartbeat sent with message_id: " << message_id; |
| } |
| |
| void HeartbeatScheduler::OnConnected(const net::IPEndPoint&) { |
| SignUpUpstreamNotification(); |
| } |
| |
| void HeartbeatScheduler::OnGcmIdUpdateRequestSent(bool success) { |
| // TODO(binjin): Handle the failure, probably by exponential backoff. |
| LOG_IF(WARNING, !success) << "Failed to send GCM id to DM server"; |
| } |
| |
| } // namespace policy |