diff options
author | Jocelyn Turcotte <jocelyn.turcotte@digia.com> | 2014-08-08 14:30:41 +0200 |
---|---|---|
committer | Jocelyn Turcotte <jocelyn.turcotte@digia.com> | 2014-08-12 13:49:54 +0200 |
commit | ab0a50979b9eb4dfa3320eff7e187e41efedf7a9 (patch) | |
tree | 498dfb8a97ff3361a9f7486863a52bb4e26bb898 /chromium/google_apis/gcm/engine | |
parent | 4ce69f7403811819800e7c5ae1318b2647e778d1 (diff) |
Update Chromium to beta version 37.0.2062.68
Change-Id: I188e3b5aff1bec75566014291b654eb19f5bc8ca
Reviewed-by: Andras Becsi <andras.becsi@digia.com>
Diffstat (limited to 'chromium/google_apis/gcm/engine')
41 files changed, 7128 insertions, 1423 deletions
diff --git a/chromium/google_apis/gcm/engine/checkin_request.cc b/chromium/google_apis/gcm/engine/checkin_request.cc new file mode 100644 index 00000000000..a5558471772 --- /dev/null +++ b/chromium/google_apis/gcm/engine/checkin_request.cc @@ -0,0 +1,221 @@ +// Copyright 2014 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 "google_apis/gcm/engine/checkin_request.h" + +#include "base/bind.h" +#include "base/message_loop/message_loop.h" +#include "base/metrics/histogram.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_status.h" + +namespace gcm { + +namespace { +const char kRequestContentType[] = "application/x-protobuf"; +const int kRequestVersionValue = 3; +const int kDefaultUserSerialNumber = 0; + +// This enum is also used in an UMA histogram (GCMCheckinRequestStatus +// enum defined in tools/metrics/histograms/histogram.xml). Hence the entries +// here shouldn't be deleted or re-ordered and new ones should be added to +// the end, and update the GetCheckinRequestStatusString(...) below. +enum CheckinRequestStatus { + SUCCESS, // Checkin completed successfully. + URL_FETCHING_FAILED, // URL fetching failed. + HTTP_BAD_REQUEST, // The request was malformed. + HTTP_UNAUTHORIZED, // The security token didn't match the android id. + HTTP_NOT_OK, // HTTP status was not OK. + RESPONSE_PARSING_FAILED, // Check in response parsing failed. + ZERO_ID_OR_TOKEN, // Either returned android id or security token + // was zero. + // NOTE: always keep this entry at the end. Add new status types only + // immediately above this line. Make sure to update the corresponding + // histogram enum accordingly. + STATUS_COUNT +}; + +// Returns string representation of enum CheckinRequestStatus. +std::string GetCheckinRequestStatusString(CheckinRequestStatus status) { + switch (status) { + case SUCCESS: + return "SUCCESS"; + case URL_FETCHING_FAILED: + return "URL_FETCHING_FAILED"; + case HTTP_BAD_REQUEST: + return "HTTP_BAD_REQUEST"; + case HTTP_UNAUTHORIZED: + return "HTTP_UNAUTHORIZED"; + case HTTP_NOT_OK: + return "HTTP_NOT_OK"; + case RESPONSE_PARSING_FAILED: + return "RESPONSE_PARSING_FAILED"; + case ZERO_ID_OR_TOKEN: + return "ZERO_ID_OR_TOKEN"; + default: + NOTREACHED(); + return "UNKNOWN_STATUS"; + } +} + +// Records checkin status to both stats recorder and reports to UMA. +void RecordCheckinStatusAndReportUMA(CheckinRequestStatus status, + GCMStatsRecorder* recorder, + bool will_retry) { + UMA_HISTOGRAM_ENUMERATION("GCM.CheckinRequestStatus", status, STATUS_COUNT); + if (status == SUCCESS) + recorder->RecordCheckinSuccess(); + else { + recorder->RecordCheckinFailure(GetCheckinRequestStatusString(status), + will_retry); + } +} + +} // namespace + +CheckinRequest::RequestInfo::RequestInfo( + uint64 android_id, + uint64 security_token, + const std::string& settings_digest, + const checkin_proto::ChromeBuildProto& chrome_build_proto) + : android_id(android_id), + security_token(security_token), + settings_digest(settings_digest), + chrome_build_proto(chrome_build_proto) { +} + +CheckinRequest::RequestInfo::~RequestInfo() {} + +CheckinRequest::CheckinRequest( + const GURL& checkin_url, + const RequestInfo& request_info, + const net::BackoffEntry::Policy& backoff_policy, + const CheckinRequestCallback& callback, + net::URLRequestContextGetter* request_context_getter, + GCMStatsRecorder* recorder) + : request_context_getter_(request_context_getter), + callback_(callback), + backoff_entry_(&backoff_policy), + checkin_url_(checkin_url), + request_info_(request_info), + recorder_(recorder), + weak_ptr_factory_(this) { +} + +CheckinRequest::~CheckinRequest() {} + +void CheckinRequest::Start() { + DCHECK(!url_fetcher_.get()); + + checkin_proto::AndroidCheckinRequest request; + request.set_id(request_info_.android_id); + request.set_security_token(request_info_.security_token); + request.set_user_serial_number(kDefaultUserSerialNumber); + request.set_version(kRequestVersionValue); + if (!request_info_.settings_digest.empty()) + request.set_digest(request_info_.settings_digest); + + checkin_proto::AndroidCheckinProto* checkin = request.mutable_checkin(); + checkin->mutable_chrome_build()->CopyFrom(request_info_.chrome_build_proto); +#if defined(CHROME_OS) + checkin->set_type(checkin_proto::DEVICE_CHROME_OS); +#else + checkin->set_type(checkin_proto::DEVICE_CHROME_BROWSER); +#endif + + std::string upload_data; + CHECK(request.SerializeToString(&upload_data)); + + url_fetcher_.reset( + net::URLFetcher::Create(checkin_url_, net::URLFetcher::POST, this)); + url_fetcher_->SetRequestContext(request_context_getter_); + url_fetcher_->SetUploadData(kRequestContentType, upload_data); + recorder_->RecordCheckinInitiated(request_info_.android_id); + request_start_time_ = base::TimeTicks::Now(); + url_fetcher_->Start(); +} + +void CheckinRequest::RetryWithBackoff(bool update_backoff) { + if (update_backoff) { + backoff_entry_.InformOfRequest(false); + url_fetcher_.reset(); + } + + if (backoff_entry_.ShouldRejectRequest()) { + DVLOG(1) << "Delay GCM checkin for: " + << backoff_entry_.GetTimeUntilRelease().InMilliseconds() + << " milliseconds."; + recorder_->RecordCheckinDelayedDueToBackoff( + backoff_entry_.GetTimeUntilRelease().InMilliseconds()); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&CheckinRequest::RetryWithBackoff, + weak_ptr_factory_.GetWeakPtr(), + false), + backoff_entry_.GetTimeUntilRelease()); + return; + } + + Start(); +} + +void CheckinRequest::OnURLFetchComplete(const net::URLFetcher* source) { + std::string response_string; + checkin_proto::AndroidCheckinResponse response_proto; + if (!source->GetStatus().is_success()) { + LOG(ERROR) << "Failed to get checkin response. Fetcher failed. Retrying."; + RecordCheckinStatusAndReportUMA(URL_FETCHING_FAILED, recorder_, true); + RetryWithBackoff(true); + return; + } + + net::HttpStatusCode response_status = static_cast<net::HttpStatusCode>( + source->GetResponseCode()); + if (response_status == net::HTTP_BAD_REQUEST || + response_status == net::HTTP_UNAUTHORIZED) { + // BAD_REQUEST indicates that the request was malformed. + // UNAUTHORIZED indicates that security token didn't match the android id. + LOG(ERROR) << "No point retrying the checkin with status: " + << response_status << ". Checkin failed."; + CheckinRequestStatus status = response_status == net::HTTP_BAD_REQUEST ? + HTTP_BAD_REQUEST : HTTP_UNAUTHORIZED; + RecordCheckinStatusAndReportUMA(status, recorder_, false); + callback_.Run(response_proto); + return; + } + + if (response_status != net::HTTP_OK || + !source->GetResponseAsString(&response_string) || + !response_proto.ParseFromString(response_string)) { + LOG(ERROR) << "Failed to get checkin response. HTTP Status: " + << response_status << ". Retrying."; + CheckinRequestStatus status = response_status != net::HTTP_OK ? + HTTP_NOT_OK : RESPONSE_PARSING_FAILED; + RecordCheckinStatusAndReportUMA(status, recorder_, true); + RetryWithBackoff(true); + return; + } + + if (!response_proto.has_android_id() || + !response_proto.has_security_token() || + response_proto.android_id() == 0 || + response_proto.security_token() == 0) { + LOG(ERROR) << "Android ID or security token is 0. Retrying."; + RecordCheckinStatusAndReportUMA(ZERO_ID_OR_TOKEN, recorder_, true); + RetryWithBackoff(true); + return; + } + + RecordCheckinStatusAndReportUMA(SUCCESS, recorder_, false); + UMA_HISTOGRAM_COUNTS("GCM.CheckinRetryCount", + backoff_entry_.failure_count()); + UMA_HISTOGRAM_TIMES("GCM.CheckinCompleteTime", + base::TimeTicks::Now() - request_start_time_); + callback_.Run(response_proto); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/checkin_request.h b/chromium/google_apis/gcm/engine/checkin_request.h new file mode 100644 index 00000000000..ae4a7d43897 --- /dev/null +++ b/chromium/google_apis/gcm/engine/checkin_request.h @@ -0,0 +1,95 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_CHECKIN_REQUEST_H_ +#define GOOGLE_APIS_GCM_ENGINE_CHECKIN_REQUEST_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/protocol/android_checkin.pb.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "net/base/backoff_entry.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "url/gurl.h" + +namespace net { +class URLRequestContextGetter; +} + +namespace gcm { + +class GCMStatsRecorder; + +// Enables making check-in requests with the GCM infrastructure. When called +// with android_id and security_token both set to 0 it is an initial check-in +// used to obtain credentials. These should be persisted and used for subsequent +// check-ins. +class GCM_EXPORT CheckinRequest : public net::URLFetcherDelegate { + public: + // A callback function for the checkin request, accepting |checkin_response| + // protobuf. + typedef base::Callback<void(const checkin_proto::AndroidCheckinResponse& + checkin_response)> CheckinRequestCallback; + + // Checkin request details. + struct GCM_EXPORT RequestInfo { + RequestInfo(uint64 android_id, + uint64 security_token, + const std::string& settings_digest, + const checkin_proto::ChromeBuildProto& chrome_build_proto); + ~RequestInfo(); + + // Android ID of the device. + uint64 android_id; + // Security token of the device. + uint64 security_token; + // Digest of GServices settings on the device. + std::string settings_digest; + // Information of the Chrome build of this device. + checkin_proto::ChromeBuildProto chrome_build_proto; + }; + + CheckinRequest(const GURL& checkin_url, + const RequestInfo& request_info, + const net::BackoffEntry::Policy& backoff_policy, + const CheckinRequestCallback& callback, + net::URLRequestContextGetter* request_context_getter, + GCMStatsRecorder* recorder); + virtual ~CheckinRequest(); + + void Start(); + + // URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + // Schedules a retry attempt, informs the backoff of a previous request's + // failure when |update_backoff| is true. + void RetryWithBackoff(bool update_backoff); + + net::URLRequestContextGetter* request_context_getter_; + CheckinRequestCallback callback_; + + net::BackoffEntry backoff_entry_; + GURL checkin_url_; + scoped_ptr<net::URLFetcher> url_fetcher_; + const RequestInfo request_info_; + base::TimeTicks request_start_time_; + + // Recorder that records GCM activities for debugging purpose. Not owned. + GCMStatsRecorder* recorder_; + + base::WeakPtrFactory<CheckinRequest> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(CheckinRequest); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_CHECKIN_REQUEST_H_ diff --git a/chromium/google_apis/gcm/engine/checkin_request_unittest.cc b/chromium/google_apis/gcm/engine/checkin_request_unittest.cc new file mode 100644 index 00000000000..12e9a601b79 --- /dev/null +++ b/chromium/google_apis/gcm/engine/checkin_request_unittest.cc @@ -0,0 +1,380 @@ +// Copyright 2014 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 <string> + +#include "google_apis/gcm/engine/checkin_request.h" +#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "net/base/backoff_entry.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const net::BackoffEntry::Policy kDefaultBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + // Explicitly set to 1 to skip the delay of the first Retry, as we are not + // trying to test the backoff itself, but rather the fact that retry happens. + 1, + + // Initial delay for exponential back-off in ms. + 15000, // 15 seconds. + + // 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.5, // 50%. + + // Maximum amount of time we are willing to delay our request in ms. + 1000 * 60 * 5, // 5 minutes. + + // 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, +}; + +} + +const uint64 kAndroidId = 42UL; +const uint64 kBlankAndroidId = 999999UL; +const uint64 kBlankSecurityToken = 999999UL; +const char kCheckinURL[] = "http://foo.bar/checkin"; +const char kChromeVersion[] = "Version String"; +const uint64 kSecurityToken = 77; +const char kSettingsDigest[] = "settings_digest"; + +class CheckinRequestTest : public testing::Test { + public: + enum ResponseScenario { + VALID_RESPONSE, // Both android_id and security_token set in response. + MISSING_ANDROID_ID, // android_id is missing. + MISSING_SECURITY_TOKEN, // security_token is missing. + ANDROID_ID_IS_ZER0, // android_id is 0. + SECURITY_TOKEN_IS_ZERO // security_token is 0. + }; + + CheckinRequestTest(); + virtual ~CheckinRequestTest(); + + void FetcherCallback( + const checkin_proto::AndroidCheckinResponse& response); + + void CreateRequest(uint64 android_id, uint64 security_token); + + void SetResponseStatusAndString( + net::HttpStatusCode status_code, + const std::string& response_data); + + void CompleteFetch(); + + void SetResponse(ResponseScenario response_scenario); + + protected: + bool callback_called_; + uint64 android_id_; + uint64 security_token_; + int checkin_device_type_; + base::MessageLoop message_loop_; + net::TestURLFetcherFactory url_fetcher_factory_; + scoped_refptr<net::TestURLRequestContextGetter> url_request_context_getter_; + checkin_proto::ChromeBuildProto chrome_build_proto_; + scoped_ptr<CheckinRequest> request_; + FakeGCMStatsRecorder recorder_; +}; + +CheckinRequestTest::CheckinRequestTest() + : callback_called_(false), + android_id_(kBlankAndroidId), + security_token_(kBlankSecurityToken), + checkin_device_type_(0), + url_request_context_getter_(new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy())) { +} + +CheckinRequestTest::~CheckinRequestTest() {} + +void CheckinRequestTest::FetcherCallback( + const checkin_proto::AndroidCheckinResponse& checkin_response) { + callback_called_ = true; + if (checkin_response.has_android_id()) + android_id_ = checkin_response.android_id(); + if (checkin_response.has_security_token()) + security_token_ = checkin_response.security_token(); +} + +void CheckinRequestTest::CreateRequest(uint64 android_id, + uint64 security_token) { + // First setup a chrome_build protobuf. + chrome_build_proto_.set_platform( + checkin_proto::ChromeBuildProto::PLATFORM_LINUX); + chrome_build_proto_.set_channel( + checkin_proto::ChromeBuildProto::CHANNEL_CANARY); + chrome_build_proto_.set_chrome_version(kChromeVersion); + + CheckinRequest::RequestInfo request_info( + android_id, + security_token, + kSettingsDigest, + chrome_build_proto_); + // Then create a request with that protobuf and specified android_id, + // security_token. + request_.reset(new CheckinRequest( + GURL(kCheckinURL), + request_info, + kDefaultBackoffPolicy, + base::Bind(&CheckinRequestTest::FetcherCallback, base::Unretained(this)), + url_request_context_getter_.get(), + &recorder_)); + + // Setting android_id_ and security_token_ to blank value, not used elsewhere + // in the tests. + callback_called_ = false; + android_id_ = kBlankAndroidId; + security_token_ = kBlankSecurityToken; +} + +void CheckinRequestTest::SetResponseStatusAndString( + net::HttpStatusCode status_code, + const std::string& response_data) { + net::TestURLFetcher* fetcher = + url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + fetcher->set_response_code(status_code); + fetcher->SetResponseString(response_data); +} + +void CheckinRequestTest::CompleteFetch() { + net::TestURLFetcher* fetcher = + url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + fetcher->delegate()->OnURLFetchComplete(fetcher); +} + +void CheckinRequestTest::SetResponse(ResponseScenario response_scenario) { + checkin_proto::AndroidCheckinResponse response; + response.set_stats_ok(true); + + uint64 android_id = response_scenario == ANDROID_ID_IS_ZER0 ? 0 : kAndroidId; + uint64 security_token = + response_scenario == SECURITY_TOKEN_IS_ZERO ? 0 : kSecurityToken; + + if (response_scenario != MISSING_ANDROID_ID) + response.set_android_id(android_id); + + if (response_scenario != MISSING_SECURITY_TOKEN) + response.set_security_token(security_token); + + std::string response_string; + response.SerializeToString(&response_string); + SetResponseStatusAndString(net::HTTP_OK, response_string); +} + +TEST_F(CheckinRequestTest, FetcherDataAndURL) { + CreateRequest(kAndroidId, kSecurityToken); + request_->Start(); + + // Get data sent by request. + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + EXPECT_EQ(GURL(kCheckinURL), fetcher->GetOriginalURL()); + + checkin_proto::AndroidCheckinRequest request_proto; + request_proto.ParseFromString(fetcher->upload_data()); + EXPECT_EQ(kAndroidId, static_cast<uint64>(request_proto.id())); + EXPECT_EQ(kSecurityToken, request_proto.security_token()); + EXPECT_EQ(chrome_build_proto_.platform(), + request_proto.checkin().chrome_build().platform()); + EXPECT_EQ(chrome_build_proto_.chrome_version(), + request_proto.checkin().chrome_build().chrome_version()); + EXPECT_EQ(chrome_build_proto_.channel(), + request_proto.checkin().chrome_build().channel()); + +#if defined(CHROME_OS) + EXPECT_EQ(checkin_proto::DEVICE_CHROME_OS, request_proto.checkin().type()); +#else + EXPECT_EQ(checkin_proto::DEVICE_CHROME_BROWSER, + request_proto.checkin().type()); +#endif + + EXPECT_EQ(kSettingsDigest, request_proto.digest()); +} + +TEST_F(CheckinRequestTest, ResponseBodyEmpty) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, std::string()); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, ResponseBodyCorrupted) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "Corrupted response body"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, ResponseHttpStatusUnauthorized) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, std::string()); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kBlankAndroidId, android_id_); + EXPECT_EQ(kBlankSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, ResponseHttpStatusBadRequest) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_BAD_REQUEST, std::string()); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kBlankAndroidId, android_id_); + EXPECT_EQ(kBlankSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, ResponseHttpStatusNotOK) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_INTERNAL_SERVER_ERROR, std::string()); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, ResponseMissingAndroidId) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponse(MISSING_ANDROID_ID); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, ResponseMissingSecurityToken) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponse(MISSING_SECURITY_TOKEN); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, AndroidIdEqualsZeroInResponse) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponse(ANDROID_ID_IS_ZER0); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, SecurityTokenEqualsZeroInResponse) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponse(SECURITY_TOKEN_IS_ZERO); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, SuccessfulFirstTimeCheckin) { + CreateRequest(0u, 0u); + request_->Start(); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +TEST_F(CheckinRequestTest, SuccessfulSubsequentCheckin) { + CreateRequest(kAndroidId, kSecurityToken); + request_->Start(); + + SetResponse(VALID_RESPONSE); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(kAndroidId, android_id_); + EXPECT_EQ(kSecurityToken, security_token_); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_factory.cc b/chromium/google_apis/gcm/engine/connection_factory.cc index 016e1e2b89c..a4bffd17076 100644 --- a/chromium/google_apis/gcm/engine/connection_factory.cc +++ b/chromium/google_apis/gcm/engine/connection_factory.cc @@ -6,6 +6,9 @@ namespace gcm { +ConnectionFactory::ConnectionListener::ConnectionListener() {} +ConnectionFactory::ConnectionListener::~ConnectionListener() {} + ConnectionFactory::ConnectionFactory() {} ConnectionFactory::~ConnectionFactory() {} diff --git a/chromium/google_apis/gcm/engine/connection_factory.h b/chromium/google_apis/gcm/engine/connection_factory.h index 3cff48299b6..1db02a6ca13 100644 --- a/chromium/google_apis/gcm/engine/connection_factory.h +++ b/chromium/google_apis/gcm/engine/connection_factory.h @@ -5,10 +5,18 @@ #ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_ #define GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_ +#include <string> + #include "base/time/time.h" #include "google_apis/gcm/base/gcm_export.h" #include "google_apis/gcm/engine/connection_handler.h" +class GURL; + +namespace net { +class IPEndPoint; +} + namespace mcs_proto { class LoginRequest; } @@ -23,6 +31,35 @@ class GCM_EXPORT ConnectionFactory { typedef base::Callback<void(mcs_proto::LoginRequest* login_request)> BuildLoginRequestCallback; + // Reasons for triggering a connection reset. Note that these enums are + // consumed by a histogram, so ordering should not be modified. + enum ConnectionResetReason { + LOGIN_FAILURE, // Login response included an error. + CLOSE_COMMAND, // Received a close command. + HEARTBEAT_FAILURE, // Heartbeat was not acknowledged in time. + SOCKET_FAILURE, // net::Socket error. + NETWORK_CHANGE, // NetworkChangeNotifier notified of a network change. + // Count of total number of connection reset reasons. All new reset reasons + // should be added above this line. + CONNECTION_RESET_COUNT, + }; + + // Listener interface to be notified of endpoint connection events. + class GCM_EXPORT ConnectionListener { + public: + ConnectionListener(); + virtual ~ConnectionListener(); + + // Notifies the listener that GCM has performed a handshake with and is now + // actively connected to |current_server|. |ip_endpoint| is the resolved + // ip address/port through which the connection is being made. + virtual void OnConnected(const GURL& current_server, + const net::IPEndPoint& ip_endpoint) = 0; + + // Notifies the listener that the connection has been interrupted. + virtual void OnDisconnected() = 0; + }; + ConnectionFactory(); virtual ~ConnectionFactory(); @@ -53,10 +90,24 @@ class GCM_EXPORT ConnectionFactory { // connection. virtual bool IsEndpointReachable() const = 0; + // Returns a debug string describing the connection state. + virtual std::string GetConnectionStateString() const = 0; + // If in backoff, the time at which the next retry will be made. Otherwise, // a null time, indicating either no attempt to connect has been made or no // backoff is in progress. virtual base::TimeTicks NextRetryAttempt() const = 0; + + // Manually reset the connection. This can occur if an application specific + // event forced a reset (e.g. server sends a close connection response). + // If the last connection was made within kConnectionResetWindowSecs, the old + // backoff is restored, else a new backoff kicks off. + virtual void SignalConnectionReset(ConnectionResetReason reason) = 0; + + // Sets the current connection listener. Only one listener is supported at a + // time, and the listener must either outlive the connection factory or + // call SetConnectionListener(NULL) upon destruction. + virtual void SetConnectionListener(ConnectionListener* listener) = 0; }; } // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.cc b/chromium/google_apis/gcm/engine/connection_factory_impl.cc index 388b9dca5e3..d322ffa0e94 100644 --- a/chromium/google_apis/gcm/engine/connection_factory_impl.cc +++ b/chromium/google_apis/gcm/engine/connection_factory_impl.cc @@ -5,7 +5,10 @@ #include "google_apis/gcm/engine/connection_factory_impl.h" #include "base/message_loop/message_loop.h" +#include "base/metrics/histogram.h" +#include "base/metrics/sparse_histogram.h" #include "google_apis/gcm/engine/connection_handler_impl.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" #include "google_apis/gcm/protocol/mcs.pb.h" #include "net/base/net_errors.h" #include "net/http/http_network_session.h" @@ -22,46 +25,52 @@ namespace { // The amount of time a Socket read should wait before timing out. const int kReadTimeoutMs = 30000; // 30 seconds. -// Backoff policy. -const net::BackoffEntry::Policy kConnectionBackoffPolicy = { - // Number of initial errors (in sequence) to ignore before applying - // exponential back-off rules. - 0, - - // Initial delay for exponential back-off in ms. - 10000, // 10 seconds. - - // 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, -}; +// If a connection is reset after succeeding within this window of time, +// the previous backoff entry is restored (and the connection success is treated +// as if it was transient). +const int kConnectionResetWindowSecs = 10; // 10 seconds. + +// Decides whether the last login was within kConnectionResetWindowSecs of now +// or not. +bool ShouldRestorePreviousBackoff(const base::TimeTicks& login_time, + const base::TimeTicks& now_ticks) { + return !login_time.is_null() && + now_ticks - login_time <= + base::TimeDelta::FromSeconds(kConnectionResetWindowSecs); +} } // namespace ConnectionFactoryImpl::ConnectionFactoryImpl( - const GURL& mcs_endpoint, + const std::vector<GURL>& mcs_endpoints, + const net::BackoffEntry::Policy& backoff_policy, scoped_refptr<net::HttpNetworkSession> network_session, - net::NetLog* net_log) - : mcs_endpoint_(mcs_endpoint), + net::NetLog* net_log, + GCMStatsRecorder* recorder) + : mcs_endpoints_(mcs_endpoints), + next_endpoint_(0), + last_successful_endpoint_(0), + backoff_policy_(backoff_policy), network_session_(network_session), - net_log_(net_log), + bound_net_log_( + net::BoundNetLog::Make(net_log, net::NetLog::SOURCE_SOCKET)), + pac_request_(NULL), + connecting_(false), + waiting_for_backoff_(false), + waiting_for_network_online_(false), + logging_in_(false), + recorder_(recorder), + listener_(NULL), weak_ptr_factory_(this) { + DCHECK_GE(mcs_endpoints_.size(), 1U); } ConnectionFactoryImpl::~ConnectionFactoryImpl() { + net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this); + if (pac_request_) { + network_session_->proxy_service()->CancelPacRequest(pac_request_); + pac_request_ = NULL; + } } void ConnectionFactoryImpl::Initialize( @@ -70,18 +79,18 @@ void ConnectionFactoryImpl::Initialize( const ConnectionHandler::ProtoSentCallback& write_callback) { DCHECK(!connection_handler_); - backoff_entry_ = CreateBackoffEntry(&kConnectionBackoffPolicy); + previous_backoff_ = CreateBackoffEntry(&backoff_policy_); + backoff_entry_ = CreateBackoffEntry(&backoff_policy_); request_builder_ = request_builder; - net::NetworkChangeNotifier::AddIPAddressObserver(this); - net::NetworkChangeNotifier::AddConnectionTypeObserver(this); - connection_handler_.reset( - new ConnectionHandlerImpl( - base::TimeDelta::FromMilliseconds(kReadTimeoutMs), - read_callback, - write_callback, - base::Bind(&ConnectionFactoryImpl::ConnectionHandlerCallback, - weak_ptr_factory_.GetWeakPtr()))); + net::NetworkChangeNotifier::AddNetworkChangeObserver(this); + waiting_for_network_online_ = net::NetworkChangeNotifier::IsOffline(); + connection_handler_ = CreateConnectionHandler( + base::TimeDelta::FromMilliseconds(kReadTimeoutMs), + read_callback, + write_callback, + base::Bind(&ConnectionFactoryImpl::ConnectionHandlerCallback, + weak_ptr_factory_.GetWeakPtr())).Pass(); } ConnectionHandler* ConnectionFactoryImpl::GetConnectionHandler() const { @@ -90,21 +99,41 @@ ConnectionHandler* ConnectionFactoryImpl::GetConnectionHandler() const { void ConnectionFactoryImpl::Connect() { DCHECK(connection_handler_); - DCHECK(!IsEndpointReachable()); + + if (connecting_ || waiting_for_backoff_) + return; // Connection attempt already in progress or pending. + + if (IsEndpointReachable()) + return; // Already connected. + + ConnectWithBackoff(); +} + +void ConnectionFactoryImpl::ConnectWithBackoff() { + // If a canary managed to connect while a backoff expiration was pending, + // just cleanup the internal state. + if (connecting_ || logging_in_ || IsEndpointReachable()) { + waiting_for_backoff_ = false; + return; + } if (backoff_entry_->ShouldRejectRequest()) { DVLOG(1) << "Delaying MCS endpoint connection for " << backoff_entry_->GetTimeUntilRelease().InMilliseconds() << " milliseconds."; + waiting_for_backoff_ = true; + recorder_->RecordConnectionDelayedDueToBackoff( + backoff_entry_->GetTimeUntilRelease().InMilliseconds()); base::MessageLoop::current()->PostDelayedTask( FROM_HERE, - base::Bind(&ConnectionFactoryImpl::Connect, + base::Bind(&ConnectionFactoryImpl::ConnectWithBackoff, weak_ptr_factory_.GetWeakPtr()), - NextRetryAttempt() - base::TimeTicks::Now()); + backoff_entry_->GetTimeUntilRelease()); return; } DVLOG(1) << "Attempting connection to MCS endpoint."; + waiting_for_backoff_ = false; ConnectImpl(); } @@ -112,54 +141,153 @@ bool ConnectionFactoryImpl::IsEndpointReachable() const { return connection_handler_ && connection_handler_->CanSendMessage(); } +std::string ConnectionFactoryImpl::GetConnectionStateString() const { + if (IsEndpointReachable()) + return "CONNECTED"; + if (logging_in_) + return "LOGGING IN"; + if (connecting_) + return "CONNECTING"; + if (waiting_for_backoff_) + return "WAITING FOR BACKOFF"; + if (waiting_for_network_online_) + return "WAITING FOR NETWORK CHANGE"; + return "NOT CONNECTED"; +} + +void ConnectionFactoryImpl::SignalConnectionReset( + ConnectionResetReason reason) { + // A failure can trigger multiple resets, so no need to do anything if a + // connection is already in progress. + if (connecting_) { + DVLOG(1) << "Connection in progress, ignoring reset."; + return; + } + + if (listener_) + listener_->OnDisconnected(); + + UMA_HISTOGRAM_ENUMERATION("GCM.ConnectionResetReason", + reason, + CONNECTION_RESET_COUNT); + recorder_->RecordConnectionResetSignaled(reason); + if (!last_login_time_.is_null()) { + UMA_HISTOGRAM_CUSTOM_TIMES("GCM.ConnectionUpTime", + NowTicks() - last_login_time_, + base::TimeDelta::FromSeconds(1), + base::TimeDelta::FromHours(24), + 50); + // |last_login_time_| will be reset below, before attempting the new + // connection. + } + + CloseSocket(); + DCHECK(!IsEndpointReachable()); + + // TODO(zea): if the network is offline, don't attempt to connect. + // See crbug.com/396687 + + // Network changes get special treatment as they can trigger a one-off canary + // request that bypasses backoff (but does nothing if a connection is in + // progress). Other connection reset events can be ignored as a connection + // is already awaiting backoff expiration. + if (waiting_for_backoff_ && reason != NETWORK_CHANGE) { + DVLOG(1) << "Backoff expiration pending, ignoring reset."; + return; + } + + if (logging_in_) { + // Failures prior to login completion just reuse the existing backoff entry. + logging_in_ = false; + backoff_entry_->InformOfRequest(false); + } else if (reason == LOGIN_FAILURE || + ShouldRestorePreviousBackoff(last_login_time_, NowTicks())) { + // Failures due to login, or within the reset window of a login, restore + // the backoff entry that was saved off at login completion time. + backoff_entry_.swap(previous_backoff_); + backoff_entry_->InformOfRequest(false); + } else if (reason == NETWORK_CHANGE) { + ConnectImpl(); // Canary attempts bypass backoff without resetting it. + return; + } else { + // We shouldn't be in backoff in thise case. + DCHECK_EQ(0, backoff_entry_->failure_count()); + } + + // At this point the last login time has been consumed or deemed irrelevant, + // reset it. + last_login_time_ = base::TimeTicks(); + + Connect(); +} + +void ConnectionFactoryImpl::SetConnectionListener( + ConnectionListener* listener) { + listener_ = listener; +} + base::TimeTicks ConnectionFactoryImpl::NextRetryAttempt() const { if (!backoff_entry_) return base::TimeTicks(); return backoff_entry_->GetReleaseTime(); } -void ConnectionFactoryImpl::OnConnectionTypeChanged( +void ConnectionFactoryImpl::OnNetworkChanged( net::NetworkChangeNotifier::ConnectionType type) { - if (type == net::NetworkChangeNotifier::CONNECTION_NONE) + if (type == net::NetworkChangeNotifier::CONNECTION_NONE) { + DVLOG(1) << "Network lost, resettion connection."; + waiting_for_network_online_ = true; + + // Will do nothing due to |waiting_for_network_online_ == true|. + // TODO(zea): make the above statement actually true. See crbug.com/396687 + SignalConnectionReset(NETWORK_CHANGE); return; + } - // TODO(zea): implement different backoff/retry policies based on connection - // type. - DVLOG(1) << "Connection type changed to " << type << ", resetting backoff."; - backoff_entry_->Reset(); - // Connect(..) should be retrying with backoff already if a connection is - // necessary, so no need to call again. + DVLOG(1) << "Connection type changed to " << type << ", reconnecting."; + waiting_for_network_online_ = false; + SignalConnectionReset(NETWORK_CHANGE); } -void ConnectionFactoryImpl::OnIPAddressChanged() { - DVLOG(1) << "IP Address changed, resetting backoff."; - backoff_entry_->Reset(); - // Connect(..) should be retrying with backoff already if a connection is - // necessary, so no need to call again. +GURL ConnectionFactoryImpl::GetCurrentEndpoint() const { + // Note that IsEndpointReachable() returns false anytime connecting_ is true, + // so while connecting this always uses |next_endpoint_|. + if (IsEndpointReachable()) + return mcs_endpoints_[last_successful_endpoint_]; + return mcs_endpoints_[next_endpoint_]; } -void ConnectionFactoryImpl::ConnectImpl() { - DCHECK(!IsEndpointReachable()); +net::IPEndPoint ConnectionFactoryImpl::GetPeerIP() { + if (!socket_handle_.socket()) + return net::IPEndPoint(); - // TODO(zea): resolve proxies. - net::ProxyInfo proxy_info; - proxy_info.UseDirect(); - net::SSLConfig ssl_config; - network_session_->ssl_config_service()->GetSSLConfig(&ssl_config); + net::IPEndPoint ip_endpoint; + int result = socket_handle_.socket()->GetPeerAddress(&ip_endpoint); + if (result != net::OK) + return net::IPEndPoint(); - int status = net::InitSocketHandleForTlsConnect( - net::HostPortPair::FromURL(mcs_endpoint_), - network_session_.get(), - proxy_info, - ssl_config, - ssl_config, - net::kPrivacyModeDisabled, - net::BoundNetLog::Make(net_log_, net::NetLog::SOURCE_SOCKET), - &socket_handle_, - base::Bind(&ConnectionFactoryImpl::OnConnectDone, - weak_ptr_factory_.GetWeakPtr())); + return ip_endpoint; +} + +void ConnectionFactoryImpl::ConnectImpl() { + DCHECK(!IsEndpointReachable()); + DCHECK(!socket_handle_.socket()); + + // TODO(zea): if the network is offline, don't attempt to connect. + // See crbug.com/396687 + + connecting_ = true; + GURL current_endpoint = GetCurrentEndpoint(); + recorder_->RecordConnectionInitiated(current_endpoint.host()); + int status = network_session_->proxy_service()->ResolveProxy( + current_endpoint, + &proxy_info_, + base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone, + weak_ptr_factory_.GetWeakPtr()), + &pac_request_, + bound_net_log_); if (status != net::ERR_IO_PENDING) - OnConnectDone(status); + OnProxyResolveDone(status); } void ConnectionFactoryImpl::InitHandler() { @@ -170,7 +298,7 @@ void ConnectionFactoryImpl::InitHandler() { DCHECK(login_request.IsInitialized()); } - connection_handler_->Init(login_request, socket_handle_.PassSocket()); + connection_handler_->Init(login_request, socket_handle_.socket()); } scoped_ptr<net::BackoffEntry> ConnectionFactoryImpl::CreateBackoffEntry( @@ -178,28 +306,238 @@ scoped_ptr<net::BackoffEntry> ConnectionFactoryImpl::CreateBackoffEntry( return scoped_ptr<net::BackoffEntry>(new net::BackoffEntry(policy)); } +scoped_ptr<ConnectionHandler> ConnectionFactoryImpl::CreateConnectionHandler( + base::TimeDelta read_timeout, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback, + const ConnectionHandler::ConnectionChangedCallback& connection_callback) { + return make_scoped_ptr<ConnectionHandler>( + new ConnectionHandlerImpl(read_timeout, + read_callback, + write_callback, + connection_callback)); +} + +base::TimeTicks ConnectionFactoryImpl::NowTicks() { + return base::TimeTicks::Now(); +} + void ConnectionFactoryImpl::OnConnectDone(int result) { if (result != net::OK) { + // If the connection fails, try another proxy. + result = ReconsiderProxyAfterError(result); + // ReconsiderProxyAfterError either returns an error (in which case it is + // not reconsidering a proxy) or returns ERR_IO_PENDING if it is considering + // another proxy. + DCHECK_NE(result, net::OK); + if (result == net::ERR_IO_PENDING) + return; // Proxy reconsideration pending. Return. LOG(ERROR) << "Failed to connect to MCS endpoint with error " << result; + UMA_HISTOGRAM_BOOLEAN("GCM.ConnectionSuccessRate", false); + recorder_->RecordConnectionFailure(result); + CloseSocket(); backoff_entry_->InformOfRequest(false); + UMA_HISTOGRAM_SPARSE_SLOWLY("GCM.ConnectionFailureErrorCode", result); + + // If there are other endpoints available, use the next endpoint on the + // subsequent retry. + next_endpoint_++; + if (next_endpoint_ >= mcs_endpoints_.size()) + next_endpoint_ = 0; + connecting_ = false; Connect(); return; } - DVLOG(1) << "MCS endpoint connection success."; + UMA_HISTOGRAM_BOOLEAN("GCM.ConnectionSuccessRate", true); + UMA_HISTOGRAM_COUNTS("GCM.ConnectionEndpoint", next_endpoint_); + UMA_HISTOGRAM_BOOLEAN("GCM.ConnectedViaProxy", + !(proxy_info_.is_empty() || proxy_info_.is_direct())); + ReportSuccessfulProxyConnection(); + recorder_->RecordConnectionSuccess(); + + // Reset the endpoint back to the default. + // TODO(zea): consider prioritizing endpoints more intelligently based on + // which ones succeed most for this client? Although that will affect + // measuring the success rate of the default endpoint vs fallback. + last_successful_endpoint_ = next_endpoint_; + next_endpoint_ = 0; + connecting_ = false; + logging_in_ = true; + DVLOG(1) << "MCS endpoint socket connection success, starting login."; + InitHandler(); +} - // Reset the backoff. +void ConnectionFactoryImpl::ConnectionHandlerCallback(int result) { + DCHECK(!connecting_); + if (result != net::OK) { + // TODO(zea): Consider how to handle errors that may require some sort of + // user intervention (login page, etc.). + UMA_HISTOGRAM_SPARSE_SLOWLY("GCM.ConnectionDisconnectErrorCode", result); + SignalConnectionReset(SOCKET_FAILURE); + return; + } + + // Handshake complete, reset backoff. If the login failed with an error, + // the client should invoke SignalConnectionReset(LOGIN_FAILURE), which will + // restore the previous backoff. + DVLOG(1) << "Handshake complete."; + last_login_time_ = NowTicks(); + previous_backoff_.swap(backoff_entry_); backoff_entry_->Reset(); + logging_in_ = false; - InitHandler(); + if (listener_) + listener_->OnConnected(GetCurrentEndpoint(), GetPeerIP()); } -void ConnectionFactoryImpl::ConnectionHandlerCallback(int result) { - // TODO(zea): Consider how to handle errors that may require some sort of - // user intervention (login page, etc.). - LOG(ERROR) << "Connection reset with error " << result; - backoff_entry_->InformOfRequest(false); - Connect(); +// This has largely been copied from +// HttpStreamFactoryImpl::Job::DoResolveProxyComplete. This should be +// refactored into some common place. +void ConnectionFactoryImpl::OnProxyResolveDone(int status) { + pac_request_ = NULL; + DVLOG(1) << "Proxy resolution status: " << status; + + DCHECK_NE(status, net::ERR_IO_PENDING); + if (status == net::OK) { + // Remove unsupported proxies from the list. + proxy_info_.RemoveProxiesWithoutScheme( + net::ProxyServer::SCHEME_DIRECT | + net::ProxyServer::SCHEME_HTTP | net::ProxyServer::SCHEME_HTTPS | + net::ProxyServer::SCHEME_SOCKS4 | net::ProxyServer::SCHEME_SOCKS5); + + if (proxy_info_.is_empty()) { + // No proxies/direct to choose from. This happens when we don't support + // any of the proxies in the returned list. + status = net::ERR_NO_SUPPORTED_PROXIES; + } + } + + if (status != net::OK) { + // Failed to resolve proxy. Retry later. + OnConnectDone(status); + return; + } + + DVLOG(1) << "Resolved proxy with PAC:" << proxy_info_.ToPacString(); + + net::SSLConfig ssl_config; + network_session_->ssl_config_service()->GetSSLConfig(&ssl_config); + status = net::InitSocketHandleForTlsConnect( + net::HostPortPair::FromURL(GetCurrentEndpoint()), + network_session_.get(), + proxy_info_, + ssl_config, + ssl_config, + net::PRIVACY_MODE_DISABLED, + bound_net_log_, + &socket_handle_, + base::Bind(&ConnectionFactoryImpl::OnConnectDone, + weak_ptr_factory_.GetWeakPtr())); + if (status != net::ERR_IO_PENDING) + OnConnectDone(status); +} + +// This has largely been copied from +// HttpStreamFactoryImpl::Job::ReconsiderProxyAfterError. This should be +// refactored into some common place. +// This method reconsiders the proxy on certain errors. If it does reconsider +// a proxy it always returns ERR_IO_PENDING and posts a call to +// OnProxyResolveDone with the result of the reconsideration. +int ConnectionFactoryImpl::ReconsiderProxyAfterError(int error) { + DCHECK(!pac_request_); + DCHECK_NE(error, net::OK); + DCHECK_NE(error, net::ERR_IO_PENDING); + // A failure to resolve the hostname or any error related to establishing a + // TCP connection could be grounds for trying a new proxy configuration. + // + // Why do this when a hostname cannot be resolved? Some URLs only make sense + // to proxy servers. The hostname in those URLs might fail to resolve if we + // are still using a non-proxy config. We need to check if a proxy config + // now exists that corresponds to a proxy server that could load the URL. + // + switch (error) { + case net::ERR_PROXY_CONNECTION_FAILED: + case net::ERR_NAME_NOT_RESOLVED: + case net::ERR_INTERNET_DISCONNECTED: + case net::ERR_ADDRESS_UNREACHABLE: + case net::ERR_CONNECTION_CLOSED: + case net::ERR_CONNECTION_TIMED_OUT: + case net::ERR_CONNECTION_RESET: + case net::ERR_CONNECTION_REFUSED: + case net::ERR_CONNECTION_ABORTED: + case net::ERR_TIMED_OUT: + case net::ERR_TUNNEL_CONNECTION_FAILED: + case net::ERR_SOCKS_CONNECTION_FAILED: + // This can happen in the case of trying to talk to a proxy using SSL, and + // ending up talking to a captive portal that supports SSL instead. + case net::ERR_PROXY_CERTIFICATE_INVALID: + // This can happen when trying to talk SSL to a non-SSL server (Like a + // captive portal). + case net::ERR_SSL_PROTOCOL_ERROR: + break; + case net::ERR_SOCKS_CONNECTION_HOST_UNREACHABLE: + // Remap the SOCKS-specific "host unreachable" error to a more + // generic error code (this way consumers like the link doctor + // know to substitute their error page). + // + // Note that if the host resolving was done by the SOCKS5 proxy, we can't + // differentiate between a proxy-side "host not found" versus a proxy-side + // "address unreachable" error, and will report both of these failures as + // ERR_ADDRESS_UNREACHABLE. + return net::ERR_ADDRESS_UNREACHABLE; + default: + return error; + } + + net::SSLConfig ssl_config; + network_session_->ssl_config_service()->GetSSLConfig(&ssl_config); + if (proxy_info_.is_https() && ssl_config.send_client_cert) { + network_session_->ssl_client_auth_cache()->Remove( + proxy_info_.proxy_server().host_port_pair()); + } + + int status = network_session_->proxy_service()->ReconsiderProxyAfterError( + GetCurrentEndpoint(), error, &proxy_info_, + base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone, + weak_ptr_factory_.GetWeakPtr()), + &pac_request_, + bound_net_log_); + if (status == net::OK || status == net::ERR_IO_PENDING) { + CloseSocket(); + } else { + // If ReconsiderProxyAfterError() failed synchronously, it means + // there was nothing left to fall-back to, so fail the transaction + // with the last connection error we got. + status = error; + } + + // If there is new proxy info, post OnProxyResolveDone to retry it. Otherwise, + // if there was an error falling back, fail synchronously. + if (status == net::OK) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone, + weak_ptr_factory_.GetWeakPtr(), status)); + status = net::ERR_IO_PENDING; + } + return status; +} + +void ConnectionFactoryImpl::ReportSuccessfulProxyConnection() { + if (network_session_ && network_session_->proxy_service()) + network_session_->proxy_service()->ReportSuccess(proxy_info_); +} + +void ConnectionFactoryImpl::CloseSocket() { + // The connection handler needs to be reset, else it'll attempt to keep using + // the destroyed socket. + if (connection_handler_) + connection_handler_->Reset(); + + if (socket_handle_.socket() && socket_handle_.socket()->IsConnected()) + socket_handle_.socket()->Disconnect(); + socket_handle_.Reset(); } } // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.h b/chromium/google_apis/gcm/engine/connection_factory_impl.h index d807270bfdc..80fbc94ea8c 100644 --- a/chromium/google_apis/gcm/engine/connection_factory_impl.h +++ b/chromium/google_apis/gcm/engine/connection_factory_impl.h @@ -8,9 +8,12 @@ #include "google_apis/gcm/engine/connection_factory.h" #include "base/memory/weak_ptr.h" +#include "base/time/time.h" #include "google_apis/gcm/protocol/mcs.pb.h" #include "net/base/backoff_entry.h" #include "net/base/network_change_notifier.h" +#include "net/proxy/proxy_info.h" +#include "net/proxy/proxy_service.h" #include "net/socket/client_socket_handle.h" #include "url/gurl.h" @@ -22,16 +25,18 @@ class NetLog; namespace gcm { class ConnectionHandlerImpl; +class GCMStatsRecorder; class GCM_EXPORT ConnectionFactoryImpl : public ConnectionFactory, - public net::NetworkChangeNotifier::ConnectionTypeObserver, - public net::NetworkChangeNotifier::IPAddressObserver { + public net::NetworkChangeNotifier::NetworkChangeObserver { public: ConnectionFactoryImpl( - const GURL& mcs_endpoint, + const std::vector<GURL>& mcs_endpoints, + const net::BackoffEntry::Policy& backoff_policy, scoped_refptr<net::HttpNetworkSession> network_session, - net::NetLog* net_log); + net::NetLog* net_log, + GCMStatsRecorder* recorder); virtual ~ConnectionFactoryImpl(); // ConnectionFactory implementation. @@ -42,12 +47,23 @@ class GCM_EXPORT ConnectionFactoryImpl : virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE; virtual void Connect() OVERRIDE; virtual bool IsEndpointReachable() const OVERRIDE; + virtual std::string GetConnectionStateString() const OVERRIDE; virtual base::TimeTicks NextRetryAttempt() const OVERRIDE; + virtual void SignalConnectionReset(ConnectionResetReason reason) OVERRIDE; + virtual void SetConnectionListener(ConnectionListener* listener) OVERRIDE; - // NetworkChangeNotifier observer implementations. - virtual void OnConnectionTypeChanged( + // NetworkChangeObserver implementation. + virtual void OnNetworkChanged( net::NetworkChangeNotifier::ConnectionType type) OVERRIDE; - virtual void OnIPAddressChanged() OVERRIDE; + + // Returns the server to which the factory is currently connected, or if + // a connection is currently pending, the server to which the next connection + // attempt will be made. + GURL GetCurrentEndpoint() const; + + // Returns the IPEndpoint to which the factory is currently connected. If no + // connection is active, returns an empty IPEndpoint. + net::IPEndPoint GetPeerIP(); protected: // Implementation of Connect(..). If not in backoff, uses |login_request_| @@ -65,32 +81,98 @@ class GCM_EXPORT ConnectionFactoryImpl : virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry( const net::BackoffEntry::Policy* const policy); + // Helper method for creating the connection handler. + // Virtual for testing. + virtual scoped_ptr<ConnectionHandler> CreateConnectionHandler( + base::TimeDelta read_timeout, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback, + const ConnectionHandler::ConnectionChangedCallback& connection_callback); + + // Returns the current time in Ticks. + // Virtual for testing. + virtual base::TimeTicks NowTicks(); + // Callback for Socket connection completion. void OnConnectDone(int result); - private: // ConnectionHandler callback for connection issues. void ConnectionHandlerCallback(int result); - // The MCS endpoint to make connections to. - const GURL mcs_endpoint_; + private: + // Helper method for checking backoff and triggering a connection as + // necessary. + void ConnectWithBackoff(); + + // Proxy resolution and connection functions. + void OnProxyResolveDone(int status); + void OnProxyConnectDone(int status); + int ReconsiderProxyAfterError(int error); + void ReportSuccessfulProxyConnection(); + + void CloseSocket(); + + // The MCS endpoints to make connections to, sorted in order of priority. + const std::vector<GURL> mcs_endpoints_; + // Index to the endpoint for which a connection should be attempted next. + size_t next_endpoint_; + // Index to the endpoint that was last successfully connected. + size_t last_successful_endpoint_; + + // The backoff policy to use. + const net::BackoffEntry::Policy backoff_policy_; // ---- net:: components for establishing connections. ---- // Network session for creating new connections. const scoped_refptr<net::HttpNetworkSession> network_session_; // Net log to use in connection attempts. - net::NetLog* const net_log_; + net::BoundNetLog bound_net_log_; + // The current PAC request, if one exists. Owned by the proxy service. + net::ProxyService::PacRequest* pac_request_; + // The current proxy info. + net::ProxyInfo proxy_info_; // The handle to the socket for the current connection, if one exists. net::ClientSocketHandle socket_handle_; - // Connection attempt backoff policy. + // Current backoff entry. scoped_ptr<net::BackoffEntry> backoff_entry_; + // Backoff entry from previous connection attempt. Updated on each login + // completion. + scoped_ptr<net::BackoffEntry> previous_backoff_; + + // Whether a connection attempt is currently actively in progress. + bool connecting_; + + // Whether the client is waiting for backoff to finish before attempting to + // connect. Canary jobs are able to preempt connections pending backoff + // expiration. + bool waiting_for_backoff_; + + // Whether the NetworkChangeNotifier has informed the client that there is + // no current connection. No connection attempts will be made until the + // client is informed of a valid connection type. + bool waiting_for_network_online_; + + // Whether login successfully completed after the connection was established. + // If a connection reset happens while attempting to log in, the current + // backoff entry is reused (after incrementing with a new failure). + bool logging_in_; + + // The time of the last login completion. Used for calculating whether to + // restore a previous backoff entry and for measuring uptime. + base::TimeTicks last_login_time_; // The current connection handler, if one exists. - scoped_ptr<ConnectionHandlerImpl> connection_handler_; + scoped_ptr<ConnectionHandler> connection_handler_; // Builder for generating new login requests. BuildLoginRequestCallback request_builder_; + // Recorder that records GCM activities for debugging purpose. Not owned. + GCMStatsRecorder* recorder_; + + // Listener for connection change events. + ConnectionListener* listener_; + base::WeakPtrFactory<ConnectionFactoryImpl> weak_ptr_factory_; DISALLOW_COPY_AND_ASSIGN(ConnectionFactoryImpl); diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc index 1e0ccefef26..fda42a1f93f 100644 --- a/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc +++ b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc @@ -8,7 +8,10 @@ #include "base/message_loop/message_loop.h" #include "base/run_loop.h" -#include "base/time/time.h" +#include "base/test/simple_test_tick_clock.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/engine/fake_connection_handler.h" +#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h" #include "net/base/backoff_entry.h" #include "net/http/http_network_session.h" #include "testing/gtest/include/gtest/gtest.h" @@ -19,6 +22,7 @@ namespace gcm { namespace { const char kMCSEndpoint[] = "http://my.server"; +const char kMCSEndpoint2[] = "http://my.alt.server"; const int kBackoffDelayMs = 1; const int kBackoffMultiplier = 2; @@ -50,6 +54,13 @@ const net::BackoffEntry::Policy kTestBackoffPolicy = { false, }; +std::vector<GURL> BuildEndpoints() { + std::vector<GURL> endpoints; + endpoints.push_back(GURL(kMCSEndpoint)); + endpoints.push_back(GURL(kMCSEndpoint2)); + return endpoints; +} + // Helper for calculating total expected exponential backoff delay given an // arbitrary number of failed attempts. See BackoffEntry::CalculateReleaseTime. double CalculateBackoff(int num_attempts) { @@ -62,15 +73,33 @@ double CalculateBackoff(int num_attempts) { return delay; } -// Helper methods that should never actually be called due to real connections -// being stubbed out. void ReadContinuation( scoped_ptr<google::protobuf::MessageLite> message) { - ADD_FAILURE(); } void WriteContinuation() { - ADD_FAILURE(); +} + +class TestBackoffEntry : public net::BackoffEntry { + public: + explicit TestBackoffEntry(base::SimpleTestTickClock* tick_clock); + virtual ~TestBackoffEntry(); + + virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE; + + private: + base::SimpleTestTickClock* tick_clock_; +}; + +TestBackoffEntry::TestBackoffEntry(base::SimpleTestTickClock* tick_clock) + : BackoffEntry(&kTestBackoffPolicy), + tick_clock_(tick_clock) { +} + +TestBackoffEntry::~TestBackoffEntry() {} + +base::TimeTicks TestBackoffEntry::ImplGetTimeNow() const { + return tick_clock_->NowTicks(); } // A connection factory that stubs out network requests and overrides the @@ -80,18 +109,34 @@ class TestConnectionFactoryImpl : public ConnectionFactoryImpl { TestConnectionFactoryImpl(const base::Closure& finished_callback); virtual ~TestConnectionFactoryImpl(); + void InitializeFactory(); + // Overridden stubs. virtual void ConnectImpl() OVERRIDE; virtual void InitHandler() OVERRIDE; virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry( const net::BackoffEntry::Policy* const policy) OVERRIDE; + virtual scoped_ptr<ConnectionHandler> CreateConnectionHandler( + base::TimeDelta read_timeout, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback, + const ConnectionHandler::ConnectionChangedCallback& connection_callback) + OVERRIDE; + virtual base::TimeTicks NowTicks() OVERRIDE; // Helpers for verifying connection attempts are made. Connection results // must be consumed. void SetConnectResult(int connect_result); void SetMultipleConnectResults(int connect_result, int num_expected_attempts); + // Force a login handshake to be delayed. + void SetDelayLogin(bool delay_login); + + base::SimpleTestTickClock* tick_clock() { return &tick_clock_; } + private: + // Clock for controlling delay. + base::SimpleTestTickClock tick_clock_; // The result to return on the next connect attempt. int connect_result_; // The number of expected connection attempts; @@ -99,17 +144,30 @@ class TestConnectionFactoryImpl : public ConnectionFactoryImpl { // Whether all expected connection attempts have been fulfilled since an // expectation was last set. bool connections_fulfilled_; + // Whether to delay a login handshake completion or not. + bool delay_login_; // Callback to invoke when all connection attempts have been made. base::Closure finished_callback_; + // The current fake connection handler.. + FakeConnectionHandler* fake_handler_; + FakeGCMStatsRecorder dummy_recorder_; }; TestConnectionFactoryImpl::TestConnectionFactoryImpl( const base::Closure& finished_callback) - : ConnectionFactoryImpl(GURL(kMCSEndpoint), NULL, NULL), - connect_result_(net::ERR_UNEXPECTED), - num_expected_attempts_(0), - connections_fulfilled_(true), - finished_callback_(finished_callback) { + : ConnectionFactoryImpl(BuildEndpoints(), + net::BackoffEntry::Policy(), + NULL, + NULL, + &dummy_recorder_), + connect_result_(net::ERR_UNEXPECTED), + num_expected_attempts_(0), + connections_fulfilled_(true), + delay_login_(false), + finished_callback_(finished_callback), + fake_handler_(NULL) { + // Set a non-null time. + tick_clock_.Advance(base::TimeDelta::FromMilliseconds(1)); } TestConnectionFactoryImpl::~TestConnectionFactoryImpl() { @@ -118,8 +176,15 @@ TestConnectionFactoryImpl::~TestConnectionFactoryImpl() { void TestConnectionFactoryImpl::ConnectImpl() { ASSERT_GT(num_expected_attempts_, 0); - + scoped_ptr<mcs_proto::LoginRequest> request(BuildLoginRequest(0, 0, "")); + GetConnectionHandler()->Init(*request, NULL); OnConnectDone(connect_result_); + if (!NextRetryAttempt().is_null()) { + // Advance the time to the next retry time. + base::TimeDelta time_till_retry = + NextRetryAttempt() - tick_clock_.NowTicks(); + tick_clock_.Advance(time_till_retry); + } --num_expected_attempts_; if (num_expected_attempts_ == 0) { connect_result_ = net::ERR_UNEXPECTED; @@ -130,12 +195,29 @@ void TestConnectionFactoryImpl::ConnectImpl() { void TestConnectionFactoryImpl::InitHandler() { EXPECT_NE(connect_result_, net::ERR_UNEXPECTED); + if (!delay_login_) + ConnectionHandlerCallback(net::OK); } scoped_ptr<net::BackoffEntry> TestConnectionFactoryImpl::CreateBackoffEntry( const net::BackoffEntry::Policy* const policy) { - return scoped_ptr<net::BackoffEntry>( - new net::BackoffEntry(&kTestBackoffPolicy)); + return scoped_ptr<net::BackoffEntry>(new TestBackoffEntry(&tick_clock_)); +} + +scoped_ptr<ConnectionHandler> +TestConnectionFactoryImpl::CreateConnectionHandler( + base::TimeDelta read_timeout, + const ConnectionHandler::ProtoReceivedCallback& read_callback, + const ConnectionHandler::ProtoSentCallback& write_callback, + const ConnectionHandler::ConnectionChangedCallback& connection_callback) { + fake_handler_ = new FakeConnectionHandler( + base::Bind(&ReadContinuation), + base::Bind(&WriteContinuation)); + return make_scoped_ptr<ConnectionHandler>(fake_handler_); +} + +base::TimeTicks TestConnectionFactoryImpl::NowTicks() { + return tick_clock_.NowTicks(); } void TestConnectionFactoryImpl::SetConnectResult(int connect_result) { @@ -144,6 +226,10 @@ void TestConnectionFactoryImpl::SetConnectResult(int connect_result) { connections_fulfilled_ = false; connect_result_ = connect_result; num_expected_attempts_ = 1; + fake_handler_->ExpectOutgoingMessage( + MCSMessage(kLoginRequestTag, + BuildLoginRequest(0, 0, "").PassAs< + const google::protobuf::MessageLite>())); } void TestConnectionFactoryImpl::SetMultipleConnectResults( @@ -155,29 +241,58 @@ void TestConnectionFactoryImpl::SetMultipleConnectResults( connections_fulfilled_ = false; connect_result_ = connect_result; num_expected_attempts_ = num_expected_attempts; + for (int i = 0 ; i < num_expected_attempts; ++i) { + fake_handler_->ExpectOutgoingMessage( + MCSMessage(kLoginRequestTag, + BuildLoginRequest(0, 0, "").PassAs< + const google::protobuf::MessageLite>())); + } +} + +void TestConnectionFactoryImpl::SetDelayLogin(bool delay_login) { + delay_login_ = delay_login; + fake_handler_->set_fail_login(delay_login_); } -class ConnectionFactoryImplTest : public testing::Test { +} // namespace + +class ConnectionFactoryImplTest + : public testing::Test, + public ConnectionFactory::ConnectionListener { public: ConnectionFactoryImplTest(); virtual ~ConnectionFactoryImplTest(); TestConnectionFactoryImpl* factory() { return &factory_; } + GURL& connected_server() { return connected_server_; } void WaitForConnections(); + // ConnectionFactory::ConnectionListener + virtual void OnConnected(const GURL& current_server, + const net::IPEndPoint& ip_endpoint) OVERRIDE; + virtual void OnDisconnected() OVERRIDE; + private: void ConnectionsComplete(); TestConnectionFactoryImpl factory_; base::MessageLoop message_loop_; scoped_ptr<base::RunLoop> run_loop_; + + GURL connected_server_; }; ConnectionFactoryImplTest::ConnectionFactoryImplTest() : factory_(base::Bind(&ConnectionFactoryImplTest::ConnectionsComplete, base::Unretained(this))), - run_loop_(new base::RunLoop()) {} + run_loop_(new base::RunLoop()) { + factory()->SetConnectionListener(this); + factory()->Initialize( + ConnectionFactory::BuildLoginRequestCallback(), + ConnectionHandler::ProtoReceivedCallback(), + ConnectionHandler::ProtoSentCallback()); +} ConnectionFactoryImplTest::~ConnectionFactoryImplTest() {} void ConnectionFactoryImplTest::WaitForConnections() { @@ -191,73 +306,75 @@ void ConnectionFactoryImplTest::ConnectionsComplete() { run_loop_->Quit(); } +void ConnectionFactoryImplTest::OnConnected( + const GURL& current_server, + const net::IPEndPoint& ip_endpoint) { + connected_server_ = current_server; +} + +void ConnectionFactoryImplTest::OnDisconnected() { + connected_server_ = GURL(); +} + // Verify building a connection handler works. TEST_F(ConnectionFactoryImplTest, Initialize) { - EXPECT_FALSE(factory()->IsEndpointReachable()); - factory()->Initialize( - ConnectionFactory::BuildLoginRequestCallback(), - base::Bind(&ReadContinuation), - base::Bind(&WriteContinuation)); ConnectionHandler* handler = factory()->GetConnectionHandler(); ASSERT_TRUE(handler); EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_FALSE(connected_server().is_valid()); } // An initial successful connection should not result in backoff. TEST_F(ConnectionFactoryImplTest, ConnectSuccess) { - factory()->Initialize( - ConnectionFactory::BuildLoginRequestCallback(), - ConnectionHandler::ProtoReceivedCallback(), - ConnectionHandler::ProtoSentCallback()); factory()->SetConnectResult(net::OK); factory()->Connect(); EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + EXPECT_EQ(factory()->GetCurrentEndpoint(), BuildEndpoints()[0]); + EXPECT_TRUE(factory()->IsEndpointReachable()); + EXPECT_TRUE(connected_server().is_valid()); } -// A connection failure should result in backoff. +// A connection failure should result in backoff, and attempting the fallback +// endpoint next. TEST_F(ConnectionFactoryImplTest, ConnectFail) { - factory()->Initialize( - ConnectionFactory::BuildLoginRequestCallback(), - ConnectionHandler::ProtoReceivedCallback(), - ConnectionHandler::ProtoSentCallback()); factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); factory()->Connect(); EXPECT_FALSE(factory()->NextRetryAttempt().is_null()); + EXPECT_EQ(factory()->GetCurrentEndpoint(), BuildEndpoints()[1]); + EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_FALSE(connected_server().is_valid()); } // A connection success after a failure should reset backoff. TEST_F(ConnectionFactoryImplTest, FailThenSucceed) { - factory()->Initialize( - ConnectionFactory::BuildLoginRequestCallback(), - ConnectionHandler::ProtoReceivedCallback(), - ConnectionHandler::ProtoSentCallback()); factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); - base::TimeTicks connect_time = base::TimeTicks::Now(); + base::TimeTicks connect_time = factory()->tick_clock()->NowTicks(); factory()->Connect(); WaitForConnections(); + EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_FALSE(connected_server().is_valid()); base::TimeTicks retry_time = factory()->NextRetryAttempt(); EXPECT_FALSE(retry_time.is_null()); EXPECT_GE((retry_time - connect_time).InMilliseconds(), CalculateBackoff(1)); factory()->SetConnectResult(net::OK); WaitForConnections(); EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + EXPECT_TRUE(factory()->IsEndpointReachable()); + EXPECT_TRUE(connected_server().is_valid()); } // Multiple connection failures should retry with an exponentially increasing // backoff, then reset on success. TEST_F(ConnectionFactoryImplTest, MultipleFailuresThenSucceed) { - factory()->Initialize( - ConnectionFactory::BuildLoginRequestCallback(), - ConnectionHandler::ProtoReceivedCallback(), - ConnectionHandler::ProtoSentCallback()); - const int kNumAttempts = 5; factory()->SetMultipleConnectResults(net::ERR_CONNECTION_FAILED, kNumAttempts); - base::TimeTicks connect_time = base::TimeTicks::Now(); + base::TimeTicks connect_time = factory()->tick_clock()->NowTicks(); factory()->Connect(); WaitForConnections(); + EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_FALSE(connected_server().is_valid()); base::TimeTicks retry_time = factory()->NextRetryAttempt(); EXPECT_FALSE(retry_time.is_null()); EXPECT_GE((retry_time - connect_time).InMilliseconds(), @@ -266,38 +383,171 @@ TEST_F(ConnectionFactoryImplTest, MultipleFailuresThenSucceed) { factory()->SetConnectResult(net::OK); WaitForConnections(); EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + EXPECT_TRUE(factory()->IsEndpointReachable()); + EXPECT_TRUE(connected_server().is_valid()); } -// IP events should reset backoff. -TEST_F(ConnectionFactoryImplTest, FailThenIPEvent) { - factory()->Initialize( - ConnectionFactory::BuildLoginRequestCallback(), - ConnectionHandler::ProtoReceivedCallback(), - ConnectionHandler::ProtoSentCallback()); +// Network change events should trigger canary connections. +TEST_F(ConnectionFactoryImplTest, FailThenNetworkChangeEvent) { factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); factory()->Connect(); WaitForConnections(); + base::TimeTicks initial_backoff = factory()->NextRetryAttempt(); + EXPECT_FALSE(initial_backoff.is_null()); + + factory()->SetConnectResult(net::ERR_FAILED); + factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_WIFI); + WaitForConnections(); + + // Backoff should increase. + base::TimeTicks next_backoff = factory()->NextRetryAttempt(); + EXPECT_GT(next_backoff, initial_backoff); + EXPECT_FALSE(factory()->IsEndpointReachable()); +} + +// Verify that we reconnect even if a canary succeeded then disconnected while +// a backoff was pending. +TEST_F(ConnectionFactoryImplTest, CanarySucceedsThenDisconnects) { + factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); + factory()->Connect(); + WaitForConnections(); + base::TimeTicks initial_backoff = factory()->NextRetryAttempt(); + EXPECT_FALSE(initial_backoff.is_null()); + + factory()->SetConnectResult(net::OK); + factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_ETHERNET); + WaitForConnections(); + EXPECT_TRUE(factory()->IsEndpointReachable()); + EXPECT_TRUE(connected_server().is_valid()); + + factory()->SetConnectResult(net::OK); + factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE); + EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_FALSE(connected_server().is_valid()); + WaitForConnections(); + EXPECT_TRUE(factory()->IsEndpointReachable()); + EXPECT_TRUE(connected_server().is_valid()); +} + +// Verify that if a canary connects, but hasn't finished the handshake, a +// pending backoff attempt doesn't interrupt the connection. +TEST_F(ConnectionFactoryImplTest, CanarySucceedsRetryDuringLogin) { + factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); + factory()->Connect(); + WaitForConnections(); + base::TimeTicks initial_backoff = factory()->NextRetryAttempt(); + EXPECT_FALSE(initial_backoff.is_null()); + + factory()->SetDelayLogin(true); + factory()->SetConnectResult(net::OK); + factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_WIFI); + WaitForConnections(); + EXPECT_FALSE(factory()->IsEndpointReachable()); + + // Pump the loop, to ensure the pending backoff retry has no effect. + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::MessageLoop::QuitClosure(), + base::TimeDelta::FromMilliseconds(1)); + WaitForConnections(); +} + +// Fail after successful connection via signal reset. +TEST_F(ConnectionFactoryImplTest, FailViaSignalReset) { + factory()->SetConnectResult(net::OK); + factory()->Connect(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + + factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE); EXPECT_FALSE(factory()->NextRetryAttempt().is_null()); + EXPECT_FALSE(factory()->IsEndpointReachable()); +} - factory()->OnIPAddressChanged(); +TEST_F(ConnectionFactoryImplTest, IgnoreResetWhileConnecting) { + factory()->SetConnectResult(net::OK); + factory()->Connect(); EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + + factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE); + base::TimeTicks retry_time = factory()->NextRetryAttempt(); + EXPECT_FALSE(retry_time.is_null()); + EXPECT_FALSE(factory()->IsEndpointReachable()); + + const int kNumAttempts = 5; + for (int i = 0; i < kNumAttempts; ++i) + factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE); + EXPECT_EQ(retry_time, factory()->NextRetryAttempt()); + EXPECT_FALSE(factory()->IsEndpointReachable()); } -// Connection type events should reset backoff. -TEST_F(ConnectionFactoryImplTest, FailThenConnectionTypeEvent) { - factory()->Initialize( - ConnectionFactory::BuildLoginRequestCallback(), - ConnectionHandler::ProtoReceivedCallback(), - ConnectionHandler::ProtoSentCallback()); +// Go into backoff due to connection failure. On successful connection, receive +// a signal reset. The original backoff should be restored and extended, rather +// than a new backoff starting from scratch. +TEST_F(ConnectionFactoryImplTest, SignalResetRestoresBackoff) { factory()->SetConnectResult(net::ERR_CONNECTION_FAILED); + base::TimeTicks connect_time = factory()->tick_clock()->NowTicks(); factory()->Connect(); WaitForConnections(); - EXPECT_FALSE(factory()->NextRetryAttempt().is_null()); + base::TimeTicks retry_time = factory()->NextRetryAttempt(); + EXPECT_FALSE(retry_time.is_null()); - factory()->OnConnectionTypeChanged( - net::NetworkChangeNotifier::CONNECTION_WIFI); + factory()->SetConnectResult(net::OK); + connect_time = factory()->tick_clock()->NowTicks(); + WaitForConnections(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + + factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE); + EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_FALSE(connected_server().is_valid()); + EXPECT_NE(retry_time, factory()->NextRetryAttempt()); + retry_time = factory()->NextRetryAttempt(); + EXPECT_FALSE(retry_time.is_null()); + EXPECT_GE((retry_time - connect_time).InMilliseconds(), + CalculateBackoff(2)); + + factory()->SetConnectResult(net::OK); + connect_time = factory()->tick_clock()->NowTicks(); + factory()->tick_clock()->Advance( + factory()->NextRetryAttempt() - connect_time); + WaitForConnections(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + EXPECT_TRUE(factory()->IsEndpointReachable()); + EXPECT_TRUE(connected_server().is_valid()); + + factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE); + EXPECT_NE(retry_time, factory()->NextRetryAttempt()); + retry_time = factory()->NextRetryAttempt(); + EXPECT_FALSE(retry_time.is_null()); + EXPECT_GE((retry_time - connect_time).InMilliseconds(), + CalculateBackoff(3)); + EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_FALSE(connected_server().is_valid()); +} + +// When the network is disconnected, close the socket and suppress further +// connection attempts until the network returns. +// Disabled while crbug.com/396687 is being investigated. +TEST_F(ConnectionFactoryImplTest, DISABLED_SuppressConnectWhenNoNetwork) { + factory()->SetConnectResult(net::OK); + factory()->Connect(); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + EXPECT_TRUE(factory()->IsEndpointReachable()); + + // Advance clock so the login window reset isn't encountered. + factory()->tick_clock()->Advance(base::TimeDelta::FromSeconds(11)); + + // Will trigger reset, but will not attempt a new connection. + factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_NONE); + EXPECT_FALSE(factory()->IsEndpointReachable()); + EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); + + // When the network returns, attempt to connect. + factory()->SetConnectResult(net::OK); + factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_4G); + WaitForConnections(); + + EXPECT_TRUE(factory()->IsEndpointReachable()); EXPECT_TRUE(factory()->NextRetryAttempt().is_null()); } -} // namespace } // namespace gcm diff --git a/chromium/google_apis/gcm/engine/connection_handler.h b/chromium/google_apis/gcm/engine/connection_handler.h index 5b9ea715c78..7b265f9d3bc 100644 --- a/chromium/google_apis/gcm/engine/connection_handler.h +++ b/chromium/google_apis/gcm/engine/connection_handler.h @@ -48,7 +48,11 @@ class GCM_EXPORT ConnectionHandler { // Note: It is correct and expected to call Init more than once, as connection // issues are encountered and new connections must be made. virtual void Init(const mcs_proto::LoginRequest& login_request, - scoped_ptr<net::StreamSocket> socket) = 0; + net::StreamSocket* socket) = 0; + + // Resets the handler and any internal state. Should be called any time + // a connection reset happens externally to the handler. + virtual void Reset() = 0; // Checks that a handshake has been completed and a message is not already // in flight. diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.cc b/chromium/google_apis/gcm/engine/connection_handler_impl.cc index aff0dfd3651..d47e0f25b79 100644 --- a/chromium/google_apis/gcm/engine/connection_handler_impl.cc +++ b/chromium/google_apis/gcm/engine/connection_handler_impl.cc @@ -27,8 +27,7 @@ const int kSizePacketLenMin = 1; const int kSizePacketLenMax = 2; // The current MCS protocol version. -// TODO(zea): bump to 41 once the server supports it. -const int kMCSVersion = 38; +const int kMCSVersion = 41; } // namespace @@ -38,6 +37,7 @@ ConnectionHandlerImpl::ConnectionHandlerImpl( const ProtoSentCallback& write_callback, const ConnectionChangedCallback& connection_callback) : read_timeout_(read_timeout), + socket_(NULL), handshake_complete_(false), message_tag_(0), message_size_(0), @@ -52,7 +52,7 @@ ConnectionHandlerImpl::~ConnectionHandlerImpl() { void ConnectionHandlerImpl::Init( const mcs_proto::LoginRequest& login_request, - scoped_ptr<net::StreamSocket> socket) { + net::StreamSocket* socket) { DCHECK(!read_callback_.is_null()); DCHECK(!write_callback_.is_null()); DCHECK(!connection_callback_.is_null()); @@ -63,13 +63,17 @@ void ConnectionHandlerImpl::Init( handshake_complete_ = false; message_tag_ = 0; message_size_ = 0; - socket_ = socket.Pass(); - input_stream_.reset(new SocketInputStream(socket_.get())); - output_stream_.reset(new SocketOutputStream(socket_.get())); + socket_ = socket; + input_stream_.reset(new SocketInputStream(socket_)); + output_stream_.reset(new SocketOutputStream(socket_)); Login(login_request); } +void ConnectionHandlerImpl::Reset() { + CloseConnection(); +} + bool ConnectionHandlerImpl::CanSendMessage() const { return handshake_complete_ && output_stream_.get() && output_stream_->GetState() == SocketOutputStream::EMPTY; @@ -180,9 +184,9 @@ void ConnectionHandlerImpl::WaitForData(ProcessingState state) { } // Used to determine whether a Socket::Read is necessary. - int min_bytes_needed = 0; + size_t min_bytes_needed = 0; // Used to limit the size of the Socket::Read. - int max_bytes_needed = 0; + size_t max_bytes_needed = 0; switch(state) { case MCS_VERSION_TAG_AND_SIZE: @@ -210,20 +214,20 @@ void ConnectionHandlerImpl::WaitForData(ProcessingState state) { } DCHECK_GE(max_bytes_needed, min_bytes_needed); - int byte_count = input_stream_->UnreadByteCount(); - if (min_bytes_needed - byte_count > 0 && + size_t unread_byte_count = input_stream_->UnreadByteCount(); + if (min_bytes_needed > unread_byte_count && input_stream_->Refresh( base::Bind(&ConnectionHandlerImpl::WaitForData, weak_ptr_factory_.GetWeakPtr(), state), - max_bytes_needed - byte_count) == net::ERR_IO_PENDING) { + max_bytes_needed - unread_byte_count) == net::ERR_IO_PENDING) { return; } // Check for refresh errors. if (input_stream_->GetState() != SocketInputStream::READY) { // An error occurred. - int last_error = output_stream_->last_error(); + int last_error = input_stream_->last_error(); CloseConnection(); // If the socket stream had an error, plumb it up, else plumb up FAILED. if (last_error == net::OK) @@ -232,6 +236,20 @@ void ConnectionHandlerImpl::WaitForData(ProcessingState state) { return; } + // Check whether read is complete, or needs to be continued ( + // SocketInputStream::Refresh can finish without reading all the data). + if (input_stream_->UnreadByteCount() < min_bytes_needed) { + DVLOG(1) << "Socket read finished prematurely. Waiting for " + << min_bytes_needed - input_stream_->UnreadByteCount() + << " more bytes."; + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ConnectionHandlerImpl::WaitForData, + weak_ptr_factory_.GetWeakPtr(), + MCS_PROTO_BYTES)); + return; + } + // Received enough bytes, process them. DVLOG(1) << "Processing MCS data: state == " << state; switch(state) { @@ -258,7 +276,8 @@ void ConnectionHandlerImpl::OnGotVersion() { CodedInputStream coded_input_stream(input_stream_.get()); coded_input_stream.ReadRaw(&version, 1); } - if (version < kMCSVersion) { + // TODO(zea): remove this when the server is ready. + if (version < kMCSVersion && version != 38) { LOG(ERROR) << "Invalid GCM version response: " << static_cast<int>(version); connection_callback_.Run(net::ERR_FAILED); return; @@ -352,18 +371,18 @@ void ConnectionHandlerImpl::OnGotMessageBytes() { input_stream_->GetState() != SocketInputStream::READY) { LOG(ERROR) << "Failed to extract protobuf bytes of type " << static_cast<unsigned int>(message_tag_); - protobuf.reset(); // Return a null pointer to denote an error. - read_callback_.Run(protobuf.Pass()); + // Reset the connection. + connection_callback_.Run(net::ERR_FAILED); return; } { CodedInputStream coded_input_stream(input_stream_.get()); if (!protobuf->ParsePartialFromCodedStream(&coded_input_stream)) { - NOTREACHED() << "Unable to parse GCM message of type " - << static_cast<unsigned int>(message_tag_); - protobuf.reset(); // Return a null pointer to denote an error. - read_callback_.Run(protobuf.Pass()); + LOG(ERROR) << "Unable to parse GCM message of type " + << static_cast<unsigned int>(message_tag_); + // Reset the connection. + connection_callback_.Run(net::ERR_FAILED); return; } } @@ -379,6 +398,7 @@ void ConnectionHandlerImpl::OnGotMessageBytes() { } else { handshake_complete_ = true; DVLOG(1) << "GCM Handshake complete."; + connection_callback_.Run(net::OK); } } read_callback_.Run(protobuf.Pass()); @@ -392,10 +412,13 @@ void ConnectionHandlerImpl::OnTimeout() { void ConnectionHandlerImpl::CloseConnection() { DVLOG(1) << "Closing connection."; - read_callback_.Reset(); - write_callback_.Reset(); read_timeout_timer_.Stop(); - socket_->Disconnect(); + if (socket_) + socket_->Disconnect(); + socket_ = NULL; + handshake_complete_ = false; + message_tag_ = 0; + message_size_ = 0; input_stream_.reset(); output_stream_.reset(); weak_ptr_factory_.InvalidateWeakPtrs(); diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.h b/chromium/google_apis/gcm/engine/connection_handler_impl.h index 110cdcdddda..9f9daa94e39 100644 --- a/chromium/google_apis/gcm/engine/connection_handler_impl.h +++ b/chromium/google_apis/gcm/engine/connection_handler_impl.h @@ -34,7 +34,8 @@ class GCM_EXPORT ConnectionHandlerImpl : public ConnectionHandler { // ConnectionHandler implementation. virtual void Init(const mcs_proto::LoginRequest& login_request, - scoped_ptr<net::StreamSocket> socket) OVERRIDE; + net::StreamSocket* socket) OVERRIDE; + virtual void Reset() OVERRIDE; virtual bool CanSendMessage() const OVERRIDE; virtual void SendMessage(const google::protobuf::MessageLite& message) OVERRIDE; @@ -96,7 +97,7 @@ class GCM_EXPORT ConnectionHandlerImpl : public ConnectionHandler { base::OneShotTimer<ConnectionHandlerImpl> read_timeout_timer_; // This connection's socket and the input/output streams attached to it. - scoped_ptr<net::StreamSocket> socket_; + net::StreamSocket* socket_; scoped_ptr<SocketInputStream> input_stream_; scoped_ptr<SocketOutputStream> output_stream_; diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc index 0cdcdc621ff..6b89644462c 100644 --- a/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc +++ b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc @@ -27,7 +27,7 @@ typedef std::vector<net::MockWrite> WriteList; const uint64 kAuthId = 54321; const uint64 kAuthToken = 12345; -const char kMCSVersion = 38; // The protocol version. +const char kMCSVersion = 41; // The protocol version. const int kMCSPort = 5228; // The server port. const char kDataMsgFrom[] = "data_from"; const char kDataMsgCategory[] = "data_category"; @@ -49,12 +49,16 @@ const char kDataMsgCategoryLong2[] = std::string EncodePacket(uint8 tag, const std::string& proto) { std::string result; google::protobuf::io::StringOutputStream string_output_stream(&result); - google::protobuf::io::CodedOutputStream coded_output_stream( + { + google::protobuf::io::CodedOutputStream coded_output_stream( &string_output_stream); - const unsigned char tag_byte[1] = {tag}; - coded_output_stream.WriteRaw(tag_byte, 1); - coded_output_stream.WriteVarint32(proto.size()); - coded_output_stream.WriteRaw(proto.c_str(), proto.size()); + const unsigned char tag_byte[1] = { tag }; + coded_output_stream.WriteRaw(tag_byte, 1); + coded_output_stream.WriteVarint32(proto.size()); + coded_output_stream.WriteRaw(proto.c_str(), proto.size()); + // ~CodedOutputStream must run before the move constructor at the + // return statement. http://crbug.com/338962 + } return result; } @@ -63,7 +67,8 @@ std::string EncodeHandshakeRequest() { std::string result; const char version_byte[1] = {kMCSVersion}; result.append(version_byte, 1); - ScopedMessage login_request(BuildLoginRequest(kAuthId, kAuthToken)); + ScopedMessage login_request( + BuildLoginRequest(kAuthId, kAuthToken, "")); result.append(EncodePacket(kLoginRequestTag, login_request->SerializeAsString())); return result; @@ -198,8 +203,9 @@ void GCMConnectionHandlerImplTest::Connect( base::Bind(&GCMConnectionHandlerImplTest::ConnectionContinuation, base::Unretained(this)))); EXPECT_FALSE(connection_handler()->CanSendMessage()); - connection_handler_->Init(*BuildLoginRequest(kAuthId, kAuthToken), - socket_.Pass()); + connection_handler_->Init( + *BuildLoginRequest(kAuthId, kAuthToken, ""), + socket_.get()); } void GCMConnectionHandlerImplTest::ReadContinuation( @@ -376,6 +382,7 @@ TEST_F(GCMConnectionHandlerImplTest, RecvMsg) { WaitForMessage(); // The data message. ASSERT_TRUE(received_message.get()); EXPECT_EQ(data_message_proto, received_message->SerializeAsString()); + EXPECT_EQ(net::OK, last_error()); } // Verify that if two messages arrive at once, they're treated appropriately. @@ -413,6 +420,7 @@ TEST_F(GCMConnectionHandlerImplTest, Recv2Msgs) { WaitForMessage(); // The second data message. ASSERT_TRUE(received_message.get()); EXPECT_EQ(data_message_proto2, received_message->SerializeAsString()); + EXPECT_EQ(net::OK, last_error()); } // Receive a long (>128 bytes) message. @@ -444,6 +452,46 @@ TEST_F(GCMConnectionHandlerImplTest, RecvLongMsg) { WaitForMessage(); // The data message. ASSERT_TRUE(received_message.get()); EXPECT_EQ(data_message_proto, received_message->SerializeAsString()); + EXPECT_EQ(net::OK, last_error()); +} + +// Receive a long (>128 bytes) message in two synchronous parts. +TEST_F(GCMConnectionHandlerImplTest, RecvLongMsg2Parts) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + std::string data_message_proto = + BuildDataMessage(kDataMsgFromLong, kDataMsgCategoryLong); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message_proto); + DCHECK_GT(data_message_pkt.size(), 128U); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + + int bytes_in_first_message = data_message_pkt.size() / 2; + read_list.push_back(net::MockRead(net::SYNCHRONOUS, + data_message_pkt.c_str(), + bytes_in_first_message)); + read_list.push_back(net::MockRead(net::SYNCHRONOUS, + data_message_pkt.c_str() + + bytes_in_first_message, + data_message_pkt.size() - + bytes_in_first_message)); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + WaitForMessage(); // The data message. + ASSERT_TRUE(received_message.get()); + EXPECT_EQ(net::OK, last_error()); + EXPECT_EQ(data_message_proto, received_message->SerializeAsString()); } // Receive two long (>128 bytes) message. @@ -482,6 +530,7 @@ TEST_F(GCMConnectionHandlerImplTest, Recv2LongMsgs) { WaitForMessage(); // The second data message. ASSERT_TRUE(received_message.get()); EXPECT_EQ(data_message_proto2, received_message->SerializeAsString()); + EXPECT_EQ(net::OK, last_error()); } // Simulate a message where the end of the data does not arrive in time and the @@ -624,5 +673,37 @@ TEST_F(GCMConnectionHandlerImplTest, SendMsgSocketDisconnected) { EXPECT_EQ(net::ERR_CONNECTION_CLOSED, last_error()); } +// Receive a message whose size field was corrupted and is larger than the +// socket's buffer. Should fail gracefully. +TEST_F(GCMConnectionHandlerImplTest, CorruptedSize) { + std::string handshake_request = EncodeHandshakeRequest(); + WriteList write_list(1, net::MockWrite(net::ASYNC, + handshake_request.c_str(), + handshake_request.size())); + std::string handshake_response = EncodeHandshakeResponse(); + + // Fill a string with 9000 character zero. + std::string data_message_proto(9000, '0'); + std::string data_message_pkt = + EncodePacket(kDataMessageStanzaTag, data_message_proto); + ReadList read_list; + read_list.push_back(net::MockRead(net::ASYNC, + handshake_response.c_str(), + handshake_response.size())); + read_list.push_back(net::MockRead(net::ASYNC, + data_message_pkt.c_str(), + data_message_pkt.size())); + BuildSocket(read_list, write_list); + + ScopedMessage received_message; + Connect(&received_message); + WaitForMessage(); // The login send. + WaitForMessage(); // The login response. + received_message.reset(); + WaitForMessage(); // The data message. + EXPECT_FALSE(received_message.get()); + EXPECT_EQ(net::ERR_FILE_TOO_BIG, last_error()); +} + } // namespace } // namespace gcm diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.cc b/chromium/google_apis/gcm/engine/fake_connection_factory.cc index 54b3423b2d5..51447b9e5a8 100644 --- a/chromium/google_apis/gcm/engine/fake_connection_factory.cc +++ b/chromium/google_apis/gcm/engine/fake_connection_factory.cc @@ -10,7 +10,9 @@ namespace gcm { -FakeConnectionFactory::FakeConnectionFactory() { +FakeConnectionFactory::FakeConnectionFactory() + : reconnect_pending_(false), + delay_reconnect_(false) { } FakeConnectionFactory::~FakeConnectionFactory() { @@ -32,15 +34,31 @@ ConnectionHandler* FakeConnectionFactory::GetConnectionHandler() const { void FakeConnectionFactory::Connect() { mcs_proto::LoginRequest login_request; request_builder_.Run(&login_request); - connection_handler_->Init(login_request, scoped_ptr<net::StreamSocket>()); + connection_handler_->Init(login_request, NULL); } bool FakeConnectionFactory::IsEndpointReachable() const { return connection_handler_.get() && connection_handler_->CanSendMessage(); } +std::string FakeConnectionFactory::GetConnectionStateString() const { + return ""; +} + base::TimeTicks FakeConnectionFactory::NextRetryAttempt() const { return base::TimeTicks(); } +void FakeConnectionFactory::SignalConnectionReset( + ConnectionResetReason reason) { + if (!delay_reconnect_) + Connect(); + else + reconnect_pending_ = true; +} + +void FakeConnectionFactory::SetConnectionListener( + ConnectionListener* listener) { +} + } // namespace gcm diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.h b/chromium/google_apis/gcm/engine/fake_connection_factory.h index 60b10e130db..b4f0e884d5d 100644 --- a/chromium/google_apis/gcm/engine/fake_connection_factory.h +++ b/chromium/google_apis/gcm/engine/fake_connection_factory.h @@ -27,13 +27,29 @@ class FakeConnectionFactory : public ConnectionFactory { virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE; virtual void Connect() OVERRIDE; virtual bool IsEndpointReachable() const OVERRIDE; + virtual std::string GetConnectionStateString() const OVERRIDE; virtual base::TimeTicks NextRetryAttempt() const OVERRIDE; + virtual void SignalConnectionReset(ConnectionResetReason reason) OVERRIDE; + virtual void SetConnectionListener(ConnectionListener* listener) OVERRIDE; + + // Whether a connection reset has been triggered and is yet to run. + bool reconnect_pending() const { return reconnect_pending_; } + + // Whether connection resets should be handled immediately or delayed until + // release. + void set_delay_reconnect(bool should_delay) { + delay_reconnect_ = should_delay; + } private: scoped_ptr<FakeConnectionHandler> connection_handler_; BuildLoginRequestCallback request_builder_; + // Logic for handling connection resets. + bool reconnect_pending_; + bool delay_reconnect_; + DISALLOW_COPY_AND_ASSIGN(FakeConnectionFactory); }; diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.cc b/chromium/google_apis/gcm/engine/fake_connection_handler.cc index 06639331ebe..3e5d5ac2770 100644 --- a/chromium/google_apis/gcm/engine/fake_connection_handler.cc +++ b/chromium/google_apis/gcm/engine/fake_connection_handler.cc @@ -39,7 +39,8 @@ FakeConnectionHandler::~FakeConnectionHandler() { } void FakeConnectionHandler::Init(const mcs_proto::LoginRequest& login_request, - scoped_ptr<net::StreamSocket> socket) { + net::StreamSocket* socket) { + ASSERT_GE(expected_outgoing_messages_.size(), 1U); EXPECT_EQ(expected_outgoing_messages_.front().SerializeAsString(), login_request.SerializeAsString()); expected_outgoing_messages_.pop_front(); @@ -48,6 +49,10 @@ void FakeConnectionHandler::Init(const mcs_proto::LoginRequest& login_request, initialized_ = !fail_login_; } +void FakeConnectionHandler::Reset() { + initialized_ = false; +} + bool FakeConnectionHandler::CanSendMessage() const { return initialized_; } diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.h b/chromium/google_apis/gcm/engine/fake_connection_handler.h index 5356b771086..5229e3fb49d 100644 --- a/chromium/google_apis/gcm/engine/fake_connection_handler.h +++ b/chromium/google_apis/gcm/engine/fake_connection_handler.h @@ -23,7 +23,8 @@ class FakeConnectionHandler : public ConnectionHandler { // ConnectionHandler implementation. virtual void Init(const mcs_proto::LoginRequest& login_request, - scoped_ptr<net::StreamSocket> socket) OVERRIDE; + net::StreamSocket* socket) OVERRIDE; + virtual void Reset() OVERRIDE; virtual bool CanSendMessage() const OVERRIDE; virtual void SendMessage(const google::protobuf::MessageLite& message) OVERRIDE; diff --git a/chromium/google_apis/gcm/engine/gcm_store.cc b/chromium/google_apis/gcm/engine/gcm_store.cc new file mode 100644 index 00000000000..91ad60fe852 --- /dev/null +++ b/chromium/google_apis/gcm/engine/gcm_store.cc @@ -0,0 +1,21 @@ +// Copyright 2014 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 "google_apis/gcm/engine/gcm_store.h" + +namespace gcm { + +GCMStore::LoadResult::LoadResult() + : success(false), + device_android_id(0), + device_security_token(0) { +} + +GCMStore::LoadResult::~LoadResult() {} + +GCMStore::GCMStore() {} + +GCMStore::~GCMStore() {} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/gcm_store.h b/chromium/google_apis/gcm/engine/gcm_store.h new file mode 100644 index 00000000000..8b9891d0ecc --- /dev/null +++ b/chromium/google_apis/gcm/engine/gcm_store.h @@ -0,0 +1,121 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_GCM_STORE_H_ +#define GOOGLE_APIS_GCM_ENGINE_GCM_STORE_H_ + +#include <map> +#include <string> +#include <vector> + +#include <google/protobuf/message_lite.h> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/memory/linked_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/time/time.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/engine/registration_info.h" + +namespace gcm { + +class MCSMessage; + +// A GCM data store interface. GCM Store will handle persistence portion of RMQ, +// as well as store device and user checkin information. +class GCM_EXPORT GCMStore { + public: + // Map of message id to message data for outgoing messages. + typedef std::map<std::string, linked_ptr<google::protobuf::MessageLite> > + OutgoingMessageMap; + + // Container for Load(..) results. + struct GCM_EXPORT LoadResult { + LoadResult(); + ~LoadResult(); + + bool success; + uint64 device_android_id; + uint64 device_security_token; + RegistrationInfoMap registrations; + std::vector<std::string> incoming_messages; + OutgoingMessageMap outgoing_messages; + std::map<std::string, std::string> gservices_settings; + std::string gservices_digest; + base::Time last_checkin_time; + }; + + typedef std::vector<std::string> PersistentIdList; + typedef base::Callback<void(scoped_ptr<LoadResult> result)> LoadCallback; + typedef base::Callback<void(bool success)> UpdateCallback; + + GCMStore(); + virtual ~GCMStore(); + + // Load the data from persistent store and pass the initial state back to + // caller. + virtual void Load(const LoadCallback& callback) = 0; + + // Close the persistent store. + virtual void Close() = 0; + + // Clears the GCM store of all data. + virtual void Destroy(const UpdateCallback& callback) = 0; + + // Sets this device's messaging credentials. + virtual void SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback) = 0; + + // Registration info. + virtual void AddRegistration(const std::string& app_id, + const linked_ptr<RegistrationInfo>& registration, + const UpdateCallback& callback) = 0; + virtual void RemoveRegistration(const std::string& app_id, + const UpdateCallback& callback) = 0; + + // Unacknowledged incoming message handling. + virtual void AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) = 0; + virtual void RemoveIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) = 0; + virtual void RemoveIncomingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback) = 0; + + // Unacknowledged outgoing messages handling. + // Returns false if app has surpassed message limits, else returns true. Note + // that the message isn't persisted until |callback| is invoked with + // |success| == true. + virtual bool AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) = 0; + virtual void OverwriteOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) = 0; + virtual void RemoveOutgoingMessage(const std::string& persistent_id, + const UpdateCallback& callback) = 0; + virtual void RemoveOutgoingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback) = 0; + + // Sets last device's checkin time. + virtual void SetLastCheckinTime(const base::Time& last_checkin_time, + const UpdateCallback& callback) = 0; + + // G-service settings handling. + // Persists |settings| and |settings_digest|. It completely replaces the + // existing data. + virtual void SetGServicesSettings( + const std::map<std::string, std::string>& settings, + const std::string& settings_digest, + const UpdateCallback& callback) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(GCMStore); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_GCM_STORE_H_ diff --git a/chromium/google_apis/gcm/engine/gcm_store_impl.cc b/chromium/google_apis/gcm/engine/gcm_store_impl.cc new file mode 100644 index 00000000000..e27e82e6ce2 --- /dev/null +++ b/chromium/google_apis/gcm/engine/gcm_store_impl.cc @@ -0,0 +1,956 @@ +// Copyright 2014 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 "google_apis/gcm/engine/gcm_store_impl.h" + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "base/sequenced_task_runner.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/time/time.h" +#include "base/tracked_objects.h" +#include "google_apis/gcm/base/encryptor.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "third_party/leveldatabase/src/include/leveldb/db.h" +#include "third_party/leveldatabase/src/include/leveldb/write_batch.h" + +namespace gcm { + +namespace { + +// Limit to the number of outstanding messages per app. +const int kMessagesPerAppLimit = 20; + +// ---- LevelDB keys. ---- +// Key for this device's android id. +const char kDeviceAIDKey[] = "device_aid_key"; +// Key for this device's android security token. +const char kDeviceTokenKey[] = "device_token_key"; +// Lowest lexicographically ordered app ids. +// Used for prefixing app id. +const char kRegistrationKeyStart[] = "reg1-"; +// Key guaranteed to be higher than all app ids. +// Used for limiting iteration. +const char kRegistrationKeyEnd[] = "reg2-"; +// Lowest lexicographically ordered incoming message key. +// Used for prefixing messages. +const char kIncomingMsgKeyStart[] = "incoming1-"; +// Key guaranteed to be higher than all incoming message keys. +// Used for limiting iteration. +const char kIncomingMsgKeyEnd[] = "incoming2-"; +// Lowest lexicographically ordered outgoing message key. +// Used for prefixing outgoing messages. +const char kOutgoingMsgKeyStart[] = "outgoing1-"; +// Key guaranteed to be higher than all outgoing message keys. +// Used for limiting iteration. +const char kOutgoingMsgKeyEnd[] = "outgoing2-"; +// Lowest lexicographically ordered G-service settings key. +// Used for prefixing G-services settings. +const char kGServiceSettingKeyStart[] = "gservice1-"; +// Key guaranteed to be higher than all G-services settings keys. +// Used for limiting iteration. +const char kGServiceSettingKeyEnd[] = "gservice2-"; +// Key for digest of the last G-services settings update. +const char kGServiceSettingsDigestKey[] = "gservices_digest"; +// Key used to timestamp last checkin (marked with G services settings update). +const char kLastCheckinTimeKey[] = "last_checkin_time"; + +std::string MakeRegistrationKey(const std::string& app_id) { + return kRegistrationKeyStart + app_id; +} + +std::string ParseRegistrationKey(const std::string& key) { + return key.substr(arraysize(kRegistrationKeyStart) - 1); +} + +std::string MakeIncomingKey(const std::string& persistent_id) { + return kIncomingMsgKeyStart + persistent_id; +} + +std::string MakeOutgoingKey(const std::string& persistent_id) { + return kOutgoingMsgKeyStart + persistent_id; +} + +std::string ParseOutgoingKey(const std::string& key) { + return key.substr(arraysize(kOutgoingMsgKeyStart) - 1); +} + +std::string MakeGServiceSettingKey(const std::string& setting_name) { + return kGServiceSettingKeyStart + setting_name; +} + +std::string ParseGServiceSettingKey(const std::string& key) { + return key.substr(arraysize(kGServiceSettingKeyStart) - 1); +} + +// Note: leveldb::Slice keeps a pointer to the data in |s|, which must therefore +// outlive the slice. +// For example: MakeSlice(MakeOutgoingKey(x)) is invalid. +leveldb::Slice MakeSlice(const base::StringPiece& s) { + return leveldb::Slice(s.begin(), s.size()); +} + +} // namespace + +class GCMStoreImpl::Backend + : public base::RefCountedThreadSafe<GCMStoreImpl::Backend> { + public: + Backend(const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> foreground_runner, + scoped_ptr<Encryptor> encryptor); + + // Blocking implementations of GCMStoreImpl methods. + void Load(const LoadCallback& callback); + void Close(); + void Destroy(const UpdateCallback& callback); + void SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback); + void AddRegistration(const std::string& app_id, + const linked_ptr<RegistrationInfo>& registration, + const UpdateCallback& callback); + void RemoveRegistration(const std::string& app_id, + const UpdateCallback& callback); + void AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback); + void RemoveIncomingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback); + void AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback); + void RemoveOutgoingMessages( + const PersistentIdList& persistent_ids, + const base::Callback<void(bool, const AppIdToMessageCountMap&)> + callback); + void AddUserSerialNumber(const std::string& username, + int64 serial_number, + const UpdateCallback& callback); + void RemoveUserSerialNumber(const std::string& username, + const UpdateCallback& callback); + void SetLastCheckinTime(const base::Time& last_checkin_time, + const UpdateCallback& callback); + void SetGServicesSettings( + const std::map<std::string, std::string>& settings, + const std::string& digest, + const UpdateCallback& callback); + + private: + friend class base::RefCountedThreadSafe<Backend>; + ~Backend(); + + bool LoadDeviceCredentials(uint64* android_id, uint64* security_token); + bool LoadRegistrations(RegistrationInfoMap* registrations); + bool LoadIncomingMessages(std::vector<std::string>* incoming_messages); + bool LoadOutgoingMessages(OutgoingMessageMap* outgoing_messages); + bool LoadLastCheckinTime(base::Time* last_checkin_time); + bool LoadGServicesSettings(std::map<std::string, std::string>* settings, + std::string* digest); + + const base::FilePath path_; + scoped_refptr<base::SequencedTaskRunner> foreground_task_runner_; + scoped_ptr<Encryptor> encryptor_; + + scoped_ptr<leveldb::DB> db_; +}; + +GCMStoreImpl::Backend::Backend( + const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> foreground_task_runner, + scoped_ptr<Encryptor> encryptor) + : path_(path), + foreground_task_runner_(foreground_task_runner), + encryptor_(encryptor.Pass()) { +} + +GCMStoreImpl::Backend::~Backend() {} + +void GCMStoreImpl::Backend::Load(const LoadCallback& callback) { + scoped_ptr<LoadResult> result(new LoadResult()); + if (db_.get()) { + LOG(ERROR) << "Attempting to reload open database."; + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, + base::Passed(&result))); + return; + } + + leveldb::Options options; + options.create_if_missing = true; + leveldb::DB* db; + leveldb::Status status = + leveldb::DB::Open(options, path_.AsUTF8Unsafe(), &db); + UMA_HISTOGRAM_BOOLEAN("GCM.LoadSucceeded", status.ok()); + if (!status.ok()) { + LOG(ERROR) << "Failed to open database " << path_.value() << ": " + << status.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, + base::Passed(&result))); + return; + } + db_.reset(db); + + if (!LoadDeviceCredentials(&result->device_android_id, + &result->device_security_token) || + !LoadRegistrations(&result->registrations) || + !LoadIncomingMessages(&result->incoming_messages) || + !LoadOutgoingMessages(&result->outgoing_messages) || + !LoadLastCheckinTime(&result->last_checkin_time) || + !LoadGServicesSettings(&result->gservices_settings, + &result->gservices_digest)) { + result->device_android_id = 0; + result->device_security_token = 0; + result->registrations.clear(); + result->incoming_messages.clear(); + result->outgoing_messages.clear(); + result->gservices_settings.clear(); + result->gservices_digest.clear(); + result->last_checkin_time = base::Time::FromInternalValue(0LL); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, + base::Passed(&result))); + return; + } + + // Only record histograms if GCM had already been set up for this device. + if (result->device_android_id != 0 && result->device_security_token != 0) { + int64 file_size = 0; + if (base::GetFileSize(path_, &file_size)) { + UMA_HISTOGRAM_COUNTS("GCM.StoreSizeKB", + static_cast<int>(file_size / 1024)); + } + UMA_HISTOGRAM_COUNTS("GCM.RestoredRegistrations", + result->registrations.size()); + UMA_HISTOGRAM_COUNTS("GCM.RestoredOutgoingMessages", + result->outgoing_messages.size()); + UMA_HISTOGRAM_COUNTS("GCM.RestoredIncomingMessages", + result->incoming_messages.size()); + } + + DVLOG(1) << "Succeeded in loading " << result->registrations.size() + << " registrations, " + << result->incoming_messages.size() + << " unacknowledged incoming messages and " + << result->outgoing_messages.size() + << " unacknowledged outgoing messages."; + result->success = true; + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, + base::Passed(&result))); + return; +} + +void GCMStoreImpl::Backend::Close() { + DVLOG(1) << "Closing GCM store."; + db_.reset(); +} + +void GCMStoreImpl::Backend::Destroy(const UpdateCallback& callback) { + DVLOG(1) << "Destroying GCM store."; + db_.reset(); + const leveldb::Status s = + leveldb::DestroyDB(path_.AsUTF8Unsafe(), leveldb::Options()); + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true)); + return; + } + LOG(ERROR) << "Destroy failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); +} + +void GCMStoreImpl::Backend::SetDeviceCredentials( + uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback) { + DVLOG(1) << "Saving device credentials with AID " << device_android_id; + if (!db_.get()) { + LOG(ERROR) << "GCMStore db doesn't exist."; + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); + return; + } + + leveldb::WriteOptions write_options; + write_options.sync = true; + + std::string encrypted_token; + encryptor_->EncryptString(base::Uint64ToString(device_security_token), + &encrypted_token); + std::string android_id_str = base::Uint64ToString(device_android_id); + leveldb::Status s = + db_->Put(write_options, + MakeSlice(kDeviceAIDKey), + MakeSlice(android_id_str)); + if (s.ok()) { + s = db_->Put( + write_options, MakeSlice(kDeviceTokenKey), MakeSlice(encrypted_token)); + } + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB put failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); +} + +void GCMStoreImpl::Backend::AddRegistration( + const std::string& app_id, + const linked_ptr<RegistrationInfo>& registration, + const UpdateCallback& callback) { + DVLOG(1) << "Saving registration info for app: " << app_id; + if (!db_.get()) { + LOG(ERROR) << "GCMStore db doesn't exist."; + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); + return; + } + leveldb::WriteOptions write_options; + write_options.sync = true; + + std::string key = MakeRegistrationKey(app_id); + std::string value = registration->SerializeAsString(); + const leveldb::Status status = db_->Put(write_options, + MakeSlice(key), + MakeSlice(value)); + if (status.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB put failed: " << status.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); +} + +void GCMStoreImpl::Backend::RemoveRegistration(const std::string& app_id, + const UpdateCallback& callback) { + if (!db_.get()) { + LOG(ERROR) << "GCMStore db doesn't exist."; + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); + return; + } + leveldb::WriteOptions write_options; + write_options.sync = true; + + leveldb::Status status = db_->Delete(write_options, MakeSlice(app_id)); + if (status.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB remove failed: " << status.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); +} + +void GCMStoreImpl::Backend::AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + DVLOG(1) << "Saving incoming message with id " << persistent_id; + if (!db_.get()) { + LOG(ERROR) << "GCMStore db doesn't exist."; + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); + return; + } + + leveldb::WriteOptions write_options; + write_options.sync = true; + + std::string key = MakeIncomingKey(persistent_id); + const leveldb::Status s = db_->Put(write_options, + MakeSlice(key), + MakeSlice(persistent_id)); + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB put failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); +} + +void GCMStoreImpl::Backend::RemoveIncomingMessages( + const PersistentIdList& persistent_ids, + const UpdateCallback& callback) { + if (!db_.get()) { + LOG(ERROR) << "GCMStore db doesn't exist."; + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); + return; + } + leveldb::WriteOptions write_options; + write_options.sync = true; + + leveldb::Status s; + for (PersistentIdList::const_iterator iter = persistent_ids.begin(); + iter != persistent_ids.end(); + ++iter) { + DVLOG(1) << "Removing incoming message with id " << *iter; + std::string key = MakeIncomingKey(*iter); + s = db_->Delete(write_options, MakeSlice(key)); + if (!s.ok()) + break; + } + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB remove failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); +} + +void GCMStoreImpl::Backend::AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) { + DVLOG(1) << "Saving outgoing message with id " << persistent_id; + if (!db_.get()) { + LOG(ERROR) << "GCMStore db doesn't exist."; + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); + return; + } + leveldb::WriteOptions write_options; + write_options.sync = true; + + std::string data = + static_cast<char>(message.tag()) + message.SerializeAsString(); + std::string key = MakeOutgoingKey(persistent_id); + const leveldb::Status s = db_->Put(write_options, + MakeSlice(key), + MakeSlice(data)); + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true)); + return; + } + LOG(ERROR) << "LevelDB put failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false)); +} + +void GCMStoreImpl::Backend::RemoveOutgoingMessages( + const PersistentIdList& persistent_ids, + const base::Callback<void(bool, const AppIdToMessageCountMap&)> + callback) { + if (!db_.get()) { + LOG(ERROR) << "GCMStore db doesn't exist."; + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, + false, + AppIdToMessageCountMap())); + return; + } + leveldb::ReadOptions read_options; + leveldb::WriteOptions write_options; + write_options.sync = true; + + AppIdToMessageCountMap removed_message_counts; + + leveldb::Status s; + for (PersistentIdList::const_iterator iter = persistent_ids.begin(); + iter != persistent_ids.end(); + ++iter) { + DVLOG(1) << "Removing outgoing message with id " << *iter; + std::string outgoing_message; + std::string key = MakeOutgoingKey(*iter); + s = db_->Get(read_options, + MakeSlice(key), + &outgoing_message); + if (!s.ok()) + break; + mcs_proto::DataMessageStanza data_message; + // Skip the initial tag byte and parse the rest to extract the message. + if (data_message.ParseFromString(outgoing_message.substr(1))) { + DCHECK(!data_message.category().empty()); + if (removed_message_counts.count(data_message.category()) != 0) + removed_message_counts[data_message.category()]++; + else + removed_message_counts[data_message.category()] = 1; + } + DVLOG(1) << "Removing outgoing message with id " << *iter; + s = db_->Delete(write_options, MakeSlice(key)); + if (!s.ok()) + break; + } + if (s.ok()) { + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, + true, + removed_message_counts)); + return; + } + LOG(ERROR) << "LevelDB remove failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, + base::Bind(callback, + false, + AppIdToMessageCountMap())); +} + +void GCMStoreImpl::Backend::SetLastCheckinTime( + const base::Time& last_checkin_time, + const UpdateCallback& callback) { + leveldb::WriteOptions write_options; + write_options.sync = true; + + int64 last_checkin_time_internal = last_checkin_time.ToInternalValue(); + const leveldb::Status s = + db_->Put(write_options, + MakeSlice(kLastCheckinTimeKey), + MakeSlice(base::Int64ToString(last_checkin_time_internal))); + + if (!s.ok()) + LOG(ERROR) << "LevelDB set last checkin time failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, s.ok())); +} + +void GCMStoreImpl::Backend::SetGServicesSettings( + const std::map<std::string, std::string>& settings, + const std::string& settings_digest, + const UpdateCallback& callback) { + leveldb::WriteBatch write_batch; + + // Remove all existing settings. + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); + for (iter->Seek(MakeSlice(kGServiceSettingKeyStart)); + iter->Valid() && iter->key().ToString() < kGServiceSettingKeyEnd; + iter->Next()) { + write_batch.Delete(iter->key()); + } + + // Add the new settings. + for (std::map<std::string, std::string>::const_iterator iter = + settings.begin(); + iter != settings.end(); ++iter) { + write_batch.Put(MakeSlice(MakeGServiceSettingKey(iter->first)), + MakeSlice(iter->second)); + } + + // Update the settings digest. + write_batch.Put(MakeSlice(kGServiceSettingsDigestKey), + MakeSlice(settings_digest)); + + // Write it all in a batch. + leveldb::WriteOptions write_options; + write_options.sync = true; + + leveldb::Status s = db_->Write(write_options, &write_batch); + if (!s.ok()) + LOG(ERROR) << "LevelDB GService Settings update failed: " << s.ToString(); + foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, s.ok())); +} + +bool GCMStoreImpl::Backend::LoadDeviceCredentials(uint64* android_id, + uint64* security_token) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + std::string result; + leveldb::Status s = db_->Get(read_options, MakeSlice(kDeviceAIDKey), &result); + if (s.ok()) { + if (!base::StringToUint64(result, android_id)) { + LOG(ERROR) << "Failed to restore device id."; + return false; + } + result.clear(); + s = db_->Get(read_options, MakeSlice(kDeviceTokenKey), &result); + } + if (s.ok()) { + std::string decrypted_token; + encryptor_->DecryptString(result, &decrypted_token); + if (!base::StringToUint64(decrypted_token, security_token)) { + LOG(ERROR) << "Failed to restore security token."; + return false; + } + return true; + } + + if (s.IsNotFound()) { + DVLOG(1) << "No credentials found."; + return true; + } + + LOG(ERROR) << "Error reading credentials from store."; + return false; +} + +bool GCMStoreImpl::Backend::LoadRegistrations( + RegistrationInfoMap* registrations) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); + for (iter->Seek(MakeSlice(kRegistrationKeyStart)); + iter->Valid() && iter->key().ToString() < kRegistrationKeyEnd; + iter->Next()) { + leveldb::Slice s = iter->value(); + if (s.size() <= 1) { + LOG(ERROR) << "Error reading registration with key " << s.ToString(); + return false; + } + std::string app_id = ParseRegistrationKey(iter->key().ToString()); + linked_ptr<RegistrationInfo> registration(new RegistrationInfo); + if (!registration->ParseFromString(iter->value().ToString())) { + LOG(ERROR) << "Failed to parse registration with app id " << app_id; + return false; + } + DVLOG(1) << "Found registration with app id " << app_id; + (*registrations)[app_id] = registration; + } + + return true; +} + +bool GCMStoreImpl::Backend::LoadIncomingMessages( + std::vector<std::string>* incoming_messages) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); + for (iter->Seek(MakeSlice(kIncomingMsgKeyStart)); + iter->Valid() && iter->key().ToString() < kIncomingMsgKeyEnd; + iter->Next()) { + leveldb::Slice s = iter->value(); + if (s.empty()) { + LOG(ERROR) << "Error reading incoming message with key " + << iter->key().ToString(); + return false; + } + DVLOG(1) << "Found incoming message with id " << s.ToString(); + incoming_messages->push_back(s.ToString()); + } + + return true; +} + +bool GCMStoreImpl::Backend::LoadOutgoingMessages( + OutgoingMessageMap* outgoing_messages) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); + for (iter->Seek(MakeSlice(kOutgoingMsgKeyStart)); + iter->Valid() && iter->key().ToString() < kOutgoingMsgKeyEnd; + iter->Next()) { + leveldb::Slice s = iter->value(); + if (s.size() <= 1) { + LOG(ERROR) << "Error reading incoming message with key " << s.ToString(); + return false; + } + uint8 tag = iter->value().data()[0]; + std::string id = ParseOutgoingKey(iter->key().ToString()); + scoped_ptr<google::protobuf::MessageLite> message( + BuildProtobufFromTag(tag)); + if (!message.get() || + !message->ParseFromString(iter->value().ToString().substr(1))) { + LOG(ERROR) << "Failed to parse outgoing message with id " << id + << " and tag " << tag; + return false; + } + DVLOG(1) << "Found outgoing message with id " << id << " of type " + << base::IntToString(tag); + (*outgoing_messages)[id] = make_linked_ptr(message.release()); + } + + return true; +} + +bool GCMStoreImpl::Backend::LoadLastCheckinTime( + base::Time* last_checkin_time) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + std::string result; + leveldb::Status s = db_->Get(read_options, + MakeSlice(kLastCheckinTimeKey), + &result); + int64 time_internal = 0LL; + if (s.ok() && !base::StringToInt64(result, &time_internal)) + LOG(ERROR) << "Failed to restore last checkin time. Using default = 0."; + + // In case we cannot read last checkin time, we default it to 0, as we don't + // want that situation to cause the whole load to fail. + *last_checkin_time = base::Time::FromInternalValue(time_internal); + + return true; +} + +bool GCMStoreImpl::Backend::LoadGServicesSettings( + std::map<std::string, std::string>* settings, + std::string* digest) { + leveldb::ReadOptions read_options; + read_options.verify_checksums = true; + + // Load all of the GServices settings. + scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); + for (iter->Seek(MakeSlice(kGServiceSettingKeyStart)); + iter->Valid() && iter->key().ToString() < kGServiceSettingKeyEnd; + iter->Next()) { + std::string value = iter->value().ToString(); + if (value.empty()) { + LOG(ERROR) << "Error reading GService Settings " << value; + return false; + } + std::string id = ParseGServiceSettingKey(iter->key().ToString()); + (*settings)[id] = value; + DVLOG(1) << "Found G Service setting with key: " << id + << ", and value: " << value; + } + + // Load the settings digest. It's ok if it is empty. + db_->Get(read_options, MakeSlice(kGServiceSettingsDigestKey), digest); + + return true; +} + +GCMStoreImpl::GCMStoreImpl( + const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner, + scoped_ptr<Encryptor> encryptor) + : backend_(new Backend(path, + base::MessageLoopProxy::current(), + encryptor.Pass())), + blocking_task_runner_(blocking_task_runner), + weak_ptr_factory_(this) { +} + +GCMStoreImpl::~GCMStoreImpl() {} + +void GCMStoreImpl::Load(const LoadCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::Load, + backend_, + base::Bind(&GCMStoreImpl::LoadContinuation, + weak_ptr_factory_.GetWeakPtr(), + callback))); +} + +void GCMStoreImpl::Close() { + weak_ptr_factory_.InvalidateWeakPtrs(); + app_message_counts_.clear(); + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::Close, backend_)); +} + +void GCMStoreImpl::Destroy(const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::Destroy, backend_, callback)); +} + +void GCMStoreImpl::SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::SetDeviceCredentials, + backend_, + device_android_id, + device_security_token, + callback)); +} + +void GCMStoreImpl::AddRegistration( + const std::string& app_id, + const linked_ptr<RegistrationInfo>& registration, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::AddRegistration, + backend_, + app_id, + registration, + callback)); +} + +void GCMStoreImpl::RemoveRegistration(const std::string& app_id, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::RemoveRegistration, + backend_, + app_id, + callback)); +} + +void GCMStoreImpl::AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::AddIncomingMessage, + backend_, + persistent_id, + callback)); +} + +void GCMStoreImpl::RemoveIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::RemoveIncomingMessages, + backend_, + PersistentIdList(1, persistent_id), + callback)); +} + +void GCMStoreImpl::RemoveIncomingMessages( + const PersistentIdList& persistent_ids, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::RemoveIncomingMessages, + backend_, + persistent_ids, + callback)); +} + +bool GCMStoreImpl::AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) { + DCHECK_EQ(message.tag(), kDataMessageStanzaTag); + std::string app_id = reinterpret_cast<const mcs_proto::DataMessageStanza*>( + &message.GetProtobuf())->category(); + DCHECK(!app_id.empty()); + if (app_message_counts_.count(app_id) == 0) + app_message_counts_[app_id] = 0; + if (app_message_counts_[app_id] < kMessagesPerAppLimit) { + app_message_counts_[app_id]++; + + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::AddOutgoingMessage, + backend_, + persistent_id, + message, + base::Bind(&GCMStoreImpl::AddOutgoingMessageContinuation, + weak_ptr_factory_.GetWeakPtr(), + callback, + app_id))); + return true; + } + return false; +} + +void GCMStoreImpl::OverwriteOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) { + DCHECK_EQ(message.tag(), kDataMessageStanzaTag); + std::string app_id = reinterpret_cast<const mcs_proto::DataMessageStanza*>( + &message.GetProtobuf())->category(); + DCHECK(!app_id.empty()); + // There should already be pending messages for this app. + DCHECK(app_message_counts_.count(app_id)); + // TODO(zea): consider verifying the specific message already exists. + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::AddOutgoingMessage, + backend_, + persistent_id, + message, + callback)); +} + +void GCMStoreImpl::RemoveOutgoingMessage(const std::string& persistent_id, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::RemoveOutgoingMessages, + backend_, + PersistentIdList(1, persistent_id), + base::Bind(&GCMStoreImpl::RemoveOutgoingMessagesContinuation, + weak_ptr_factory_.GetWeakPtr(), + callback))); +} + +void GCMStoreImpl::RemoveOutgoingMessages( + const PersistentIdList& persistent_ids, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::RemoveOutgoingMessages, + backend_, + persistent_ids, + base::Bind(&GCMStoreImpl::RemoveOutgoingMessagesContinuation, + weak_ptr_factory_.GetWeakPtr(), + callback))); +} + +void GCMStoreImpl::SetLastCheckinTime(const base::Time& last_checkin_time, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::SetLastCheckinTime, + backend_, + last_checkin_time, + callback)); +} + +void GCMStoreImpl::SetGServicesSettings( + const std::map<std::string, std::string>& settings, + const std::string& digest, + const UpdateCallback& callback) { + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(&GCMStoreImpl::Backend::SetGServicesSettings, + backend_, + settings, + digest, + callback)); +} + +void GCMStoreImpl::LoadContinuation(const LoadCallback& callback, + scoped_ptr<LoadResult> result) { + if (!result->success) { + callback.Run(result.Pass()); + return; + } + int num_throttled_apps = 0; + for (OutgoingMessageMap::const_iterator + iter = result->outgoing_messages.begin(); + iter != result->outgoing_messages.end(); ++iter) { + const mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<mcs_proto::DataMessageStanza*>(iter->second.get()); + DCHECK(!data_message->category().empty()); + if (app_message_counts_.count(data_message->category()) == 0) + app_message_counts_[data_message->category()] = 1; + else + app_message_counts_[data_message->category()]++; + if (app_message_counts_[data_message->category()] == kMessagesPerAppLimit) + num_throttled_apps++; + } + UMA_HISTOGRAM_COUNTS("GCM.NumThrottledApps", num_throttled_apps); + callback.Run(result.Pass()); +} + +void GCMStoreImpl::AddOutgoingMessageContinuation( + const UpdateCallback& callback, + const std::string& app_id, + bool success) { + if (!success) { + DCHECK(app_message_counts_[app_id] > 0); + app_message_counts_[app_id]--; + } + callback.Run(success); +} + +void GCMStoreImpl::RemoveOutgoingMessagesContinuation( + const UpdateCallback& callback, + bool success, + const AppIdToMessageCountMap& removed_message_counts) { + if (!success) { + callback.Run(false); + return; + } + for (AppIdToMessageCountMap::const_iterator iter = + removed_message_counts.begin(); + iter != removed_message_counts.end(); ++iter) { + DCHECK_NE(app_message_counts_.count(iter->first), 0U); + app_message_counts_[iter->first] -= iter->second; + DCHECK_GE(app_message_counts_[iter->first], 0); + } + callback.Run(true); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/gcm_store_impl.h b/chromium/google_apis/gcm/engine/gcm_store_impl.h new file mode 100644 index 00000000000..f49509a3e17 --- /dev/null +++ b/chromium/google_apis/gcm/engine/gcm_store_impl.h @@ -0,0 +1,126 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_GCM_STORE_IMPL_H_ +#define GOOGLE_APIS_GCM_ENGINE_GCM_STORE_IMPL_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/engine/gcm_store.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace gcm { + +class Encryptor; + +// An implementation of GCM Store that uses LevelDB for persistence. +// It performs all blocking operations on the blocking task runner, and posts +// all callbacks to the thread on which the GCMStoreImpl is created. +class GCM_EXPORT GCMStoreImpl : public GCMStore { + public: + GCMStoreImpl(const base::FilePath& path, + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner, + scoped_ptr<Encryptor> encryptor); + virtual ~GCMStoreImpl(); + + // Load the directory and pass the initial state back to caller. + virtual void Load(const LoadCallback& callback) OVERRIDE; + + // Closes the GCM store. + virtual void Close() OVERRIDE; + + // Clears the GCM store of all data and destroys any LevelDB files associated + // with this store. + // WARNING: this will permanently destroy any pending outgoing messages + // and require the device to re-create credentials and serial number mapping + // tables. + virtual void Destroy(const UpdateCallback& callback) OVERRIDE; + + // Sets this device's messaging credentials. + virtual void SetDeviceCredentials(uint64 device_android_id, + uint64 device_security_token, + const UpdateCallback& callback) OVERRIDE; + + // Registration info. + virtual void AddRegistration(const std::string& app_id, + const linked_ptr<RegistrationInfo>& registration, + const UpdateCallback& callback) OVERRIDE; + virtual void RemoveRegistration(const std::string& app_id, + const UpdateCallback& callback) OVERRIDE; + + // Unacknowledged incoming message handling. + virtual void AddIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) OVERRIDE; + virtual void RemoveIncomingMessage(const std::string& persistent_id, + const UpdateCallback& callback) OVERRIDE; + virtual void RemoveIncomingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback) OVERRIDE; + + // Unacknowledged outgoing messages handling. + virtual bool AddOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) OVERRIDE; + virtual void OverwriteOutgoingMessage(const std::string& persistent_id, + const MCSMessage& message, + const UpdateCallback& callback) + OVERRIDE; + virtual void RemoveOutgoingMessage(const std::string& persistent_id, + const UpdateCallback& callback) OVERRIDE; + virtual void RemoveOutgoingMessages(const PersistentIdList& persistent_ids, + const UpdateCallback& callback) OVERRIDE; + + // Sets last device's checkin time. + virtual void SetLastCheckinTime(const base::Time& last_checkin_time, + const UpdateCallback& callback) OVERRIDE; + + // G-service settings handling. + virtual void SetGServicesSettings( + const std::map<std::string, std::string>& settings, + const std::string& settings_digest, + const UpdateCallback& callback) OVERRIDE; + + private: + typedef std::map<std::string, int> AppIdToMessageCountMap; + + // Continuation to update the per-app message counts after a load. + void LoadContinuation(const LoadCallback& callback, + scoped_ptr<LoadResult> result); + + // Continuation to update the per-app message counts when adding messages. + // In particular, if a message fails to add, the message count is decremented. + void AddOutgoingMessageContinuation(const UpdateCallback& callback, + const std::string& app_id, + bool success); + + // Continuation to update the per-app message counts when removing messages. + // Note: if doing a read-then-write when removing messages proves expensive, + // an in-memory mapping of persisted message id to app could be maintained + // instead. + void RemoveOutgoingMessagesContinuation( + const UpdateCallback& callback, + bool success, + const std::map<std::string, int>& removed_message_counts); + + class Backend; + + // Map of App ids to their message counts. + AppIdToMessageCountMap app_message_counts_; + + scoped_refptr<Backend> backend_; + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + + base::WeakPtrFactory<GCMStoreImpl> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(GCMStoreImpl); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_GCM_STORE_IMPL_H_ diff --git a/chromium/google_apis/gcm/engine/gcm_store_impl_unittest.cc b/chromium/google_apis/gcm/engine/gcm_store_impl_unittest.cc new file mode 100644 index 00000000000..7b9c8936529 --- /dev/null +++ b/chromium/google_apis/gcm/engine/gcm_store_impl_unittest.cc @@ -0,0 +1,552 @@ +// Copyright 2014 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 "google_apis/gcm/engine/gcm_store_impl.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "google_apis/gcm/base/fake_encryptor.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +// Number of persistent ids to use in tests. +const int kNumPersistentIds = 10; + +// Number of per-app messages in tests. +const int kNumMessagesPerApp = 20; + +// App name for testing. +const char kAppName[] = "my_app"; + +// Category name for testing. +const char kCategoryName[] = "my_category"; + +const uint64 kDeviceId = 22; +const uint64 kDeviceToken = 55; + +class GCMStoreImplTest : public testing::Test { + public: + GCMStoreImplTest(); + virtual ~GCMStoreImplTest(); + + virtual void SetUp() OVERRIDE; + + scoped_ptr<GCMStore> BuildGCMStore(); + + std::string GetNextPersistentId(); + + void PumpLoop(); + + void LoadCallback(scoped_ptr<GCMStore::LoadResult>* result_dst, + scoped_ptr<GCMStore::LoadResult> result); + void UpdateCallback(bool success); + + protected: + base::MessageLoop message_loop_; + base::ScopedTempDir temp_directory_; + bool expected_success_; + uint64 next_persistent_id_; + scoped_ptr<base::RunLoop> run_loop_; +}; + +GCMStoreImplTest::GCMStoreImplTest() + : expected_success_(true), + next_persistent_id_(base::Time::Now().ToInternalValue()) { + EXPECT_TRUE(temp_directory_.CreateUniqueTempDir()); + run_loop_.reset(new base::RunLoop()); +} + +GCMStoreImplTest::~GCMStoreImplTest() {} + +void GCMStoreImplTest::SetUp() { + testing::Test::SetUp(); +} + +scoped_ptr<GCMStore> GCMStoreImplTest::BuildGCMStore() { + return scoped_ptr<GCMStore>(new GCMStoreImpl( + temp_directory_.path(), + message_loop_.message_loop_proxy(), + make_scoped_ptr<Encryptor>(new FakeEncryptor))); +} + +std::string GCMStoreImplTest::GetNextPersistentId() { + return base::Uint64ToString(next_persistent_id_++); +} + +void GCMStoreImplTest::PumpLoop() { message_loop_.RunUntilIdle(); } + +void GCMStoreImplTest::LoadCallback( + scoped_ptr<GCMStore::LoadResult>* result_dst, + scoped_ptr<GCMStore::LoadResult> result) { + ASSERT_TRUE(result->success); + *result_dst = result.Pass(); + run_loop_->Quit(); + run_loop_.reset(new base::RunLoop()); +} + +void GCMStoreImplTest::UpdateCallback(bool success) { + ASSERT_EQ(expected_success_, success); +} + +// Verify creating a new database and loading it. +TEST_F(GCMStoreImplTest, LoadNew) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + EXPECT_EQ(0U, load_result->device_android_id); + EXPECT_EQ(0U, load_result->device_security_token); + EXPECT_TRUE(load_result->incoming_messages.empty()); + EXPECT_TRUE(load_result->outgoing_messages.empty()); + EXPECT_TRUE(load_result->gservices_settings.empty()); + EXPECT_EQ(base::Time::FromInternalValue(0LL), load_result->last_checkin_time); +} + +TEST_F(GCMStoreImplTest, DeviceCredentials) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + gcm_store->SetDeviceCredentials( + kDeviceId, + kDeviceToken, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_EQ(kDeviceId, load_result->device_android_id); + ASSERT_EQ(kDeviceToken, load_result->device_security_token); +} + +TEST_F(GCMStoreImplTest, LastCheckinTime) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + base::Time last_checkin_time = base::Time::Now(); + + gcm_store->SetLastCheckinTime( + last_checkin_time, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + ASSERT_EQ(last_checkin_time, load_result->last_checkin_time); +} + +TEST_F(GCMStoreImplTest, GServicesSettings_ProtocolV2) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + std::map<std::string, std::string> settings; + settings["checkin_interval"] = "12345"; + settings["mcs_port"] = "438"; + settings["checkin_url"] = "http://checkin.google.com"; + std::string digest = "digest1"; + + gcm_store->SetGServicesSettings( + settings, + digest, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_EQ(settings, load_result->gservices_settings); + ASSERT_EQ(digest, load_result->gservices_digest); + + // Remove some, and add some. + settings.clear(); + settings["checkin_interval"] = "54321"; + settings["registration_url"] = "http://registration.google.com"; + digest = "digest2"; + + gcm_store->SetGServicesSettings( + settings, + digest, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_EQ(settings, load_result->gservices_settings); + ASSERT_EQ(digest, load_result->gservices_digest); +} + +TEST_F(GCMStoreImplTest, Registrations) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + // Add one registration with one sender. + linked_ptr<RegistrationInfo> registration1(new RegistrationInfo); + registration1->sender_ids.push_back("sender1"); + registration1->registration_id = "registration1"; + gcm_store->AddRegistration( + "app1", + registration1, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + // Add one registration with multiple senders. + linked_ptr<RegistrationInfo> registration2(new RegistrationInfo); + registration2->sender_ids.push_back("sender2_1"); + registration2->sender_ids.push_back("sender2_2"); + registration2->registration_id = "registration2"; + gcm_store->AddRegistration( + "app2", + registration2, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_EQ(2u, load_result->registrations.size()); + ASSERT_TRUE(load_result->registrations.find("app1") != + load_result->registrations.end()); + EXPECT_EQ(registration1->registration_id, + load_result->registrations["app1"]->registration_id); + ASSERT_EQ(1u, load_result->registrations["app1"]->sender_ids.size()); + EXPECT_EQ(registration1->sender_ids[0], + load_result->registrations["app1"]->sender_ids[0]); + ASSERT_TRUE(load_result->registrations.find("app2") != + load_result->registrations.end()); + EXPECT_EQ(registration2->registration_id, + load_result->registrations["app2"]->registration_id); + ASSERT_EQ(2u, load_result->registrations["app2"]->sender_ids.size()); + EXPECT_EQ(registration2->sender_ids[0], + load_result->registrations["app2"]->sender_ids[0]); + EXPECT_EQ(registration2->sender_ids[1], + load_result->registrations["app2"]->sender_ids[1]); +} + +// Verify saving some incoming messages, reopening the directory, and then +// removing those incoming messages. +TEST_F(GCMStoreImplTest, IncomingMessages) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + std::vector<std::string> persistent_ids; + for (int i = 0; i < kNumPersistentIds; ++i) { + persistent_ids.push_back(GetNextPersistentId()); + gcm_store->AddIncomingMessage( + persistent_ids.back(), + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + } + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_EQ(persistent_ids, load_result->incoming_messages); + ASSERT_TRUE(load_result->outgoing_messages.empty()); + + gcm_store->RemoveIncomingMessages( + persistent_ids, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + load_result->incoming_messages.clear(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result->incoming_messages.empty()); + ASSERT_TRUE(load_result->outgoing_messages.empty()); +} + +// Verify saving some outgoing messages, reopening the directory, and then +// removing those outgoing messages. +TEST_F(GCMStoreImplTest, OutgoingMessages) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + std::vector<std::string> persistent_ids; + const int kNumPersistentIds = 10; + for (int i = 0; i < kNumPersistentIds; ++i) { + persistent_ids.push_back(GetNextPersistentId()); + mcs_proto::DataMessageStanza message; + message.set_from(kAppName + persistent_ids.back()); + message.set_category(kCategoryName + persistent_ids.back()); + gcm_store->AddOutgoingMessage( + persistent_ids.back(), + MCSMessage(message), + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + } + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result->incoming_messages.empty()); + ASSERT_EQ(load_result->outgoing_messages.size(), persistent_ids.size()); + for (int i = 0; i < kNumPersistentIds; ++i) { + std::string id = persistent_ids[i]; + ASSERT_TRUE(load_result->outgoing_messages[id].get()); + const mcs_proto::DataMessageStanza* message = + reinterpret_cast<mcs_proto::DataMessageStanza*>( + load_result->outgoing_messages[id].get()); + ASSERT_EQ(message->from(), kAppName + id); + ASSERT_EQ(message->category(), kCategoryName + id); + } + + gcm_store->RemoveOutgoingMessages( + persistent_ids, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + load_result->outgoing_messages.clear(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result->incoming_messages.empty()); + ASSERT_TRUE(load_result->outgoing_messages.empty()); +} + +// Verify incoming and outgoing messages don't conflict. +TEST_F(GCMStoreImplTest, IncomingAndOutgoingMessages) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + std::vector<std::string> persistent_ids; + const int kNumPersistentIds = 10; + for (int i = 0; i < kNumPersistentIds; ++i) { + persistent_ids.push_back(GetNextPersistentId()); + gcm_store->AddIncomingMessage( + persistent_ids.back(), + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + mcs_proto::DataMessageStanza message; + message.set_from(kAppName + persistent_ids.back()); + message.set_category(kCategoryName + persistent_ids.back()); + gcm_store->AddOutgoingMessage( + persistent_ids.back(), + MCSMessage(message), + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + } + + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_EQ(persistent_ids, load_result->incoming_messages); + ASSERT_EQ(load_result->outgoing_messages.size(), persistent_ids.size()); + for (int i = 0; i < kNumPersistentIds; ++i) { + std::string id = persistent_ids[i]; + ASSERT_TRUE(load_result->outgoing_messages[id].get()); + const mcs_proto::DataMessageStanza* message = + reinterpret_cast<mcs_proto::DataMessageStanza*>( + load_result->outgoing_messages[id].get()); + ASSERT_EQ(message->from(), kAppName + id); + ASSERT_EQ(message->category(), kCategoryName + id); + } + + gcm_store->RemoveIncomingMessages( + persistent_ids, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + gcm_store->RemoveOutgoingMessages( + persistent_ids, + base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this))); + PumpLoop(); + + gcm_store = BuildGCMStore().Pass(); + load_result->incoming_messages.clear(); + load_result->outgoing_messages.clear(); + gcm_store->Load(base::Bind( + &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result)); + PumpLoop(); + + ASSERT_TRUE(load_result->incoming_messages.empty()); + ASSERT_TRUE(load_result->outgoing_messages.empty()); +} + +// Test that per-app message limits are enforced, persisted across restarts, +// and updated as messages are removed. +TEST_F(GCMStoreImplTest, PerAppMessageLimits) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback, + base::Unretained(this), + &load_result)); + + // Add the initial (below app limit) messages. + for (int i = 0; i < kNumMessagesPerApp; ++i) { + mcs_proto::DataMessageStanza message; + message.set_from(kAppName); + message.set_category(kCategoryName); + EXPECT_TRUE(gcm_store->AddOutgoingMessage( + base::IntToString(i), + MCSMessage(message), + base::Bind(&GCMStoreImplTest::UpdateCallback, + base::Unretained(this)))); + PumpLoop(); + } + + // Attempting to add some more should fail. + for (int i = 0; i < kNumMessagesPerApp; ++i) { + mcs_proto::DataMessageStanza message; + message.set_from(kAppName); + message.set_category(kCategoryName); + EXPECT_FALSE(gcm_store->AddOutgoingMessage( + base::IntToString(i + kNumMessagesPerApp), + MCSMessage(message), + base::Bind(&GCMStoreImplTest::UpdateCallback, + base::Unretained(this)))); + PumpLoop(); + } + + // Tear down and restore the database. + gcm_store = BuildGCMStore().Pass(); + gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + // Adding more messages should still fail. + for (int i = 0; i < kNumMessagesPerApp; ++i) { + mcs_proto::DataMessageStanza message; + message.set_from(kAppName); + message.set_category(kCategoryName); + EXPECT_FALSE(gcm_store->AddOutgoingMessage( + base::IntToString(i + kNumMessagesPerApp), + MCSMessage(message), + base::Bind(&GCMStoreImplTest::UpdateCallback, + base::Unretained(this)))); + PumpLoop(); + } + + // Remove the existing messages. + for (int i = 0; i < kNumMessagesPerApp; ++i) { + gcm_store->RemoveOutgoingMessage( + base::IntToString(i), + base::Bind(&GCMStoreImplTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + } + + // Successfully add new messages. + for (int i = 0; i < kNumMessagesPerApp; ++i) { + mcs_proto::DataMessageStanza message; + message.set_from(kAppName); + message.set_category(kCategoryName); + EXPECT_TRUE(gcm_store->AddOutgoingMessage( + base::IntToString(i + kNumMessagesPerApp), + MCSMessage(message), + base::Bind(&GCMStoreImplTest::UpdateCallback, + base::Unretained(this)))); + PumpLoop(); + } +} + +// When the database is destroyed, all database updates should fail. At the +// same time, they per-app message counts should not go up, as failures should +// result in decrementing the counts. +TEST_F(GCMStoreImplTest, AddMessageAfterDestroy) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + gcm_store->Destroy(base::Bind(&GCMStoreImplTest::UpdateCallback, + base::Unretained(this))); + PumpLoop(); + + expected_success_ = false; + for (int i = 0; i < kNumMessagesPerApp * 2; ++i) { + mcs_proto::DataMessageStanza message; + message.set_from(kAppName); + message.set_category(kCategoryName); + // Because all adds are failing, none should hit the per-app message limits. + EXPECT_TRUE(gcm_store->AddOutgoingMessage( + base::IntToString(i), + MCSMessage(message), + base::Bind(&GCMStoreImplTest::UpdateCallback, + base::Unretained(this)))); + PumpLoop(); + } +} + +TEST_F(GCMStoreImplTest, ReloadAfterClose) { + scoped_ptr<GCMStore> gcm_store(BuildGCMStore()); + scoped_ptr<GCMStore::LoadResult> load_result; + gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); + + gcm_store->Close(); + PumpLoop(); + + gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback, + base::Unretained(this), + &load_result)); + PumpLoop(); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/gservices_settings.cc b/chromium/google_apis/gcm/engine/gservices_settings.cc new file mode 100644 index 00000000000..4e989724b7e --- /dev/null +++ b/chromium/google_apis/gcm/engine/gservices_settings.cc @@ -0,0 +1,342 @@ +// Copyright 2014 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 "google_apis/gcm/engine/gservices_settings.h" + +#include "base/bind.h" +#include "base/sha1.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" + +namespace { +// The expected time in seconds between periodic checkins. +const char kCheckinIntervalKey[] = "checkin_interval"; +// The override URL to the checkin server. +const char kCheckinURLKey[] = "checkin_url"; +// The MCS machine name to connect to. +const char kMCSHostnameKey[] = "gcm_hostname"; +// The MCS port to connect to. +const char kMCSSecurePortKey[] = "gcm_secure_port"; +// The URL to get MCS registration IDs. +const char kRegistrationURLKey[] = "gcm_registration_url"; + +const int64 kDefaultCheckinInterval = 2 * 24 * 60 * 60; // seconds = 2 days. +const int64 kMinimumCheckinInterval = 12 * 60 * 60; // seconds = 12 hours. +const char kDefaultCheckinURL[] = "https://android.clients.google.com/checkin"; +const char kDefaultMCSHostname[] = "mtalk.google.com"; +const int kDefaultMCSMainSecurePort = 5228; +const int kDefaultMCSFallbackSecurePort = 443; +const char kDefaultRegistrationURL[] = + "https://android.clients.google.com/c2dm/register3"; +// Settings that are to be deleted are marked with this prefix in checkin +// response. +const char kDeleteSettingPrefix[] = "delete_"; +// Settings digest starts with verison number followed by '-'. +const char kDigestVersionPrefix[] = "1-"; +const char kMCSEnpointTemplate[] = "https://%s:%d"; +const int kMaxSecurePort = 65535; + +std::string MakeMCSEndpoint(const std::string& mcs_hostname, int port) { + return base::StringPrintf(kMCSEnpointTemplate, mcs_hostname.c_str(), port); +} + +// Default settings can be omitted, as GServicesSettings class provides +// reasonable defaults. +bool CanBeOmitted(const std::string& settings_name) { + return settings_name == kCheckinIntervalKey || + settings_name == kCheckinURLKey || + settings_name == kMCSHostnameKey || + settings_name == kMCSSecurePortKey || + settings_name == kRegistrationURLKey; +} + +bool VerifyCheckinInterval( + const gcm::GServicesSettings::SettingsMap& settings) { + gcm::GServicesSettings::SettingsMap::const_iterator iter = + settings.find(kCheckinIntervalKey); + if (iter == settings.end()) + return CanBeOmitted(kCheckinIntervalKey); + + int64 checkin_interval = kMinimumCheckinInterval; + if (!base::StringToInt64(iter->second, &checkin_interval)) { + DVLOG(1) << "Failed to parse checkin interval: " << iter->second; + return false; + } + if (checkin_interval == std::numeric_limits<int64>::max()) { + DVLOG(1) << "Checkin interval is too big: " << checkin_interval; + return false; + } + if (checkin_interval < kMinimumCheckinInterval) { + DVLOG(1) << "Checkin interval: " << checkin_interval + << " is less than allowed minimum: " << kMinimumCheckinInterval; + } + + return true; +} + +bool VerifyMCSEndpoint(const gcm::GServicesSettings::SettingsMap& settings) { + std::string mcs_hostname; + gcm::GServicesSettings::SettingsMap::const_iterator iter = + settings.find(kMCSHostnameKey); + if (iter == settings.end()) { + // Because endpoint has 2 parts (hostname and port) we are defaulting and + // moving on with verification. + if (CanBeOmitted(kMCSHostnameKey)) + mcs_hostname = kDefaultMCSHostname; + else + return false; + } else if (iter->second.empty()) { + DVLOG(1) << "Empty MCS hostname provided."; + return false; + } else { + mcs_hostname = iter->second; + } + + int mcs_secure_port = 0; + iter = settings.find(kMCSSecurePortKey); + if (iter == settings.end()) { + // Simlarly we might have to default the port, when only hostname is + // provided. + if (CanBeOmitted(kMCSSecurePortKey)) + mcs_secure_port = kDefaultMCSMainSecurePort; + else + return false; + } else if (!base::StringToInt(iter->second, &mcs_secure_port)) { + DVLOG(1) << "Failed to parse MCS secure port: " << iter->second; + return false; + } + + if (mcs_secure_port < 0 || mcs_secure_port > kMaxSecurePort) { + DVLOG(1) << "Incorrect port value: " << mcs_secure_port; + return false; + } + + GURL mcs_main_endpoint(MakeMCSEndpoint(mcs_hostname, mcs_secure_port)); + if (!mcs_main_endpoint.is_valid()) { + DVLOG(1) << "Invalid main MCS endpoint: " + << mcs_main_endpoint.possibly_invalid_spec(); + return false; + } + GURL mcs_fallback_endpoint( + MakeMCSEndpoint(mcs_hostname, kDefaultMCSFallbackSecurePort)); + if (!mcs_fallback_endpoint.is_valid()) { + DVLOG(1) << "Invalid fallback MCS endpoint: " + << mcs_fallback_endpoint.possibly_invalid_spec(); + return false; + } + + return true; +} + +bool VerifyCheckinURL(const gcm::GServicesSettings::SettingsMap& settings) { + gcm::GServicesSettings::SettingsMap::const_iterator iter = + settings.find(kCheckinURLKey); + if (iter == settings.end()) + return CanBeOmitted(kCheckinURLKey); + + GURL checkin_url(iter->second); + if (!checkin_url.is_valid()) { + DVLOG(1) << "Invalid checkin URL provided: " << iter->second; + return false; + } + + return true; +} + +bool VerifyRegistrationURL( + const gcm::GServicesSettings::SettingsMap& settings) { + gcm::GServicesSettings::SettingsMap::const_iterator iter = + settings.find(kRegistrationURLKey); + if (iter == settings.end()) + return CanBeOmitted(kRegistrationURLKey); + + GURL registration_url(iter->second); + if (!registration_url.is_valid()) { + DVLOG(1) << "Invalid registration URL provided: " << iter->second; + return false; + } + + return true; +} + +bool VerifySettings(const gcm::GServicesSettings::SettingsMap& settings) { + return VerifyCheckinInterval(settings) && VerifyMCSEndpoint(settings) && + VerifyCheckinURL(settings) && VerifyRegistrationURL(settings); +} + +} // namespace + +namespace gcm { + +// static +const base::TimeDelta GServicesSettings::MinimumCheckinInterval() { + return base::TimeDelta::FromSeconds(kMinimumCheckinInterval); +} + +// static +const GURL GServicesSettings::DefaultCheckinURL() { + return GURL(kDefaultCheckinURL); +} + +// static +std::string GServicesSettings::CalculateDigest(const SettingsMap& settings) { + unsigned char hash[base::kSHA1Length]; + std::string data; + for (SettingsMap::const_iterator iter = settings.begin(); + iter != settings.end(); + ++iter) { + data += iter->first; + data += '\0'; + data += iter->second; + data += '\0'; + } + base::SHA1HashBytes( + reinterpret_cast<const unsigned char*>(&data[0]), data.size(), hash); + std::string digest = + kDigestVersionPrefix + base::HexEncode(hash, base::kSHA1Length); + digest = StringToLowerASCII(digest); + return digest; +} + +GServicesSettings::GServicesSettings() : weak_ptr_factory_(this) { + digest_ = CalculateDigest(settings_); +} + +GServicesSettings::~GServicesSettings() { +} + +bool GServicesSettings::UpdateFromCheckinResponse( + const checkin_proto::AndroidCheckinResponse& checkin_response) { + if (!checkin_response.has_settings_diff()) { + DVLOG(1) << "Field settings_diff not set in response."; + return false; + } + + bool settings_diff = checkin_response.settings_diff(); + SettingsMap new_settings; + // Only reuse the existing settings, if we are given a settings difference. + if (settings_diff) + new_settings = settings_map(); + + for (int i = 0; i < checkin_response.setting_size(); ++i) { + std::string name = checkin_response.setting(i).name(); + if (name.empty()) { + DVLOG(1) << "Setting name is empty"; + return false; + } + + if (settings_diff && name.find(kDeleteSettingPrefix) == 0) { + std::string setting_to_delete = + name.substr(arraysize(kDeleteSettingPrefix) - 1); + new_settings.erase(setting_to_delete); + DVLOG(1) << "Setting deleted: " << setting_to_delete; + } else { + std::string value = checkin_response.setting(i).value(); + new_settings[name] = value; + DVLOG(1) << "New setting: '" << name << "' : '" << value << "'"; + } + } + + if (!VerifySettings(new_settings)) + return false; + + settings_.swap(new_settings); + digest_ = CalculateDigest(settings_); + return true; +} + +void GServicesSettings::UpdateFromLoadResult( + const GCMStore::LoadResult& load_result) { + // No need to try to update settings when load_result is empty. + if (load_result.gservices_settings.empty()) + return; + if (!VerifySettings(load_result.gservices_settings)) + return; + std::string digest = CalculateDigest(load_result.gservices_settings); + if (digest != load_result.gservices_digest) { + DVLOG(1) << "G-services settings digest mismatch. " + << "Expected digest: " << load_result.gservices_digest + << ". Calculated digest is: " << digest; + return; + } + + settings_ = load_result.gservices_settings; + digest_ = load_result.gservices_digest; +} + +base::TimeDelta GServicesSettings::GetCheckinInterval() const { + int64 checkin_interval = kMinimumCheckinInterval; + SettingsMap::const_iterator iter = settings_.find(kCheckinIntervalKey); + if (iter == settings_.end() || + !base::StringToInt64(iter->second, &checkin_interval)) { + checkin_interval = kDefaultCheckinInterval; + } + + if (checkin_interval < kMinimumCheckinInterval) + checkin_interval = kMinimumCheckinInterval; + + return base::TimeDelta::FromSeconds(checkin_interval); +} + +GURL GServicesSettings::GetCheckinURL() const { + SettingsMap::const_iterator iter = settings_.find(kCheckinURLKey); + if (iter == settings_.end() || iter->second.empty()) + return GURL(kDefaultCheckinURL); + return GURL(iter->second); +} + +GURL GServicesSettings::GetMCSMainEndpoint() const { + // Get alternative hostname or use default. + std::string mcs_hostname; + SettingsMap::const_iterator iter = settings_.find(kMCSHostnameKey); + if (iter != settings_.end() && !iter->second.empty()) + mcs_hostname = iter->second; + else + mcs_hostname = kDefaultMCSHostname; + + // Get alternative secure port or use defualt. + int mcs_secure_port = 0; + iter = settings_.find(kMCSSecurePortKey); + if (iter == settings_.end() || iter->second.empty() || + !base::StringToInt(iter->second, &mcs_secure_port)) { + mcs_secure_port = kDefaultMCSMainSecurePort; + } + + // If constructed address makes sense use it. + GURL mcs_endpoint(MakeMCSEndpoint(mcs_hostname, mcs_secure_port)); + if (mcs_endpoint.is_valid()) + return mcs_endpoint; + + // Otherwise use default settings. + return GURL(MakeMCSEndpoint(kDefaultMCSHostname, kDefaultMCSMainSecurePort)); +} + +GURL GServicesSettings::GetMCSFallbackEndpoint() const { + // Get alternative hostname or use default. + std::string mcs_hostname; + SettingsMap::const_iterator iter = settings_.find(kMCSHostnameKey); + if (iter != settings_.end() && !iter->second.empty()) + mcs_hostname = iter->second; + else + mcs_hostname = kDefaultMCSHostname; + + // If constructed address makes sense use it. + GURL mcs_endpoint( + MakeMCSEndpoint(mcs_hostname, kDefaultMCSFallbackSecurePort)); + if (mcs_endpoint.is_valid()) + return mcs_endpoint; + + return GURL( + MakeMCSEndpoint(kDefaultMCSHostname, kDefaultMCSFallbackSecurePort)); +} + +GURL GServicesSettings::GetRegistrationURL() const { + SettingsMap::const_iterator iter = settings_.find(kRegistrationURLKey); + if (iter == settings_.end() || iter->second.empty()) + return GURL(kDefaultRegistrationURL); + return GURL(iter->second); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/gservices_settings.h b/chromium/google_apis/gcm/engine/gservices_settings.h new file mode 100644 index 00000000000..d3aeeb03d29 --- /dev/null +++ b/chromium/google_apis/gcm/engine/gservices_settings.h @@ -0,0 +1,82 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_GSERVICES_SETTINGS_H_ +#define GOOGLE_APIS_GCM_ENGINE_GSERVICES_SETTINGS_H_ + +#include <map> +#include <string> + +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/engine/gcm_store.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "url/gurl.h" + +namespace gcm { + +// Class responsible for handling G-services settings. It takes care of +// extracting them from checkin response and storing in GCMStore. +class GCM_EXPORT GServicesSettings { + public: + typedef std::map<std::string, std::string> SettingsMap; + + // Minimum periodic checkin interval in seconds. + static const base::TimeDelta MinimumCheckinInterval(); + + // Default checkin URL. + static const GURL DefaultCheckinURL(); + + // Calculates digest of provided settings. + static std::string CalculateDigest(const SettingsMap& settings); + + GServicesSettings(); + ~GServicesSettings(); + + // Updates the settings based on |checkin_response|. + bool UpdateFromCheckinResponse( + const checkin_proto::AndroidCheckinResponse& checkin_response); + + // Updates the settings based on |load_result|. Returns true if update was + // successful, false otherwise. + void UpdateFromLoadResult(const GCMStore::LoadResult& load_result); + + SettingsMap settings_map() const { return settings_; } + + std::string digest() const { return digest_; } + + // Gets the interval at which device should perform a checkin. + base::TimeDelta GetCheckinInterval() const; + + // Gets the URL to use when checking in. + GURL GetCheckinURL() const; + + // Gets address of main MCS endpoint. + GURL GetMCSMainEndpoint() const; + + // Gets address of fallback MCS endpoint. + GURL GetMCSFallbackEndpoint() const; + + // Gets the URL to use when registering or unregistering the apps. + GURL GetRegistrationURL() const; + + private: + // Digest (hash) of the settings, used to check whether settings need update. + // It is meant to be sent with checkin request, instead of sending the whole + // settings table. + std::string digest_; + + // G-services settings as provided by checkin response. + SettingsMap settings_; + + // Factory for creating references in callbacks. + base::WeakPtrFactory<GServicesSettings> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(GServicesSettings); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_GSERVICES_SETTINGS_H_ diff --git a/chromium/google_apis/gcm/engine/gservices_settings_unittest.cc b/chromium/google_apis/gcm/engine/gservices_settings_unittest.cc new file mode 100644 index 00000000000..9e02109200c --- /dev/null +++ b/chromium/google_apis/gcm/engine/gservices_settings_unittest.cc @@ -0,0 +1,336 @@ +// Copyright 2014 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 "base/strings/string_number_conversions.h" +#include "google_apis/gcm/engine/gservices_settings.h" +#include "google_apis/gcm/engine/registration_info.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const int64 kAlternativeCheckinInterval = 16 * 60 * 60; +const char kAlternativeCheckinURL[] = "http://alternative.url/checkin"; +const char kAlternativeMCSHostname[] = "alternative.gcm.host"; +const int kAlternativeMCSSecurePort = 7777; +const char kAlternativeRegistrationURL[] = + "http://alternative.url/registration"; + +const int64 kDefaultCheckinInterval = 2 * 24 * 60 * 60; // seconds = 2 days. +const char kDefaultCheckinURL[] = "https://android.clients.google.com/checkin"; +const char kDefaultRegistrationURL[] = + "https://android.clients.google.com/c2dm/register3"; +const char kDefaultSettingsDigest[] = + "1-da39a3ee5e6b4b0d3255bfef95601890afd80709"; +const char kAlternativeSettingsDigest[] = + "1-7da4aa4eb38a8bd3e330e3751cc0899924499134"; + +void AddSettingsToResponse( + checkin_proto::AndroidCheckinResponse& checkin_response, + const GServicesSettings::SettingsMap& settings, + bool settings_diff) { + for (GServicesSettings::SettingsMap::const_iterator iter = settings.begin(); + iter != settings.end(); + ++iter) { + checkin_proto::GservicesSetting* setting = checkin_response.add_setting(); + setting->set_name(iter->first); + setting->set_value(iter->second); + } + checkin_response.set_settings_diff(settings_diff); +} + +} // namespace + +class GServicesSettingsTest : public testing::Test { + public: + GServicesSettingsTest(); + virtual ~GServicesSettingsTest(); + + void CheckAllSetToDefault(); + + GServicesSettings& settings() { + return gserivces_settings_; + } + + private: + GServicesSettings gserivces_settings_; +}; + +GServicesSettingsTest::GServicesSettingsTest() + : gserivces_settings_() { +} + +GServicesSettingsTest::~GServicesSettingsTest() {} + +void GServicesSettingsTest::CheckAllSetToDefault() { + EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval), + settings().GetCheckinInterval()); + EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL()); + EXPECT_EQ(GURL("https://mtalk.google.com:5228"), + settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://mtalk.google.com:443"), + settings().GetMCSFallbackEndpoint()); + EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL()); +} + +// Verifies default values of the G-services settings and settings digest. +TEST_F(GServicesSettingsTest, DefaultSettingsAndDigest) { + CheckAllSetToDefault(); + EXPECT_EQ(kDefaultSettingsDigest, settings().digest()); + EXPECT_EQ(kDefaultSettingsDigest, + GServicesSettings::CalculateDigest(settings().settings_map())); +} + +// Verifies digest calculation for the sample provided by protocol owners. +TEST_F(GServicesSettingsTest, CalculateDigest) { + GServicesSettings::SettingsMap settings_map; + settings_map["android_id"] = "55XXXXXXXXXXXXXXXX0"; + settings_map["checkin_interval"] = "86400"; + settings_map["checkin_url"] = + "https://fake.address.google.com/canary/checkin"; + settings_map["chrome_device"] = "1"; + settings_map["device_country"] = "us"; + settings_map["gcm_hostname"] = "fake.address.google.com"; + settings_map["gcm_secure_port"] = "443"; + + EXPECT_EQ("1-33381ccd1cf5791dc0e6dfa234266fa9f1259197", + GServicesSettings::CalculateDigest(settings_map)); +} + +// Verifies that settings are not updated when load result is empty. +TEST_F(GServicesSettingsTest, UpdateFromEmptyLoadResult) { + GCMStore::LoadResult result; + result.gservices_digest = ""; + settings().UpdateFromLoadResult(result); + + CheckAllSetToDefault(); + EXPECT_EQ(kDefaultSettingsDigest, settings().digest()); +} + +// Verifies that settings are not when digest value does not match. +TEST_F(GServicesSettingsTest, UpdateFromLoadResultWithSettingMissing) { + GCMStore::LoadResult result; + result.gservices_settings["checkin_internval"] = "100000"; + result.gservices_digest = "digest_value"; + settings().UpdateFromLoadResult(result); + + CheckAllSetToDefault(); + EXPECT_EQ(kDefaultSettingsDigest, settings().digest()); +} + +// Verifies that the settings are set correctly based on the load result. +TEST_F(GServicesSettingsTest, UpdateFromLoadResult) { + GCMStore::LoadResult result; + result.gservices_settings["checkin_interval"] = + base::Int64ToString(kAlternativeCheckinInterval); + result.gservices_settings["checkin_url"] = kAlternativeCheckinURL; + result.gservices_settings["gcm_hostname"] = kAlternativeMCSHostname; + result.gservices_settings["gcm_secure_port"] = + base::IntToString(kAlternativeMCSSecurePort); + result.gservices_settings["gcm_registration_url"] = + kAlternativeRegistrationURL; + result.gservices_digest = kAlternativeSettingsDigest; + settings().UpdateFromLoadResult(result); + + EXPECT_EQ(base::TimeDelta::FromSeconds(kAlternativeCheckinInterval), + settings().GetCheckinInterval()); + EXPECT_EQ(GURL(kAlternativeCheckinURL), settings().GetCheckinURL()); + EXPECT_EQ(GURL("https://alternative.gcm.host:7777"), + settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://alternative.gcm.host:443"), + settings().GetMCSFallbackEndpoint()); + EXPECT_EQ(GURL(kAlternativeRegistrationURL), settings().GetRegistrationURL()); + EXPECT_EQ(GServicesSettings::CalculateDigest(result.gservices_settings), + settings().digest()); +} + +// Verifies that the checkin interval is updated to minimum if the original +// value is less than minimum. +TEST_F(GServicesSettingsTest, CheckinResponseMinimumCheckinInterval) { + // Setting the checkin interval to less than minimum. + checkin_proto::AndroidCheckinResponse checkin_response; + GServicesSettings::SettingsMap new_settings; + new_settings["checkin_interval"] = "3600"; + AddSettingsToResponse(checkin_response, new_settings, false); + + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + + EXPECT_EQ(GServicesSettings::MinimumCheckinInterval(), + settings().GetCheckinInterval()); + EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings), + settings().digest()); +} + +// Verifies that default checkin interval can be selectively overwritten. +TEST_F(GServicesSettingsTest, CheckinResponseUpdateCheckinInterval) { + checkin_proto::AndroidCheckinResponse checkin_response; + GServicesSettings::SettingsMap new_settings; + new_settings["checkin_interval"] = "86400"; + AddSettingsToResponse(checkin_response, new_settings, false); + + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + + // Only the checkin interval was updated: + EXPECT_EQ(base::TimeDelta::FromSeconds(86400), + settings().GetCheckinInterval()); + + // Other settings still set to default. + EXPECT_EQ(GURL("https://mtalk.google.com:5228"), + settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://mtalk.google.com:443"), + settings().GetMCSFallbackEndpoint()); + EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL()); + EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL()); + + EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings), + settings().digest()); +} + +// Verifies that default registration URL can be selectively overwritten. +TEST_F(GServicesSettingsTest, CheckinResponseUpdateRegistrationURL) { + checkin_proto::AndroidCheckinResponse checkin_response; + GServicesSettings::SettingsMap new_settings; + new_settings["gcm_registration_url"] = "https://new.registration.url"; + AddSettingsToResponse(checkin_response, new_settings, false); + + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + + // Only the registration URL was updated: + EXPECT_EQ(GURL("https://new.registration.url"), + settings().GetRegistrationURL()); + + // Other settings still set to default. + EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval), + settings().GetCheckinInterval()); + EXPECT_EQ(GURL("https://mtalk.google.com:5228"), + settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://mtalk.google.com:443"), + settings().GetMCSFallbackEndpoint()); + EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL()); + + EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings), + settings().digest()); +} + +// Verifies that default checkin URL can be selectively overwritten. +TEST_F(GServicesSettingsTest, CheckinResponseUpdateCheckinURL) { + checkin_proto::AndroidCheckinResponse checkin_response; + GServicesSettings::SettingsMap new_settings; + new_settings["checkin_url"] = "https://new.checkin.url"; + AddSettingsToResponse(checkin_response, new_settings, false); + + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + + // Only the checkin URL was updated: + EXPECT_EQ(GURL("https://new.checkin.url"), settings().GetCheckinURL()); + + // Other settings still set to default. + EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval), + settings().GetCheckinInterval()); + EXPECT_EQ(GURL("https://mtalk.google.com:5228"), + settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://mtalk.google.com:443"), + settings().GetMCSFallbackEndpoint()); + EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL()); + + EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings), + settings().digest()); +} + +// Verifies that default MCS hostname can be selectively overwritten. +TEST_F(GServicesSettingsTest, CheckinResponseUpdateMCSHostname) { + checkin_proto::AndroidCheckinResponse checkin_response; + GServicesSettings::SettingsMap new_settings; + new_settings["gcm_hostname"] = "new.gcm.hostname"; + AddSettingsToResponse(checkin_response, new_settings, false); + + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + + // Only the MCS endpoints were updated: + EXPECT_EQ(GURL("https://new.gcm.hostname:5228"), + settings().GetMCSMainEndpoint()); + EXPECT_EQ(GURL("https://new.gcm.hostname:443"), + settings().GetMCSFallbackEndpoint()); + + // Other settings still set to default. + EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval), + settings().GetCheckinInterval()); + EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL()); + EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL()); + + EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings), + settings().digest()); +} + +// Verifies that default MCS secure port can be selectively overwritten. +TEST_F(GServicesSettingsTest, CheckinResponseUpdateMCSSecurePort) { + checkin_proto::AndroidCheckinResponse checkin_response; + GServicesSettings::SettingsMap new_settings; + new_settings["gcm_secure_port"] = "5229"; + AddSettingsToResponse(checkin_response, new_settings, false); + + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + + // Only the main MCS endpoint was updated: + EXPECT_EQ(GURL("https://mtalk.google.com:5229"), + settings().GetMCSMainEndpoint()); + + // Other settings still set to default. + EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval), + settings().GetCheckinInterval()); + EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL()); + EXPECT_EQ(GURL("https://mtalk.google.com:443"), + settings().GetMCSFallbackEndpoint()); + EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL()); + + EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings), + settings().digest()); +} + +// Update from checkin response should also do incremental update for both cases +// where some settings are removed or added. +TEST_F(GServicesSettingsTest, UpdateFromCheckinResponseSettingsDiff) { + checkin_proto::AndroidCheckinResponse checkin_response; + + // Only the new settings will be included in the response with settings diff. + GServicesSettings::SettingsMap settings_diff; + settings_diff["new_setting_1"] = "new_setting_1_value"; + settings_diff["new_setting_2"] = "new_setting_2_value"; + settings_diff["gcm_secure_port"] = "5229"; + + // Full settings are necessary to calculate digest. + GServicesSettings::SettingsMap full_settings(settings_diff); + std::string digest = GServicesSettings::CalculateDigest(full_settings); + + checkin_response.Clear(); + AddSettingsToResponse(checkin_response, settings_diff, true); + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + EXPECT_EQ(full_settings, settings().settings_map()); + // Default setting overwritten by settings diff. + EXPECT_EQ(GURL("https://mtalk.google.com:5229"), + settings().GetMCSMainEndpoint()); + + // Setting up diff removing some of the values (including default setting). + settings_diff.clear(); + settings_diff["delete_new_setting_1"] = ""; + settings_diff["delete_gcm_secure_port"] = ""; + settings_diff["new_setting_3"] = "new_setting_3_value"; + + // Updating full settings to calculate digest. + full_settings.erase(full_settings.find("new_setting_1")); + full_settings.erase(full_settings.find("gcm_secure_port")); + full_settings["new_setting_3"] = "new_setting_3_value"; + digest = GServicesSettings::CalculateDigest(full_settings); + + checkin_response.Clear(); + AddSettingsToResponse(checkin_response, settings_diff, true); + EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response)); + EXPECT_EQ(full_settings, settings().settings_map()); + // Default setting back to norm. + EXPECT_EQ(GURL("https://mtalk.google.com:5228"), + settings().GetMCSMainEndpoint()); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/heartbeat_manager.cc b/chromium/google_apis/gcm/engine/heartbeat_manager.cc new file mode 100644 index 00000000000..5b5ae476a4b --- /dev/null +++ b/chromium/google_apis/gcm/engine/heartbeat_manager.cc @@ -0,0 +1,119 @@ +// Copyright 2014 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 "google_apis/gcm/engine/heartbeat_manager.h" + +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/base/network_change_notifier.h" + +namespace gcm { + +namespace { +// The default heartbeat when on a mobile or unknown network . +const int64 kCellHeartbeatDefaultMs = 1000 * 60 * 28; // 28 minutes. +// The default heartbeat when on WiFi (also used for ethernet). +const int64 kWifiHeartbeatDefaultMs = 1000 * 60 * 15; // 15 minutes. +// The default heartbeat ack interval. +const int64 kHeartbeatAckDefaultMs = 1000 * 60 * 1; // 1 minute. +} // namespace + +HeartbeatManager::HeartbeatManager() + : waiting_for_ack_(false), + heartbeat_interval_ms_(0), + server_interval_ms_(0), + heartbeat_timer_(true /* retain user task */, + false /* not repeating */), + weak_ptr_factory_(this) {} + +HeartbeatManager::~HeartbeatManager() {} + +void HeartbeatManager::Start( + const base::Closure& send_heartbeat_callback, + const base::Closure& trigger_reconnect_callback) { + DCHECK(!send_heartbeat_callback.is_null()); + DCHECK(!trigger_reconnect_callback.is_null()); + send_heartbeat_callback_ = send_heartbeat_callback; + trigger_reconnect_callback_ = trigger_reconnect_callback; + + // Kicks off the timer. + waiting_for_ack_ = false; + RestartTimer(); +} + +void HeartbeatManager::Stop() { + heartbeat_timer_.Stop(); + waiting_for_ack_ = false; +} + +void HeartbeatManager::OnHeartbeatAcked() { + if (!heartbeat_timer_.IsRunning()) + return; + + DCHECK(!send_heartbeat_callback_.is_null()); + DCHECK(!trigger_reconnect_callback_.is_null()); + waiting_for_ack_ = false; + RestartTimer(); +} + +void HeartbeatManager::UpdateHeartbeatConfig( + const mcs_proto::HeartbeatConfig& config) { + if (!config.IsInitialized() || + !config.has_interval_ms() || + config.interval_ms() <= 0) { + return; + } + DVLOG(1) << "Updating heartbeat interval to " << config.interval_ms(); + server_interval_ms_ = config.interval_ms(); +} + +base::TimeTicks HeartbeatManager::GetNextHeartbeatTime() const { + if (heartbeat_timer_.IsRunning()) + return heartbeat_timer_.desired_run_time(); + else + return base::TimeTicks(); +} + +void HeartbeatManager::OnHeartbeatTriggered() { + if (waiting_for_ack_) { + LOG(WARNING) << "Lost connection to MCS, reconnecting."; + Stop(); + trigger_reconnect_callback_.Run(); + return; + } + + waiting_for_ack_ = true; + RestartTimer(); + send_heartbeat_callback_.Run(); +} + +void HeartbeatManager::RestartTimer() { + if (!waiting_for_ack_) { + // Recalculate the timer interval based network type. + if (server_interval_ms_ != 0) { + // If a server interval is set, it overrides any local one. + heartbeat_interval_ms_ = server_interval_ms_; + } else if (net::NetworkChangeNotifier::GetConnectionType() == + net::NetworkChangeNotifier::CONNECTION_WIFI || + net::NetworkChangeNotifier::GetConnectionType() == + net::NetworkChangeNotifier::CONNECTION_ETHERNET) { + heartbeat_interval_ms_ = kWifiHeartbeatDefaultMs; + } else { + // For unknown connections, use the longer cellular heartbeat interval. + heartbeat_interval_ms_ = kCellHeartbeatDefaultMs; + } + DVLOG(1) << "Sending next heartbeat in " + << heartbeat_interval_ms_ << " ms."; + } else { + heartbeat_interval_ms_ = kHeartbeatAckDefaultMs; + DVLOG(1) << "Resetting timer for ack with " + << heartbeat_interval_ms_ << " ms interval."; + } + heartbeat_timer_.Start(FROM_HERE, + base::TimeDelta::FromMilliseconds( + heartbeat_interval_ms_), + base::Bind(&HeartbeatManager::OnHeartbeatTriggered, + weak_ptr_factory_.GetWeakPtr())); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/heartbeat_manager.h b/chromium/google_apis/gcm/engine/heartbeat_manager.h new file mode 100644 index 00000000000..9266b98c8d4 --- /dev/null +++ b/chromium/google_apis/gcm/engine/heartbeat_manager.h @@ -0,0 +1,82 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_HEARTBEAT_MANAGER_H_ +#define GOOGLE_APIS_GCM_ENGINE_HEARTBEAT_MANAGER_H_ + +#include "base/callback.h" +#include "base/logging.h" +#include "base/memory/weak_ptr.h" +#include "base/timer/timer.h" +#include "google_apis/gcm/base/gcm_export.h" + +namespace mcs_proto { +class HeartbeatConfig; +} + +namespace gcm { + +// A heartbeat management class, capable of sending and handling heartbeat +// receipt/failures and triggering reconnection as necessary. +class GCM_EXPORT HeartbeatManager { + public: + HeartbeatManager(); + ~HeartbeatManager(); + + // Start the heartbeat logic. + // |send_heartbeat_callback_| is the callback the HeartbeatManager uses to + // send new heartbeats. Only one heartbeat can be outstanding at a time. + void Start(const base::Closure& send_heartbeat_callback, + const base::Closure& trigger_reconnect_callback); + + // Stop the timer. Start(..) must be called again to begin sending heartbeats + // afterwards. + void Stop(); + + // Reset the heartbeat timer. It is valid to call this even if no heartbeat + // is associated with the ack (for example if another signal is used to + // determine that the connection is alive). + void OnHeartbeatAcked(); + + // Updates the current heartbeat interval. + void UpdateHeartbeatConfig(const mcs_proto::HeartbeatConfig& config); + + // Returns the next scheduled heartbeat time. A null time means + // no heartbeat is pending. If non-null and less than the + // current time (in ticks), the heartbeat has been triggered and an ack is + // pending. + base::TimeTicks GetNextHeartbeatTime() const; + + protected: + // Helper method to send heartbeat on timer trigger. + void OnHeartbeatTriggered(); + + private: + // Restarts the heartbeat timer. + void RestartTimer(); + + // Whether the last heartbeat ping sent has been acknowledged or not. + bool waiting_for_ack_; + + // The current heartbeat interval. + int heartbeat_interval_ms_; + // The most recent server-provided heartbeat interval (0 if none has been + // provided). + int server_interval_ms_; + + // Timer for triggering heartbeats. + base::Timer heartbeat_timer_; + + // Callbacks for interacting with the the connection. + base::Closure send_heartbeat_callback_; + base::Closure trigger_reconnect_callback_; + + base::WeakPtrFactory<HeartbeatManager> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(HeartbeatManager); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_HEARTBEAT_MANAGER_H_ diff --git a/chromium/google_apis/gcm/engine/heartbeat_manager_unittest.cc b/chromium/google_apis/gcm/engine/heartbeat_manager_unittest.cc new file mode 100644 index 00000000000..2c3b668d533 --- /dev/null +++ b/chromium/google_apis/gcm/engine/heartbeat_manager_unittest.cc @@ -0,0 +1,176 @@ +// Copyright 2014 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 "google_apis/gcm/engine/heartbeat_manager.h" + +#include "base/message_loop/message_loop.h" +#include "base/time/time.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +mcs_proto::HeartbeatConfig BuildHeartbeatConfig(int interval_ms) { + mcs_proto::HeartbeatConfig config; + config.set_interval_ms(interval_ms); + return config; +} + +class TestHeartbeatManager : public HeartbeatManager { + public: + TestHeartbeatManager() {} + virtual ~TestHeartbeatManager() {} + + // Bypass the heartbeat timer, and send the heartbeat now. + void TriggerHearbeat(); +}; + +void TestHeartbeatManager::TriggerHearbeat() { + OnHeartbeatTriggered(); +} + +class HeartbeatManagerTest : public testing::Test { + public: + HeartbeatManagerTest(); + virtual ~HeartbeatManagerTest() {} + + TestHeartbeatManager* manager() const { return manager_.get(); } + int heartbeats_sent() const { return heartbeats_sent_; } + int reconnects_triggered() const { return reconnects_triggered_; } + + // Starts the heartbeat manager. + void StartManager(); + + private: + // Helper functions for verifying heartbeat manager effects. + void SendHeartbeatClosure(); + void TriggerReconnectClosure(); + + scoped_ptr<TestHeartbeatManager> manager_; + + int heartbeats_sent_; + int reconnects_triggered_; + + base::MessageLoop message_loop_; +}; + +HeartbeatManagerTest::HeartbeatManagerTest() + : manager_(new TestHeartbeatManager()), + heartbeats_sent_(0), + reconnects_triggered_(0) { +} + +void HeartbeatManagerTest::StartManager() { + manager_->Start(base::Bind(&HeartbeatManagerTest::SendHeartbeatClosure, + base::Unretained(this)), + base::Bind(&HeartbeatManagerTest::TriggerReconnectClosure, + base::Unretained(this))); +} + +void HeartbeatManagerTest::SendHeartbeatClosure() { + heartbeats_sent_++; +} + +void HeartbeatManagerTest::TriggerReconnectClosure() { + reconnects_triggered_++; +} + +// Basic initialization. No heartbeat should be pending. +TEST_F(HeartbeatManagerTest, Init) { + EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null()); +} + +// Acknowledging a heartbeat before starting the manager should have no effect. +TEST_F(HeartbeatManagerTest, AckBeforeStart) { + manager()->OnHeartbeatAcked(); + EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null()); +} + +// Starting the manager should start the heartbeat timer. +TEST_F(HeartbeatManagerTest, Start) { + StartManager(); + EXPECT_GT(manager()->GetNextHeartbeatTime(), base::TimeTicks::Now()); + EXPECT_EQ(0, heartbeats_sent()); + EXPECT_EQ(0, reconnects_triggered()); +} + +// Acking the heartbeat should trigger a new heartbeat timer. +TEST_F(HeartbeatManagerTest, AckedHeartbeat) { + StartManager(); + manager()->TriggerHearbeat(); + base::TimeTicks heartbeat = manager()->GetNextHeartbeatTime(); + EXPECT_GT(heartbeat, base::TimeTicks::Now()); + EXPECT_EQ(1, heartbeats_sent()); + EXPECT_EQ(0, reconnects_triggered()); + + manager()->OnHeartbeatAcked(); + EXPECT_LT(heartbeat, manager()->GetNextHeartbeatTime()); + EXPECT_EQ(1, heartbeats_sent()); + EXPECT_EQ(0, reconnects_triggered()); + + manager()->TriggerHearbeat(); + EXPECT_EQ(2, heartbeats_sent()); + EXPECT_EQ(0, reconnects_triggered()); +} + +// Trigger a heartbeat when one was outstanding should reset the connection. +TEST_F(HeartbeatManagerTest, UnackedHeartbeat) { + StartManager(); + manager()->TriggerHearbeat(); + EXPECT_EQ(1, heartbeats_sent()); + EXPECT_EQ(0, reconnects_triggered()); + + manager()->TriggerHearbeat(); + EXPECT_EQ(1, heartbeats_sent()); + EXPECT_EQ(1, reconnects_triggered()); +} + +// Updating the heartbeat interval before starting should result in the new +// interval being used at Start time. +TEST_F(HeartbeatManagerTest, UpdateIntervalThenStart) { + const int kIntervalMs = 60 * 1000; // 60 seconds. + manager()->UpdateHeartbeatConfig(BuildHeartbeatConfig(kIntervalMs)); + EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null()); + StartManager(); + EXPECT_LE(manager()->GetNextHeartbeatTime() - base::TimeTicks::Now(), + base::TimeDelta::FromMilliseconds(kIntervalMs)); +} + +// Updating the heartbeat interval after starting should only use the new +// interval on the next heartbeat. +TEST_F(HeartbeatManagerTest, StartThenUpdateInterval) { + const int kIntervalMs = 60 * 1000; // 60 seconds. + StartManager(); + base::TimeTicks heartbeat = manager()->GetNextHeartbeatTime(); + EXPECT_GT(heartbeat - base::TimeTicks::Now(), + base::TimeDelta::FromMilliseconds(kIntervalMs)); + + // Updating the interval should not affect an outstanding heartbeat. + manager()->UpdateHeartbeatConfig(BuildHeartbeatConfig(kIntervalMs)); + EXPECT_EQ(heartbeat, manager()->GetNextHeartbeatTime()); + + // Triggering and acking the heartbeat should result in a heartbeat being + // posted with the new interval. + manager()->TriggerHearbeat(); + manager()->OnHeartbeatAcked(); + + EXPECT_LE(manager()->GetNextHeartbeatTime() - base::TimeTicks::Now(), + base::TimeDelta::FromMilliseconds(kIntervalMs)); + EXPECT_NE(heartbeat, manager()->GetNextHeartbeatTime()); +} + +// Stopping the manager should reset the heartbeat timer. +TEST_F(HeartbeatManagerTest, Stop) { + StartManager(); + EXPECT_GT(manager()->GetNextHeartbeatTime(), base::TimeTicks::Now()); + + manager()->Stop(); + EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null()); +} + +} // namespace + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/mcs_client.cc b/chromium/google_apis/gcm/engine/mcs_client.cc index f0af051379f..a99a333a37e 100644 --- a/chromium/google_apis/gcm/engine/mcs_client.cc +++ b/chromium/google_apis/gcm/engine/mcs_client.cc @@ -4,13 +4,18 @@ #include "google_apis/gcm/engine/mcs_client.h" +#include <set> + #include "base/basictypes.h" #include "base/message_loop/message_loop.h" +#include "base/metrics/histogram.h" #include "base/strings/string_number_conversions.h" +#include "base/time/clock.h" +#include "base/time/time.h" #include "google_apis/gcm/base/mcs_util.h" #include "google_apis/gcm/base/socket_stream.h" #include "google_apis/gcm/engine/connection_factory.h" -#include "google_apis/gcm/engine/rmq_store.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" using namespace google::protobuf::io; @@ -20,9 +25,6 @@ namespace { typedef scoped_ptr<google::protobuf::MessageLite> MCSProto; -// TODO(zea): get these values from MCS settings. -const int64 kHeartbeatDefaultSeconds = 60 * 15; // 15 minutes. - // The category of messages intended for the GCM client itself from MCS. const char kMCSCategory[] = "com.google.android.gsf.gtalkservice"; @@ -30,8 +32,8 @@ const char kMCSCategory[] = "com.google.android.gsf.gtalkservice"; const char kGCMFromField[] = "gcm@android.com"; // MCS status message types. +// TODO(zea): handle these at the GCMClient layer. const char kIdleNotification[] = "IdleNotification"; -// TODO(zea): consume the following message types: // const char kAlwaysShowOnIdle[] = "ShowAwayOnIdle"; // const char kPowerNotification[] = "PowerNotification"; // const char kDataActiveNotification[] = "DataActiveNotification"; @@ -64,6 +66,47 @@ bool BuildPersistentIdListFromProto(const google::protobuf::string& bytes, } // namespace +class CollapseKey { + public: + explicit CollapseKey(const mcs_proto::DataMessageStanza& message); + ~CollapseKey(); + + // Comparison operator for use in maps. + bool operator<(const CollapseKey& right) const; + + // Whether the message had a valid collapse key. + bool IsValid() const; + + std::string token() const { return token_; } + std::string app_id() const { return app_id_; } + int64 device_user_id() const { return device_user_id_; } + + private: + const std::string token_; + const std::string app_id_; + const int64 device_user_id_; +}; + +CollapseKey::CollapseKey(const mcs_proto::DataMessageStanza& message) + : token_(message.token()), + app_id_(message.category()), + device_user_id_(message.device_user_id()) {} + +CollapseKey::~CollapseKey() {} + +bool CollapseKey::IsValid() const { + // Device user id is optional, but the application id and token are not. + return !token_.empty() && !app_id_.empty(); +} + +bool CollapseKey::operator<(const CollapseKey& right) const { + if (device_user_id_ != right.device_user_id()) + return device_user_id_ < right.device_user_id(); + if (app_id_ != right.app_id()) + return app_id_ < right.app_id(); + return token_ < right.token(); +} + struct ReliablePacketInfo { ReliablePacketInfo(); ~ReliablePacketInfo(); @@ -86,11 +129,38 @@ ReliablePacketInfo::ReliablePacketInfo() } ReliablePacketInfo::~ReliablePacketInfo() {} -MCSClient::MCSClient( - const base::FilePath& rmq_path, - ConnectionFactory* connection_factory, - scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) - : state_(UNINITIALIZED), +int MCSClient::GetSendQueueSize() const { + return to_send_.size(); +} + +int MCSClient::GetResendQueueSize() const { + return to_resend_.size(); +} + +std::string MCSClient::GetStateString() const { + switch(state_) { + case UNINITIALIZED: + return "UNINITIALIZED"; + case LOADED: + return "LOADED"; + case CONNECTING: + return "CONNECTING"; + case CONNECTED: + return "CONNECTED"; + default: + NOTREACHED(); + return std::string(); + } +} + +MCSClient::MCSClient(const std::string& version_string, + base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + GCMStatsRecorder* recorder) + : version_string_(version_string), + clock_(clock), + state_(UNINITIALIZED), android_id_(0), security_token_(0), connection_factory_(connection_factory), @@ -99,11 +169,8 @@ MCSClient::MCSClient( last_server_to_device_stream_id_received_(0), stream_id_out_(0), stream_id_in_(0), - rmq_store_(rmq_path, blocking_task_runner), - heartbeat_interval_( - base::TimeDelta::FromSeconds(kHeartbeatDefaultSeconds)), - heartbeat_timer_(true, true), - blocking_task_runner_(blocking_task_runner), + gcm_store_(gcm_store), + recorder_(recorder), weak_ptr_factory_(this) { } @@ -111,18 +178,17 @@ MCSClient::~MCSClient() { } void MCSClient::Initialize( - const InitializationCompleteCallback& initialization_callback, + const ErrorCallback& error_callback, const OnMessageReceivedCallback& message_received_callback, - const OnMessageSentCallback& message_sent_callback) { + const OnMessageSentCallback& message_sent_callback, + scoped_ptr<GCMStore::LoadResult> load_result) { DCHECK_EQ(state_, UNINITIALIZED); - initialization_callback_ = initialization_callback; + + state_ = LOADED; + mcs_error_callback_ = error_callback; message_received_callback_ = message_received_callback; message_sent_callback_ = message_sent_callback; - state_ = LOADING; - rmq_store_.Load(base::Bind(&MCSClient::OnRMQLoadFinished, - weak_ptr_factory_.GetWeakPtr())); - connection_factory_->Initialize( base::Bind(&MCSClient::ResetStateAndBuildLoginRequest, weak_ptr_factory_.GetWeakPtr()), @@ -131,71 +197,171 @@ void MCSClient::Initialize( base::Bind(&MCSClient::MaybeSendMessage, weak_ptr_factory_.GetWeakPtr())); connection_handler_ = connection_factory_->GetConnectionHandler(); + + stream_id_out_ = 1; // Login request is hardcoded to id 1. + + android_id_ = load_result->device_android_id; + security_token_ = load_result->device_security_token; + + if (android_id_ == 0) { + DVLOG(1) << "No device credentials found, assuming new client."; + // No need to try and load RMQ data in that case. + return; + } + + // |android_id_| is non-zero, so should |security_token_|. + DCHECK_NE(0u, security_token_) << "Security token invalid, while android id" + << " is non-zero."; + + DVLOG(1) << "RMQ Load finished with " << load_result->incoming_messages.size() + << " incoming acks pending and " + << load_result->outgoing_messages.size() + << " outgoing messages pending."; + + restored_unackeds_server_ids_ = load_result->incoming_messages; + + // First go through and order the outgoing messages by recency. + std::map<uint64, google::protobuf::MessageLite*> ordered_messages; + std::vector<PersistentId> expired_ttl_ids; + for (GCMStore::OutgoingMessageMap::iterator iter = + load_result->outgoing_messages.begin(); + iter != load_result->outgoing_messages.end(); ++iter) { + uint64 timestamp = 0; + if (!base::StringToUint64(iter->first, ×tamp)) { + LOG(ERROR) << "Invalid restored message."; + // TODO(fgorski): Error: data unreadable + mcs_error_callback_.Run(); + return; + } + + // Check if the TTL has expired for this message. + if (HasTTLExpired(*iter->second, clock_)) { + expired_ttl_ids.push_back(iter->first); + NotifyMessageSendStatus(*iter->second, TTL_EXCEEDED); + continue; + } + + ordered_messages[timestamp] = iter->second.release(); + } + + if (!expired_ttl_ids.empty()) { + gcm_store_->RemoveOutgoingMessages( + expired_ttl_ids, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + } + + // Now go through and add the outgoing messages to the send queue in their + // appropriate order (oldest at front, most recent at back). + for (std::map<uint64, google::protobuf::MessageLite*>::iterator + iter = ordered_messages.begin(); + iter != ordered_messages.end(); ++iter) { + ReliablePacketInfo* packet_info = new ReliablePacketInfo(); + packet_info->protobuf.reset(iter->second); + packet_info->tag = GetMCSProtoTag(*iter->second); + packet_info->persistent_id = base::Uint64ToString(iter->first); + to_send_.push_back(make_linked_ptr(packet_info)); + + if (packet_info->tag == kDataMessageStanzaTag) { + mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<mcs_proto::DataMessageStanza*>( + packet_info->protobuf.get()); + CollapseKey collapse_key(*data_message); + if (collapse_key.IsValid()) + collapse_key_map_[collapse_key] = packet_info; + } + } } void MCSClient::Login(uint64 android_id, uint64 security_token) { DCHECK_EQ(state_, LOADED); + DCHECK(android_id_ == 0 || android_id_ == android_id); + DCHECK(security_token_ == 0 || security_token_ == security_token); + if (android_id != android_id_ && security_token != security_token_) { DCHECK(android_id); DCHECK(security_token); - DCHECK(restored_unackeds_server_ids_.empty()); android_id_ = android_id; security_token_ = security_token; - rmq_store_.SetDeviceCredentials(android_id_, - security_token_, - base::Bind(&MCSClient::OnRMQUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); } + DCHECK(android_id_ != 0 || restored_unackeds_server_ids_.empty()); + state_ = CONNECTING; connection_factory_->Connect(); } -void MCSClient::SendMessage(const MCSMessage& message, bool use_rmq) { - DCHECK_EQ(state_, CONNECTED); +void MCSClient::SendMessage(const MCSMessage& message) { + int ttl = GetTTL(message.GetProtobuf()); + DCHECK_GE(ttl, 0); if (to_send_.size() > kMaxSendQueueSize) { - base::MessageLoop::current()->PostTask( - FROM_HERE, - base::Bind(message_sent_callback_, "Message queue full.")); + NotifyMessageSendStatus(message.GetProtobuf(), QUEUE_SIZE_LIMIT_REACHED); return; } if (message.size() > kMaxMessageBytes) { - base::MessageLoop::current()->PostTask( - FROM_HERE, - base::Bind(message_sent_callback_, "Message too large.")); + NotifyMessageSendStatus(message.GetProtobuf(), MESSAGE_TOO_LARGE); return; } - ReliablePacketInfo* packet_info = new ReliablePacketInfo(); + scoped_ptr<ReliablePacketInfo> packet_info(new ReliablePacketInfo()); + packet_info->tag = message.tag(); packet_info->protobuf = message.CloneProtobuf(); - if (use_rmq) { - PersistentId persistent_id = GetNextPersistentId(); - DVLOG(1) << "Setting persistent id to " << persistent_id; - packet_info->persistent_id = persistent_id; - SetPersistentId(persistent_id, - packet_info->protobuf.get()); - rmq_store_.AddOutgoingMessage(persistent_id, - MCSMessage(message.tag(), - *(packet_info->protobuf)), - base::Bind(&MCSClient::OnRMQUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); - } else { - // Check that there is an active connection to the endpoint. - if (!connection_handler_->CanSendMessage()) { - base::MessageLoop::current()->PostTask( - FROM_HERE, - base::Bind(message_sent_callback_, "Unable to reach endpoint")); + if (ttl > 0) { + DCHECK_EQ(message.tag(), kDataMessageStanzaTag); + + // First check if this message should replace a pending message with the + // same collapse key. + mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<mcs_proto::DataMessageStanza*>( + packet_info->protobuf.get()); + CollapseKey collapse_key(*data_message); + if (collapse_key.IsValid() && collapse_key_map_.count(collapse_key) > 0) { + ReliablePacketInfo* original_packet = collapse_key_map_[collapse_key]; + DVLOG(1) << "Found matching collapse key, Reusing persistent id of " + << original_packet->persistent_id; + original_packet->protobuf = packet_info->protobuf.Pass(); + SetPersistentId(original_packet->persistent_id, + original_packet->protobuf.get()); + gcm_store_->OverwriteOutgoingMessage( + original_packet->persistent_id, + message, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + + // The message is already queued, return. return; + } else { + PersistentId persistent_id = GetNextPersistentId(); + DVLOG(1) << "Setting persistent id to " << persistent_id; + packet_info->persistent_id = persistent_id; + SetPersistentId(persistent_id, packet_info->protobuf.get()); + if (!gcm_store_->AddOutgoingMessage( + persistent_id, + MCSMessage(message.tag(), *(packet_info->protobuf)), + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr()))) { + NotifyMessageSendStatus(message.GetProtobuf(), + APP_QUEUE_SIZE_LIMIT_REACHED); + return; + } } + + if (collapse_key.IsValid()) + collapse_key_map_[collapse_key] = packet_info.get(); + } else if (!connection_factory_->IsEndpointReachable()) { + DVLOG(1) << "No active connection, dropping message."; + NotifyMessageSendStatus(message.GetProtobuf(), NO_CONNECTION_ON_ZERO_TTL); + return; } - to_send_.push_back(make_linked_ptr(packet_info)); - MaybeSendMessage(); -} -void MCSClient::Destroy() { - rmq_store_.Destroy(base::Bind(&MCSClient::OnRMQUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); + to_send_.push_back(make_linked_ptr(packet_info.release())); + + // Notify that the messages has been succsfully queued for sending. + // TODO(jianli): We should report QUEUED after writing to GCM store succeeds. + NotifyMessageSendStatus(message.GetProtobuf(), QUEUED); + + MaybeSendMessage(); } void MCSClient::ResetStateAndBuildLoginRequest( @@ -207,7 +373,7 @@ void MCSClient::ResetStateAndBuildLoginRequest( last_device_to_server_stream_id_received_ = 0; last_server_to_device_stream_id_received_ = 0; - // TODO(zea): expire all messages older than their TTL. + heartbeat_manager_.Stop(); // Add any pending acknowledgments to the list of ids. for (StreamIdToPersistentIdMap::const_iterator iter = @@ -229,7 +395,9 @@ void MCSClient::ResetStateAndBuildLoginRequest( acked_server_ids_.clear(); // Then build the request, consuming all pending acknowledgments. - request->Swap(BuildLoginRequest(android_id_, security_token_).get()); + request->Swap(BuildLoginRequest(android_id_, + security_token_, + version_string_).get()); for (PersistentIdList::const_iterator iter = restored_unackeds_server_ids_.begin(); iter != restored_unackeds_server_ids_.end(); ++iter) { @@ -245,74 +413,49 @@ void MCSClient::ResetStateAndBuildLoginRequest( to_send_.push_front(to_resend_.back()); to_resend_.pop_back(); } - DVLOG(1) << "Resetting state, with " << request->received_persistent_id_size() - << " incoming acks pending, and " << to_send_.size() - << " pending outgoing messages."; - - heartbeat_timer_.Stop(); - - state_ = CONNECTING; -} -void MCSClient::SendHeartbeat() { - SendMessage(MCSMessage(kHeartbeatPingTag, mcs_proto::HeartbeatPing()), - false); -} - -void MCSClient::OnRMQLoadFinished(const RMQStore::LoadResult& result) { - if (!result.success) { - state_ = UNINITIALIZED; - LOG(ERROR) << "Failed to load/create RMQ state. Not connecting."; - initialization_callback_.Run(false, 0, 0); - return; + // Drop all TTL == 0 or expired TTL messages from the queue. + std::deque<MCSPacketInternal> new_to_send; + std::vector<PersistentId> expired_ttl_ids; + while (!to_send_.empty()) { + MCSPacketInternal packet = PopMessageForSend(); + if (GetTTL(*packet->protobuf) > 0 && + !HasTTLExpired(*packet->protobuf, clock_)) { + new_to_send.push_back(packet); + } else { + // If the TTL was 0 there is no persistent id, so no need to remove the + // message from the persistent store. + if (!packet->persistent_id.empty()) + expired_ttl_ids.push_back(packet->persistent_id); + NotifyMessageSendStatus(*packet->protobuf, TTL_EXCEEDED); + } } - state_ = LOADED; - stream_id_out_ = 1; // Login request is hardcoded to id 1. - if (result.device_android_id == 0 || result.device_security_token == 0) { - DVLOG(1) << "No device credentials found, assuming new client."; - initialization_callback_.Run(true, 0, 0); - return; + if (!expired_ttl_ids.empty()) { + DVLOG(1) << "Connection reset, " << expired_ttl_ids.size() + << " messages expired."; + gcm_store_->RemoveOutgoingMessages( + expired_ttl_ids, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); } - android_id_ = result.device_android_id; - security_token_ = result.device_security_token; + to_send_.swap(new_to_send); - DVLOG(1) << "RMQ Load finished with " << result.incoming_messages.size() - << " incoming acks pending and " << result.outgoing_messages.size() - << " outgoing messages pending."; - - restored_unackeds_server_ids_ = result.incoming_messages; - - // First go through and order the outgoing messages by recency. - std::map<uint64, google::protobuf::MessageLite*> ordered_messages; - for (std::map<PersistentId, google::protobuf::MessageLite*>::const_iterator - iter = result.outgoing_messages.begin(); - iter != result.outgoing_messages.end(); ++iter) { - uint64 timestamp = 0; - if (!base::StringToUint64(iter->first, ×tamp)) { - LOG(ERROR) << "Invalid restored message."; - return; - } - ordered_messages[timestamp] = iter->second; - } + DVLOG(1) << "Resetting state, with " << request->received_persistent_id_size() + << " incoming acks pending, and " << to_send_.size() + << " pending outgoing messages."; - // Now go through and add the outgoing messages to the send queue in their - // appropriate order (oldest at front, most recent at back). - for (std::map<uint64, google::protobuf::MessageLite*>::const_iterator - iter = ordered_messages.begin(); - iter != ordered_messages.end(); ++iter) { - ReliablePacketInfo* packet_info = new ReliablePacketInfo(); - packet_info->protobuf.reset(iter->second); - packet_info->persistent_id = base::Uint64ToString(iter->first); - to_send_.push_back(make_linked_ptr(packet_info)); - } + state_ = CONNECTING; +} - initialization_callback_.Run(true, android_id_, security_token_); +void MCSClient::SendHeartbeat() { + SendMessage(MCSMessage(kHeartbeatPingTag, mcs_proto::HeartbeatPing())); } -void MCSClient::OnRMQUpdateFinished(bool success) { - LOG_IF(ERROR, !success) << "RMQ Update failed!"; +void MCSClient::OnGCMUpdateFinished(bool success) { + LOG_IF(ERROR, !success) << "GCM Update failed!"; + UMA_HISTOGRAM_BOOLEAN("GCM.StoreUpdateSucceeded", success); // TODO(zea): Rebuild the store from scratch in case of persistence failure? } @@ -320,25 +463,56 @@ void MCSClient::MaybeSendMessage() { if (to_send_.empty()) return; - if (!connection_handler_->CanSendMessage()) + // If the connection has been reset, do nothing. On reconnection + // MaybeSendMessage will be automatically invoked again. + // TODO(zea): consider doing TTL expiration at connection reset time, rather + // than reconnect time. + if (!connection_factory_->IsEndpointReachable()) return; - // TODO(zea): drop messages older than their TTL. - + MCSPacketInternal packet = PopMessageForSend(); + if (HasTTLExpired(*packet->protobuf, clock_)) { + DCHECK(!packet->persistent_id.empty()); + DVLOG(1) << "Dropping expired message " << packet->persistent_id << "."; + NotifyMessageSendStatus(*packet->protobuf, TTL_EXCEEDED); + gcm_store_->RemoveOutgoingMessage( + packet->persistent_id, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&MCSClient::MaybeSendMessage, + weak_ptr_factory_.GetWeakPtr())); + return; + } DVLOG(1) << "Pending output message found, sending."; - MCSPacketInternal packet = to_send_.front(); - to_send_.pop_front(); if (!packet->persistent_id.empty()) to_resend_.push_back(packet); SendPacketToWire(packet.get()); } void MCSClient::SendPacketToWire(ReliablePacketInfo* packet_info) { - // Reset the heartbeat interval. - heartbeat_timer_.Reset(); packet_info->stream_id = ++stream_id_out_; DVLOG(1) << "Sending packet of type " << packet_info->protobuf->GetTypeName(); + // Set the queued time as necessary. + if (packet_info->tag == kDataMessageStanzaTag) { + mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<mcs_proto::DataMessageStanza*>( + packet_info->protobuf.get()); + uint64 sent = data_message->sent(); + DCHECK_GT(sent, 0U); + int queued = (clock_->Now().ToInternalValue() / + base::Time::kMicrosecondsPerSecond) - sent; + DVLOG(1) << "Message was queued for " << queued << " seconds."; + data_message->set_queued(queued); + recorder_->RecordDataSentToWire( + data_message->category(), + data_message->to(), + data_message->id(), + queued); + } + // Set the proper last received stream id to acknowledge received server // packets. DVLOG(1) << "Setting last stream id received to " @@ -373,6 +547,9 @@ void MCSClient::HandleMCSDataMesssage( scoped_ptr<mcs_proto::DataMessageStanza> response( new mcs_proto::DataMessageStanza()); response->set_from(kGCMFromField); + response->set_sent(clock_->Now().ToInternalValue() / + base::Time::kMicrosecondsPerSecond); + response->set_ttl(0); bool send = false; for (int i = 0; i < data_message->app_data_size(); ++i) { const mcs_proto::AppData& app_data = data_message->app_data(i); @@ -390,8 +567,7 @@ void MCSClient::HandleMCSDataMesssage( if (send) { SendMessage( MCSMessage(kDataMessageStanzaTag, - response.PassAs<const google::protobuf::MessageLite>()), - false); + response.PassAs<const google::protobuf::MessageLite>())); } } @@ -430,9 +606,9 @@ void MCSClient::HandlePacketFromWire( ++stream_id_in_; if (!persistent_id.empty()) { unacked_server_ids_[stream_id_in_] = persistent_id; - rmq_store_.AddIncomingMessage(persistent_id, - base::Bind(&MCSClient::OnRMQUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); + gcm_store_->AddIncomingMessage(persistent_id, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); } DVLOG(1) << "Received message of type " << protobuf->GetTypeName() @@ -445,25 +621,36 @@ void MCSClient::HandlePacketFromWire( unacked_server_ids_.size() % kUnackedMessageBeforeStreamAck == 0) { SendMessage(MCSMessage(kIqStanzaTag, BuildStreamAck(). - PassAs<const google::protobuf::MessageLite>()), - false); + PassAs<const google::protobuf::MessageLite>())); } + // The connection is alive, treat this message as a heartbeat ack. + heartbeat_manager_.OnHeartbeatAcked(); + switch (tag) { case kLoginResponseTag: { + DCHECK_EQ(CONNECTING, state_); mcs_proto::LoginResponse* login_response = reinterpret_cast<mcs_proto::LoginResponse*>(protobuf.get()); DVLOG(1) << "Received login response:"; DVLOG(1) << " Id: " << login_response->id(); DVLOG(1) << " Timestamp: " << login_response->server_timestamp(); - if (login_response->has_error()) { + if (login_response->has_error() && login_response->error().code() != 0) { state_ = UNINITIALIZED; DVLOG(1) << " Error code: " << login_response->error().code(); DVLOG(1) << " Error message: " << login_response->error().message(); - initialization_callback_.Run(false, 0, 0); + LOG(ERROR) << "Failed to log in to GCM, resetting connection."; + connection_factory_->SignalConnectionReset( + ConnectionFactory::LOGIN_FAILURE); + mcs_error_callback_.Run(); return; } + if (login_response->has_heartbeat_config()) { + heartbeat_manager_.UpdateHeartbeatConfig( + login_response->heartbeat_config()); + } + state_ = CONNECTED; stream_id_in_ = 1; // To account for the login response. DCHECK_EQ(1U, stream_id_out_); @@ -484,29 +671,29 @@ void MCSClient::HandlePacketFromWire( weak_ptr_factory_.GetWeakPtr())); } - heartbeat_timer_.Start(FROM_HERE, - heartbeat_interval_, - base::Bind(&MCSClient::SendHeartbeat, - weak_ptr_factory_.GetWeakPtr())); + heartbeat_manager_.Start( + base::Bind(&MCSClient::SendHeartbeat, + weak_ptr_factory_.GetWeakPtr()), + base::Bind(&MCSClient::OnConnectionResetByHeartbeat, + weak_ptr_factory_.GetWeakPtr())); return; } case kHeartbeatPingTag: DCHECK_GE(stream_id_in_, 1U); DVLOG(1) << "Received heartbeat ping, sending ack."; SendMessage( - MCSMessage(kHeartbeatAckTag, mcs_proto::HeartbeatAck()), false); + MCSMessage(kHeartbeatAckTag, mcs_proto::HeartbeatAck())); return; case kHeartbeatAckTag: DCHECK_GE(stream_id_in_, 1U); DVLOG(1) << "Received heartbeat ack."; - // TODO(zea): add logic to reconnect if no ack received within a certain - // timeout (with backoff). + // Do nothing else, all messages act as heartbeat acks. return; case kCloseTag: - LOG(ERROR) << "Received close command, closing connection."; - state_ = UNINITIALIZED; - initialization_callback_.Run(false, 0, 0); - // TODO(zea): should this happen in non-error cases? Reconnect? + LOG(ERROR) << "Received close command, resetting connection."; + state_ = LOADED; + connection_factory_->SignalConnectionReset( + ConnectionFactory::CLOSE_COMMAND); return; case kIqStanzaTag: { DCHECK_GE(stream_id_in_, 1U); @@ -565,58 +752,85 @@ void MCSClient::HandleStreamAck(StreamId last_stream_id_received) { const MCSPacketInternal& outgoing_packet = to_resend_.front(); acked_outgoing_persistent_ids.push_back(outgoing_packet->persistent_id); acked_outgoing_stream_ids.push_back(outgoing_packet->stream_id); + NotifyMessageSendStatus(*outgoing_packet->protobuf, SENT); to_resend_.pop_front(); } DVLOG(1) << "Server acked " << acked_outgoing_persistent_ids.size() << " outgoing messages, " << to_resend_.size() << " remaining unacked"; - rmq_store_.RemoveOutgoingMessages(acked_outgoing_persistent_ids, - base::Bind(&MCSClient::OnRMQUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); + gcm_store_->RemoveOutgoingMessages( + acked_outgoing_persistent_ids, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); HandleServerConfirmedReceipt(last_stream_id_received); } void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) { - // First check the to_resend_ queue. Acknowledgments should always happen - // in the order they were sent, so if messages are present they should match - // the acknowledge list. - PersistentIdList::const_iterator iter = id_list.begin(); - for (; iter != id_list.end() && !to_resend_.empty(); ++iter) { + std::set<PersistentId> remaining_ids(id_list.begin(), id_list.end()); + + StreamId last_stream_id_received = -1; + + // First check the to_resend_ queue. Acknowledgments are always contiguous, + // so if there's a pending message that hasn't been acked, all newer messages + // must also be unacked. + while(!to_resend_.empty() && !remaining_ids.empty()) { const MCSPacketInternal& outgoing_packet = to_resend_.front(); - DCHECK_EQ(outgoing_packet->persistent_id, *iter); + if (remaining_ids.count(outgoing_packet->persistent_id) == 0) + break; // Newer message must be unacked too. + remaining_ids.erase(outgoing_packet->persistent_id); + NotifyMessageSendStatus(*outgoing_packet->protobuf, SENT); // No need to re-acknowledge any server messages this message already // acknowledged. StreamId device_stream_id = outgoing_packet->stream_id; - HandleServerConfirmedReceipt(device_stream_id); - + if (device_stream_id > last_stream_id_received) + last_stream_id_received = device_stream_id; to_resend_.pop_front(); } // If the acknowledged ids aren't all there, they might be in the to_send_ - // queue (typically when a StreamAck confirms messages as part of a login + // queue (typically when a SelectiveAck confirms messages as part of a login // response). - for (; iter != id_list.end() && !to_send_.empty(); ++iter) { + while (!to_send_.empty() && !remaining_ids.empty()) { const MCSPacketInternal& outgoing_packet = to_send_.front(); - DCHECK_EQ(outgoing_packet->persistent_id, *iter); + if (remaining_ids.count(outgoing_packet->persistent_id) == 0) + break; // Newer messages must be unacked too. + remaining_ids.erase(outgoing_packet->persistent_id); + NotifyMessageSendStatus(*outgoing_packet->protobuf, SENT); // No need to re-acknowledge any server messages this message already // acknowledged. StreamId device_stream_id = outgoing_packet->stream_id; - HandleServerConfirmedReceipt(device_stream_id); - - to_send_.pop_front(); + if (device_stream_id > last_stream_id_received) + last_stream_id_received = device_stream_id; + PopMessageForSend(); } - DCHECK(iter == id_list.end()); + // Only handle the largest stream id value. All other stream ids are + // implicitly handled. + if (last_stream_id_received > 0) + HandleServerConfirmedReceipt(last_stream_id_received); + + // At this point, all remaining acked ids are redundant. + PersistentIdList acked_ids; + if (remaining_ids.size() > 0) { + for (size_t i = 0; i < id_list.size(); ++i) { + if (remaining_ids.count(id_list[i]) > 0) + continue; + acked_ids.push_back(id_list[i]); + } + } else { + acked_ids = id_list; + } - DVLOG(1) << "Server acked " << id_list.size() + DVLOG(1) << "Server acked " << acked_ids.size() << " messages, " << to_resend_.size() << " remaining unacked."; - rmq_store_.RemoveOutgoingMessages(id_list, - base::Bind(&MCSClient::OnRMQUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); + gcm_store_->RemoveOutgoingMessages( + acked_ids, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); // Resend any remaining outgoing messages, as they were not received by the // server. @@ -628,12 +842,6 @@ void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) { } void MCSClient::HandleServerConfirmedReceipt(StreamId device_stream_id) { - // TODO(zea): use a message id the sender understands. - base::MessageLoop::current()->PostTask( - FROM_HERE, - base::Bind(message_sent_callback_, - "Message " + base::UintToString(device_stream_id) + " sent.")); - PersistentIdList acked_incoming_ids; for (std::map<StreamId, PersistentIdList>::iterator iter = acked_server_ids_.begin(); @@ -647,13 +855,56 @@ void MCSClient::HandleServerConfirmedReceipt(StreamId device_stream_id) { DVLOG(1) << "Server confirmed receipt of " << acked_incoming_ids.size() << " acknowledged server messages."; - rmq_store_.RemoveIncomingMessages(acked_incoming_ids, - base::Bind(&MCSClient::OnRMQUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); + gcm_store_->RemoveIncomingMessages( + acked_incoming_ids, + base::Bind(&MCSClient::OnGCMUpdateFinished, + weak_ptr_factory_.GetWeakPtr())); } MCSClient::PersistentId MCSClient::GetNextPersistentId() { return base::Uint64ToString(base::TimeTicks::Now().ToInternalValue()); } +void MCSClient::OnConnectionResetByHeartbeat() { + connection_factory_->SignalConnectionReset( + ConnectionFactory::HEARTBEAT_FAILURE); +} + +void MCSClient::NotifyMessageSendStatus( + const google::protobuf::MessageLite& protobuf, + MessageSendStatus status) { + if (GetMCSProtoTag(protobuf) != kDataMessageStanzaTag) + return; + + const mcs_proto::DataMessageStanza* data_message_stanza = + reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf); + recorder_->RecordNotifySendStatus( + data_message_stanza->category(), + data_message_stanza->to(), + data_message_stanza->id(), + status, + protobuf.ByteSize(), + data_message_stanza->ttl()); + message_sent_callback_.Run( + data_message_stanza->device_user_id(), + data_message_stanza->category(), + data_message_stanza->id(), + status); +} + +MCSClient::MCSPacketInternal MCSClient::PopMessageForSend() { + MCSPacketInternal packet = to_send_.front(); + to_send_.pop_front(); + + if (packet->tag == kDataMessageStanzaTag) { + mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<mcs_proto::DataMessageStanza*>(packet->protobuf.get()); + CollapseKey collapse_key(*data_message); + if (collapse_key.IsValid()) + collapse_key_map_.erase(collapse_key); + } + + return packet; +} + } // namespace gcm diff --git a/chromium/google_apis/gcm/engine/mcs_client.h b/chromium/google_apis/gcm/engine/mcs_client.h index 4de62cb127e..cc915e49087 100644 --- a/chromium/google_apis/gcm/engine/mcs_client.h +++ b/chromium/google_apis/gcm/engine/mcs_client.h @@ -13,11 +13,15 @@ #include "base/files/file_path.h" #include "base/memory/linked_ptr.h" #include "base/memory/weak_ptr.h" -#include "base/timer/timer.h" #include "google_apis/gcm/base/gcm_export.h" #include "google_apis/gcm/base/mcs_message.h" #include "google_apis/gcm/engine/connection_handler.h" -#include "google_apis/gcm/engine/rmq_store.h" +#include "google_apis/gcm/engine/gcm_store.h" +#include "google_apis/gcm/engine/heartbeat_manager.h" + +namespace base { +class Clock; +} // namespace base namespace google { namespace protobuf { @@ -31,7 +35,9 @@ class LoginRequest; namespace gcm { +class CollapseKey; class ConnectionFactory; +class GCMStatsRecorder; struct ReliablePacketInfo; // An MCS client. This client is in charge of all communications with an @@ -40,48 +46,74 @@ struct ReliablePacketInfo; // network requests are performed on. class GCM_EXPORT MCSClient { public: + // Any change made to this enum should have corresponding change in the + // GetStateString(...) function. enum State { - UNINITIALIZED, // Uninitialized. - LOADING, // Waiting for RMQ load to finish. - LOADED, // RMQ Load finished, waiting to connect. - CONNECTING, // Connection in progress. - CONNECTED, // Connected and running. + UNINITIALIZED, // Uninitialized. + LOADED, // GCM Load finished, waiting to connect. + CONNECTING, // Connection in progress. + CONNECTED, // Connected and running. }; - // Callback for informing MCSClient status. It is valid for this to be - // invoked more than once if a permanent error is encountered after a - // successful login was initiated. - typedef base::Callback< - void(bool success, - uint64 restored_android_id, - uint64 restored_security_token)> InitializationCompleteCallback; + // Any change made to this enum should have corresponding change in the + // GetMessageSendStatusString(...) function in mcs_client.cc. + enum MessageSendStatus { + // Message was queued succcessfully. + QUEUED, + // Message was sent to the server and the ACK was received. + SENT, + // Message not saved, because total queue size limit reached. + QUEUE_SIZE_LIMIT_REACHED, + // Message not saved, because app queue size limit reached. + APP_QUEUE_SIZE_LIMIT_REACHED, + // Message too large to send. + MESSAGE_TOO_LARGE, + // Message not send becuase of TTL = 0 and no working connection. + NO_CONNECTION_ON_ZERO_TTL, + // Message exceeded TTL. + TTL_EXCEEDED, + + // NOTE: always keep this entry at the end. Add new status types only + // immediately above this line. Make sure to update the corresponding + // histogram enum accordingly. + SEND_STATUS_COUNT + }; + + // Callback for MCSClient's error conditions. + // TODO(fgorski): Keeping it as a callback with intention to add meaningful + // error information. + typedef base::Callback<void()> ErrorCallback; // Callback when a message is received. typedef base::Callback<void(const MCSMessage& message)> OnMessageReceivedCallback; // Callback when a message is sent (and receipt has been acknowledged by // the MCS endpoint). - // TODO(zea): pass some sort of structure containing more details about - // send failures. - typedef base::Callback<void(const std::string& message_id)> - OnMessageSentCallback; + typedef base::Callback< + void(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MessageSendStatus status)> OnMessageSentCallback; - MCSClient(const base::FilePath& rmq_path, + MCSClient(const std::string& version_string, + base::Clock* clock, ConnectionFactory* connection_factory, - scoped_refptr<base::SequencedTaskRunner> blocking_task_runner); + GCMStore* gcm_store, + GCMStatsRecorder* recorder); virtual ~MCSClient(); // Initialize the client. Will load any previous id/token information as well - // as unacknowledged message information from the RMQ storage, if it exists, + // as unacknowledged message information from the GCM storage, if it exists, // passing the id/token information back via |initialization_callback| along - // with a |success == true| result. If no RMQ information is present (and - // this is therefore a fresh client), a clean RMQ store will be created and + // with a |success == true| result. If no GCM information is present (and + // this is therefore a fresh client), a clean GCM store will be created and // values of 0 will be returned via |initialization_callback| with // |success == true|. - /// If an error loading the RMQ store is encountered, + /// If an error loading the GCM store is encountered, // |initialization_callback| will be invoked with |success == false|. - void Initialize(const InitializationCompleteCallback& initialization_callback, + void Initialize(const ErrorCallback& initialization_callback, const OnMessageReceivedCallback& message_received_callback, - const OnMessageSentCallback& message_sent_callback); + const OnMessageSentCallback& message_sent_callback, + scoped_ptr<GCMStore::LoadResult> load_result); // Logs the client into the server. Client must be initialized. // |android_id| and |security_token| are optional if this is not a new @@ -90,21 +122,29 @@ class GCM_EXPORT MCSClient { // with a valid LoginResponse. // Login failure (typically invalid id/token) will shut down the client, and // |initialization_callback| to be invoked with |success = false|. - void Login(uint64 android_id, uint64 security_token); + virtual void Login(uint64 android_id, uint64 security_token); // Sends a message, with or without reliable message queueing (RMQ) support. // Will asynchronously invoke the OnMessageSent callback regardless. - // TODO(zea): support TTL. - void SendMessage(const MCSMessage& message, bool use_rmq); - - // Disconnects the client and permanently destroys the persistent RMQ store. - // WARNING: This is permanent, and the client must be recreated with new - // credentials afterwards. - void Destroy(); + // Whether to use RMQ depends on whether the protobuf has |ttl| set or not. + // |ttl == 0| denotes the message should only be sent if the connection is + // open. |ttl > 0| will keep the message saved for |ttl| seconds, after which + // it will be dropped if it was unable to be sent. When a message is dropped, + // |message_sent_callback_| is invoked with a TTL expiration error. + virtual void SendMessage(const MCSMessage& message); // Returns the current state of the client. State state() const { return state_; } + // Returns the size of the send message queue. + int GetSendQueueSize() const; + + // Returns the size of the resend messaage queue. + int GetResendQueueSize() const; + + // Returns text representation of the state enum. + std::string GetStateString() const; + private: typedef uint32 StreamId; typedef std::string PersistentId; @@ -122,9 +162,8 @@ class GCM_EXPORT MCSClient { // Send a heartbeat to the MCS server. void SendHeartbeat(); - // RMQ Store callbacks. - void OnRMQLoadFinished(const RMQStore::LoadResult& result); - void OnRMQUpdateFinished(bool success); + // GCM Store callback. + void OnGCMUpdateFinished(bool success); // Attempt to send a message. void MaybeSendMessage(); @@ -155,11 +194,28 @@ class GCM_EXPORT MCSClient { // Virtual for testing. virtual PersistentId GetNextPersistentId(); + // Helper for the heartbeat manager to signal a connection reset. + void OnConnectionResetByHeartbeat(); + + // Runs the message_sent_callback_ with send |status| of the |protobuf|. + void NotifyMessageSendStatus(const google::protobuf::MessageLite& protobuf, + MessageSendStatus status); + + // Pops the next message from the front of the send queue (cleaning up + // any associated state). + MCSPacketInternal PopMessageForSend(); + + // Local version string. Sent on login. + const std::string version_string_; + + // Clock for enforcing TTL. Passed in for testing. + base::Clock* const clock_; + // Client state. State state_; // Callbacks for owner. - InitializationCompleteCallback initialization_callback_; + ErrorCallback mcs_error_callback_; OnMessageReceivedCallback message_received_callback_; OnMessageSentCallback message_sent_callback_; @@ -182,6 +238,9 @@ class GCM_EXPORT MCSClient { std::deque<MCSPacketInternal> to_send_; std::deque<MCSPacketInternal> to_resend_; + // Map of collapse keys to their pending messages. + std::map<CollapseKey, ReliablePacketInfo*> collapse_key_map_; + // Last device_to_server stream id acknowledged by the server. StreamId last_device_to_server_stream_id_received_; // Last server_to_device stream id acknowledged by this device. @@ -209,17 +268,14 @@ class GCM_EXPORT MCSClient { // acknowledged on the next login attempt. PersistentIdList restored_unackeds_server_ids_; - // The reliable message queue persistent store. - RMQStore rmq_store_; + // The GCM persistent store. Not owned. + GCMStore* gcm_store_; - // ----- Heartbeats ----- - // The current heartbeat interval. - base::TimeDelta heartbeat_interval_; - // Timer for triggering heartbeats. - base::Timer heartbeat_timer_; + // Manager to handle triggering/detecting heartbeats. + HeartbeatManager heartbeat_manager_; - // The task runner for blocking tasks (i.e. persisting RMQ state to disk). - scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + // Recorder that records GCM activities for debugging purpose. Not owned. + GCMStatsRecorder* recorder_; base::WeakPtrFactory<MCSClient> weak_ptr_factory_; diff --git a/chromium/google_apis/gcm/engine/mcs_client_unittest.cc b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc index 6ef140586e9..915b25d18db 100644 --- a/chromium/google_apis/gcm/engine/mcs_client_unittest.cc +++ b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc @@ -4,14 +4,18 @@ #include "google_apis/gcm/engine/mcs_client.h" +#include "base/command_line.h" #include "base/files/scoped_temp_dir.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" #include "base/strings/string_number_conversions.h" -#include "components/webdata/encryptor/encryptor.h" +#include "base/test/simple_test_clock.h" +#include "google_apis/gcm/base/fake_encryptor.h" #include "google_apis/gcm/base/mcs_util.h" #include "google_apis/gcm/engine/fake_connection_factory.h" #include "google_apis/gcm/engine/fake_connection_handler.h" +#include "google_apis/gcm/engine/gcm_store_impl.h" +#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h" #include "testing/gtest/include/gtest/gtest.h" namespace gcm { @@ -30,27 +34,43 @@ const int kMessageBatchSize = 6; // TODO(zea): get this (and other constants) directly from the mcs client. const int kAckLimitSize = 10; +// TTL value for reliable messages. +const int kTTLValue = 5 * 60; // 5 minutes. + // Helper for building arbitrary data messages. MCSMessage BuildDataMessage(const std::string& from, const std::string& category, + const std::string& message_id, int last_stream_id_received, - const std::string persistent_id) { + const std::string& persistent_id, + int ttl, + uint64 sent, + int queued, + const std::string& token, + const uint64& user_id) { mcs_proto::DataMessageStanza data_message; + data_message.set_id(message_id); data_message.set_from(from); data_message.set_category(category); data_message.set_last_stream_id_received(last_stream_id_received); if (!persistent_id.empty()) data_message.set_persistent_id(persistent_id); + data_message.set_ttl(ttl); + data_message.set_sent(sent); + data_message.set_queued(queued); + data_message.set_token(token); + data_message.set_device_user_id(user_id); return MCSMessage(kDataMessageStanzaTag, data_message); } // MCSClient with overriden exposed persistent id logic. class TestMCSClient : public MCSClient { public: - TestMCSClient(const base::FilePath& rmq_path, + TestMCSClient(base::Clock* clock, ConnectionFactory* connection_factory, - scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) - : MCSClient(rmq_path, connection_factory, blocking_task_runner), + GCMStore* gcm_store, + gcm::GCMStatsRecorder* recorder) + : MCSClient("", clock, connection_factory, gcm_store, recorder), next_id_(0) { } @@ -67,10 +87,14 @@ class MCSClientTest : public testing::Test { MCSClientTest(); virtual ~MCSClientTest(); + virtual void SetUp() OVERRIDE; + void BuildMCSClient(); void InitializeClient(); + void StoreCredentials(); void LoginClient(const std::vector<std::string>& acknowledged_ids); + base::SimpleTestClock* clock() { return &clock_; } TestMCSClient* mcs_client() const { return mcs_client_.get(); } FakeConnectionFactory* connection_factory() { return &connection_factory_; @@ -80,6 +104,11 @@ class MCSClientTest : public testing::Test { uint64 restored_security_token() const { return restored_security_token_; } MCSMessage* received_message() const { return received_message_.get(); } std::string sent_message_id() const { return sent_message_id_;} + MCSClient::MessageSendStatus message_send_status() const { + return message_send_status_; + } + + void SetDeviceCredentialsCallback(bool success); FakeConnectionHandler* GetFakeHandler() const; @@ -87,15 +116,19 @@ class MCSClientTest : public testing::Test { void PumpLoop(); private: - void InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token); + void ErrorCallback(); void MessageReceivedCallback(const MCSMessage& message); - void MessageSentCallback(const std::string& message_id); + void MessageSentCallback(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status); + + base::SimpleTestClock clock_; base::ScopedTempDir temp_directory_; base::MessageLoop message_loop_; scoped_ptr<base::RunLoop> run_loop_; + scoped_ptr<GCMStore> gcm_store_; FakeConnectionFactory connection_factory_; scoped_ptr<TestMCSClient> mcs_client_; @@ -104,46 +137,58 @@ class MCSClientTest : public testing::Test { uint64 restored_security_token_; scoped_ptr<MCSMessage> received_message_; std::string sent_message_id_; + MCSClient::MessageSendStatus message_send_status_; + + gcm::FakeGCMStatsRecorder recorder_; }; MCSClientTest::MCSClientTest() : run_loop_(new base::RunLoop()), - init_success_(false), + init_success_(true), restored_android_id_(0), - restored_security_token_(0) { + restored_security_token_(0), + message_send_status_(MCSClient::SENT) { EXPECT_TRUE(temp_directory_.CreateUniqueTempDir()); run_loop_.reset(new base::RunLoop()); - // On OSX, prevent the Keychain permissions popup during unit tests. -#if defined(OS_MACOSX) - Encryptor::UseMockKeychain(true); -#endif + // Advance the clock to a non-zero time. + clock_.Advance(base::TimeDelta::FromSeconds(1)); } MCSClientTest::~MCSClientTest() {} +void MCSClientTest::SetUp() { + testing::Test::SetUp(); +} + void MCSClientTest::BuildMCSClient() { - mcs_client_.reset( - new TestMCSClient(temp_directory_.path(), - &connection_factory_, - message_loop_.message_loop_proxy())); + gcm_store_.reset(new GCMStoreImpl( + temp_directory_.path(), + message_loop_.message_loop_proxy(), + make_scoped_ptr<Encryptor>(new FakeEncryptor))); + mcs_client_.reset(new TestMCSClient(&clock_, + &connection_factory_, + gcm_store_.get(), + &recorder_)); } void MCSClientTest::InitializeClient() { - mcs_client_->Initialize(base::Bind(&MCSClientTest::InitializationCallback, - base::Unretained(this)), - base::Bind(&MCSClientTest::MessageReceivedCallback, - base::Unretained(this)), - base::Bind(&MCSClientTest::MessageSentCallback, - base::Unretained(this))); - run_loop_->Run(); + gcm_store_->Load(base::Bind( + &MCSClient::Initialize, + base::Unretained(mcs_client_.get()), + base::Bind(&MCSClientTest::ErrorCallback, + base::Unretained(this)), + base::Bind(&MCSClientTest::MessageReceivedCallback, + base::Unretained(this)), + base::Bind(&MCSClientTest::MessageSentCallback, base::Unretained(this)))); + run_loop_->RunUntilIdle(); run_loop_.reset(new base::RunLoop()); } void MCSClientTest::LoginClient( const std::vector<std::string>& acknowledged_ids) { scoped_ptr<mcs_proto::LoginRequest> login_request = - BuildLoginRequest(kAndroidId, kSecurityToken); + BuildLoginRequest(kAndroidId, kSecurityToken, ""); for (size_t i = 0; i < acknowledged_ids.size(); ++i) login_request->add_received_persistent_id(acknowledged_ids[i]); GetFakeHandler()->ExpectOutgoingMessage( @@ -154,6 +199,15 @@ void MCSClientTest::LoginClient( run_loop_.reset(new base::RunLoop()); } +void MCSClientTest::StoreCredentials() { + gcm_store_->SetDeviceCredentials( + kAndroidId, kSecurityToken, + base::Bind(&MCSClientTest::SetDeviceCredentialsCallback, + base::Unretained(this))); + run_loop_->Run(); + run_loop_.reset(new base::RunLoop()); +} + FakeConnectionHandler* MCSClientTest::GetFakeHandler() const { return reinterpret_cast<FakeConnectionHandler*>( connection_factory_.GetConnectionHandler()); @@ -169,13 +223,9 @@ void MCSClientTest::PumpLoop() { run_loop_.reset(new base::RunLoop()); } -void MCSClientTest::InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token) { - init_success_ = success; - restored_android_id_ = restored_android_id; - restored_security_token_ = restored_security_token; - DVLOG(1) << "Initialization callback invoked, killing loop."; +void MCSClientTest::ErrorCallback() { + init_success_ = false; + DVLOG(1) << "Error callback invoked, killing loop."; run_loop_->Quit(); } @@ -185,8 +235,18 @@ void MCSClientTest::MessageReceivedCallback(const MCSMessage& message) { run_loop_->Quit(); } -void MCSClientTest::MessageSentCallback(const std::string& message_id) { +void MCSClientTest::MessageSentCallback(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status) { DVLOG(1) << "Message sent callback invoked, killing loop."; + sent_message_id_ = message_id; + message_send_status_ = status; + run_loop_->Quit(); +} + +void MCSClientTest::SetDeviceCredentialsCallback(bool success) { + ASSERT_TRUE(success); run_loop_->Quit(); } @@ -194,8 +254,6 @@ void MCSClientTest::MessageSentCallback(const std::string& message_id) { TEST_F(MCSClientTest, InitializeNew) { BuildMCSClient(); InitializeClient(); - EXPECT_EQ(0U, restored_android_id()); - EXPECT_EQ(0U, restored_security_token()); EXPECT_TRUE(init_success()); } @@ -206,11 +264,10 @@ TEST_F(MCSClientTest, InitializeExisting) { InitializeClient(); LoginClient(std::vector<std::string>()); - // Rebuild the client, to reload from the RMQ. + // Rebuild the client, to reload from the GCM store. + StoreCredentials(); BuildMCSClient(); InitializeClient(); - EXPECT_EQ(kAndroidId, restored_android_id()); - EXPECT_EQ(kSecurityToken, restored_security_token()); EXPECT_TRUE(init_success()); } @@ -225,15 +282,18 @@ TEST_F(MCSClientTest, LoginSuccess) { EXPECT_EQ(kLoginResponseTag, received_message()->tag()); } -// Encounter a server error during the login attempt. +// Encounter a server error during the login attempt. Should trigger a +// reconnect. TEST_F(MCSClientTest, FailLogin) { BuildMCSClient(); InitializeClient(); GetFakeHandler()->set_fail_login(true); + connection_factory()->set_delay_reconnect(true); LoginClient(std::vector<std::string>()); EXPECT_FALSE(connection_factory()->IsEndpointReachable()); EXPECT_FALSE(init_success()); EXPECT_FALSE(received_message()); + EXPECT_TRUE(connection_factory()->reconnect_pending()); } // Send a message without RMQ support. @@ -241,11 +301,29 @@ TEST_F(MCSClientTest, SendMessageNoRMQ) { BuildMCSClient(); InitializeClient(); LoginClient(std::vector<std::string>()); - MCSMessage message(BuildDataMessage("from", "category", 1, "")); + MCSMessage message( + BuildDataMessage("from", "category", "X", 1, "", 0, 1, 0, "", 0)); GetFakeHandler()->ExpectOutgoingMessage(message); - mcs_client()->SendMessage(message, false); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + mcs_client()->SendMessage(message); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); +} + +// Send a message without RMQ support while disconnected. Message send should +// fail immediately, invoking callback. +TEST_F(MCSClientTest, SendMessageNoRMQWhileDisconnected) { + BuildMCSClient(); + InitializeClient(); + + EXPECT_TRUE(sent_message_id().empty()); + MCSMessage message( + BuildDataMessage("from", "category", "X", 1, "", 0, 1, 0, "", 0)); + mcs_client()->SendMessage(message); + + // Message sent callback should be invoked, but no message should actually + // be sent. + EXPECT_EQ("X", sent_message_id()); + EXPECT_EQ(MCSClient::NO_CONNECTION_ON_ZERO_TTL, message_send_status()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } // Send a message with RMQ support. @@ -253,11 +331,11 @@ TEST_F(MCSClientTest, SendMessageRMQ) { BuildMCSClient(); InitializeClient(); LoginClient(std::vector<std::string>()); - MCSMessage message(BuildDataMessage("from", "category", 1, "1")); + MCSMessage message(BuildDataMessage( + "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0)); GetFakeHandler()->ExpectOutgoingMessage(message); - mcs_client()->SendMessage(message, true); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + mcs_client()->SendMessage(message); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } // Send a message with RMQ support while disconnected. On reconnect, the message @@ -267,26 +345,31 @@ TEST_F(MCSClientTest, SendMessageRMQWhileDisconnected) { InitializeClient(); LoginClient(std::vector<std::string>()); GetFakeHandler()->set_fail_send(true); - MCSMessage message(BuildDataMessage("from", "category", 1, "1")); + MCSMessage message(BuildDataMessage( + "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0)); // The initial (failed) send. GetFakeHandler()->ExpectOutgoingMessage(message); // The login request. GetFakeHandler()->ExpectOutgoingMessage( - MCSMessage(kLoginRequestTag, - BuildLoginRequest(kAndroidId, kSecurityToken). - PassAs<const google::protobuf::MessageLite>())); + MCSMessage( + kLoginRequestTag, + BuildLoginRequest(kAndroidId, kSecurityToken, ""). + PassAs<const google::protobuf::MessageLite>())); // The second (re)send. - GetFakeHandler()->ExpectOutgoingMessage(message); - mcs_client()->SendMessage(message, true); - EXPECT_FALSE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + MCSMessage message2(BuildDataMessage( + "from", "category", "X", 1, "1", kTTLValue, 1, kTTLValue - 1, "", 0)); + GetFakeHandler()->ExpectOutgoingMessage(message2); + mcs_client()->SendMessage(message); + PumpLoop(); // Wait for the queuing to happen. + EXPECT_EQ(MCSClient::QUEUED, message_send_status()); + EXPECT_FALSE(GetFakeHandler()->AllOutgoingMessagesReceived()); GetFakeHandler()->set_fail_send(false); + clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue - 1)); connection_factory()->Connect(); WaitForMCSEvent(); // Wait for the login to finish. PumpLoop(); // Wait for the send to happen. - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } // Send a message with RMQ support without receiving an acknowledgement. On @@ -296,23 +379,28 @@ TEST_F(MCSClientTest, SendMessageRMQOnRestart) { InitializeClient(); LoginClient(std::vector<std::string>()); GetFakeHandler()->set_fail_send(true); - MCSMessage message(BuildDataMessage("from", "category", 1, "1")); + MCSMessage message(BuildDataMessage( + "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0)); // The initial (failed) send. GetFakeHandler()->ExpectOutgoingMessage(message); GetFakeHandler()->set_fail_send(false); - mcs_client()->SendMessage(message, true); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + mcs_client()->SendMessage(message); + PumpLoop(); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Rebuild the client, which should resend the old message. + StoreCredentials(); BuildMCSClient(); InitializeClient(); + + clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue - 1)); + MCSMessage message2(BuildDataMessage( + "from", "category", "X", 1, "1", kTTLValue, 1, kTTLValue - 1, "", 0)); LoginClient(std::vector<std::string>()); - GetFakeHandler()->ExpectOutgoingMessage(message); + GetFakeHandler()->ExpectOutgoingMessage(message2); PumpLoop(); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } // Send messages with RMQ support, followed by receiving a stream ack. On @@ -324,13 +412,21 @@ TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) { // Send some messages. for (int i = 1; i <= kMessageBatchSize; ++i) { - MCSMessage message( - BuildDataMessage("from", "category", 1, base::IntToString(i))); + MCSMessage message(BuildDataMessage("from", + "category", + "X", + 1, + base::IntToString(i), + kTTLValue, + 1, + 0, + "", + 0)); GetFakeHandler()->ExpectOutgoingMessage(message); - mcs_client()->SendMessage(message, true); + mcs_client()->SendMessage(message); + PumpLoop(); } - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Receive the ack. scoped_ptr<mcs_proto::IqStanza> ack = BuildStreamAck(); @@ -341,6 +437,7 @@ TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) { WaitForMCSEvent(); // Reconnect and ensure no messages are resent. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector<std::string>()); @@ -358,16 +455,25 @@ TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) { std::vector<std::string> id_list; for (int i = 1; i <= kMessageBatchSize; ++i) { id_list.push_back(base::IntToString(i)); - MCSMessage message( - BuildDataMessage("from", "category", 1, id_list.back())); + MCSMessage message(BuildDataMessage("from", + "category", + id_list.back(), + 1, + id_list.back(), + kTTLValue, + 1, + 0, + "", + 0)); GetFakeHandler()->ExpectOutgoingMessage(message); - mcs_client()->SendMessage(message, true); + mcs_client()->SendMessage(message); + PumpLoop(); } - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Rebuild the client, and receive an acknowledgment for the messages as // part of the login response. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector<std::string>()); @@ -375,9 +481,7 @@ TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) { GetFakeHandler()->ReceiveMessage( MCSMessage(kIqStanzaTag, ack.PassAs<const google::protobuf::MessageLite>())); - WaitForMCSEvent(); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } // Send messages with RMQ support. On restart, receive a SelectiveAck with @@ -392,16 +496,25 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { std::vector<std::string> id_list; for (int i = 1; i <= kMessageBatchSize; ++i) { id_list.push_back(base::IntToString(i)); - MCSMessage message( - BuildDataMessage("from", "category", 1, id_list.back())); + MCSMessage message(BuildDataMessage("from", + "category", + id_list.back(), + 1, + id_list.back(), + kTTLValue, + 1, + 0, + "", + 0)); GetFakeHandler()->ExpectOutgoingMessage(message); - mcs_client()->SendMessage(message, true); + mcs_client()->SendMessage(message); + PumpLoop(); } - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Rebuild the client, and receive an acknowledgment for the messages as // part of the login response. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector<std::string>()); @@ -414,11 +527,16 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { id_list.begin() + kMessageBatchSize / 2, id_list.end()); for (int i = 1; i <= kMessageBatchSize / 2; ++i) { - MCSMessage message( - BuildDataMessage("from", - "category", - 2, - remaining_ids[i - 1])); + MCSMessage message(BuildDataMessage("from", + "category", + remaining_ids[i - 1], + 2, + remaining_ids[i - 1], + kTTLValue, + 1, + 0, + "", + 0)); GetFakeHandler()->ExpectOutgoingMessage(message); } scoped_ptr<mcs_proto::IqStanza> ack(BuildSelectiveAck(acked_ids)); @@ -426,8 +544,8 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { MCSMessage(kIqStanzaTag, ack.PassAs<const google::protobuf::MessageLite>())); WaitForMCSEvent(); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + PumpLoop(); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } // Receive some messages. On restart, the login request should contain the @@ -441,14 +559,15 @@ TEST_F(MCSClientTest, AckOnLogin) { std::vector<std::string> id_list; for (int i = 1; i <= kMessageBatchSize; ++i) { id_list.push_back(base::IntToString(i)); - MCSMessage message( - BuildDataMessage("from", "category", i, id_list.back())); + MCSMessage message(BuildDataMessage( + "from", "category", "X", 1, id_list.back(), kTTLValue, 1, 0, "", 0)); GetFakeHandler()->ReceiveMessage(message); WaitForMCSEvent(); PumpLoop(); } // Restart the client. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(id_list); @@ -465,20 +584,34 @@ TEST_F(MCSClientTest, AckOnSend) { std::vector<std::string> id_list; for (int i = 1; i <= kMessageBatchSize; ++i) { id_list.push_back(base::IntToString(i)); - MCSMessage message( - BuildDataMessage("from", "category", i, id_list.back())); + MCSMessage message(BuildDataMessage("from", + "category", + id_list.back(), + 1, + id_list.back(), + kTTLValue, + 1, + 0, + "", + 0)); GetFakeHandler()->ReceiveMessage(message); - WaitForMCSEvent(); PumpLoop(); } // Trigger a message send, which should acknowledge via stream ack. - MCSMessage message( - BuildDataMessage("from", "category", kMessageBatchSize + 1, "1")); + MCSMessage message(BuildDataMessage("from", + "category", + "X", + kMessageBatchSize + 1, + "1", + kTTLValue, + 1, + 0, + "", + 0)); GetFakeHandler()->ExpectOutgoingMessage(message); - mcs_client()->SendMessage(message, true); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + mcs_client()->SendMessage(message); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } // Receive the ack limit in messages, which should trigger an automatic @@ -499,14 +632,21 @@ TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) { std::vector<std::string> id_list; for (int i = 1; i <= kAckLimitSize; ++i) { id_list.push_back(base::IntToString(i)); - MCSMessage message( - BuildDataMessage("from", "category", i, id_list.back())); + MCSMessage message(BuildDataMessage("from", + "category", + id_list.back(), + 1, + id_list.back(), + kTTLValue, + 1, + 0, + "", + 0)); GetFakeHandler()->ReceiveMessage(message); WaitForMCSEvent(); PumpLoop(); } - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Receive a heartbeat confirming the ack (and receive the heartbeat ack). scoped_ptr<mcs_proto::HeartbeatPing> heartbeat( @@ -523,16 +663,135 @@ TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) { GetFakeHandler()->ReceiveMessage( MCSMessage(kHeartbeatPingTag, heartbeat.PassAs<const google::protobuf::MessageLite>())); - WaitForMCSEvent(); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + PumpLoop(); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Rebuild the client. Nothing should be sent on login. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector<std::string>()); - EXPECT_TRUE(GetFakeHandler()-> - AllOutgoingMessagesReceived()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); +} + +// If a message's TTL has expired by the time it reaches the front of the send +// queue, it should be dropped. +TEST_F(MCSClientTest, ExpiredTTLOnSend) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + MCSMessage message(BuildDataMessage( + "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0)); + + // Advance time to after the TTL. + clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue + 2)); + EXPECT_TRUE(sent_message_id().empty()); + mcs_client()->SendMessage(message); + + // No messages should be sent, but the callback should still be invoked. + EXPECT_EQ("X", sent_message_id()); + EXPECT_EQ(MCSClient::TTL_EXCEEDED, message_send_status()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); +} + +TEST_F(MCSClientTest, ExpiredTTLOnRestart) { + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + GetFakeHandler()->set_fail_send(true); + MCSMessage message(BuildDataMessage( + "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0)); + + // The initial (failed) send. + GetFakeHandler()->ExpectOutgoingMessage(message); + GetFakeHandler()->set_fail_send(false); + mcs_client()->SendMessage(message); + PumpLoop(); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); + + // Move the clock forward and rebuild the client, which should fail the + // message send on restart. + clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue + 2)); + StoreCredentials(); + BuildMCSClient(); + InitializeClient(); + LoginClient(std::vector<std::string>()); + PumpLoop(); + EXPECT_EQ("X", sent_message_id()); + EXPECT_EQ(MCSClient::TTL_EXCEEDED, message_send_status()); + EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); +} + +// Sending two messages with the same collapse key and same app id while +// disconnected should only send the latter of the two on reconnection. +TEST_F(MCSClientTest, CollapseKeysSameApp) { + BuildMCSClient(); + InitializeClient(); + MCSMessage message(BuildDataMessage( + "from", "app", "message id 1", 1, "1", kTTLValue, 1, 0, "token", 0)); + mcs_client()->SendMessage(message); + + MCSMessage message2(BuildDataMessage( + "from", "app", "message id 2", 1, "1", kTTLValue, 1, 0, "token", 0)); + mcs_client()->SendMessage(message2); + + LoginClient(std::vector<std::string>()); + GetFakeHandler()->ExpectOutgoingMessage(message2); + PumpLoop(); +} + +// Sending two messages with the same collapse key and different app id while +// disconnected should not perform any collapsing. +TEST_F(MCSClientTest, CollapseKeysDifferentApp) { + BuildMCSClient(); + InitializeClient(); + MCSMessage message(BuildDataMessage( + "from", "app", "message id 1", 1, "1", kTTLValue, 1, 0, "token", 0)); + mcs_client()->SendMessage(message); + + MCSMessage message2(BuildDataMessage("from", + "app 2", + "message id 2", + 1, + "2", + kTTLValue, + 1, + 0, + "token", + 0)); + mcs_client()->SendMessage(message2); + + LoginClient(std::vector<std::string>()); + GetFakeHandler()->ExpectOutgoingMessage(message); + GetFakeHandler()->ExpectOutgoingMessage(message2); + PumpLoop(); +} + +// Sending two messages with the same collapse key and app id, but different +// user, while disconnected, should not perform any collapsing. +TEST_F(MCSClientTest, CollapseKeysDifferentUser) { + BuildMCSClient(); + InitializeClient(); + MCSMessage message(BuildDataMessage( + "from", "app", "message id 1", 1, "1", kTTLValue, 1, 0, "token", 0)); + mcs_client()->SendMessage(message); + + MCSMessage message2(BuildDataMessage("from", + "app", + "message id 2", + 1, + "2", + kTTLValue, + 1, + 0, + "token", + 1)); + mcs_client()->SendMessage(message2); + + LoginClient(std::vector<std::string>()); + GetFakeHandler()->ExpectOutgoingMessage(message); + GetFakeHandler()->ExpectOutgoingMessage(message2); + PumpLoop(); } } // namespace diff --git a/chromium/google_apis/gcm/engine/registration_info.cc b/chromium/google_apis/gcm/engine/registration_info.cc new file mode 100644 index 00000000000..6a41c76e00e --- /dev/null +++ b/chromium/google_apis/gcm/engine/registration_info.cc @@ -0,0 +1,62 @@ +// Copyright 2014 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 "google_apis/gcm/engine/registration_info.h" + +#include "base/strings/string_util.h" + +namespace gcm { + +RegistrationInfo::RegistrationInfo() { +} + +RegistrationInfo::~RegistrationInfo() { +} + +std::string RegistrationInfo::SerializeAsString() const { + if (sender_ids.empty() || registration_id.empty()) + return std::string(); + + // Serialize as: + // sender1,sender2,...=reg_id + std::string value; + for (std::vector<std::string>::const_iterator iter = sender_ids.begin(); + iter != sender_ids.end(); ++iter) { + DCHECK(!iter->empty() && + iter->find(',') == std::string::npos && + iter->find('=') == std::string::npos); + if (!value.empty()) + value += ","; + value += *iter; + } + + DCHECK(registration_id.find('=') == std::string::npos); + value += '='; + value += registration_id; + return value; +} + +bool RegistrationInfo::ParseFromString(const std::string& value) { + if (value.empty()) + return true; + + size_t pos = value.find('='); + if (pos == std::string::npos) + return false; + + std::string senders = value.substr(0, pos); + registration_id = value.substr(pos + 1); + + Tokenize(senders, ",", &sender_ids); + + if (sender_ids.empty() || registration_id.empty()) { + sender_ids.clear(); + registration_id.clear(); + return false; + } + + return true; +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/registration_info.h b/chromium/google_apis/gcm/engine/registration_info.h new file mode 100644 index 00000000000..c06a6aa4c56 --- /dev/null +++ b/chromium/google_apis/gcm/engine/registration_info.h @@ -0,0 +1,35 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_REGISTRATION_INFO_H_ +#define GOOGLE_APIS_GCM_ENGINE_REGISTRATION_INFO_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/memory/linked_ptr.h" +#include "google_apis/gcm/base/gcm_export.h" + +namespace gcm { + +struct GCM_EXPORT RegistrationInfo { + RegistrationInfo(); + ~RegistrationInfo(); + + std::string SerializeAsString() const; + bool ParseFromString(const std::string& value); + + std::vector<std::string> sender_ids; + std::string registration_id; +}; + +// Map of app id to registration info. +typedef std::map<std::string, linked_ptr<RegistrationInfo> > +RegistrationInfoMap; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_REGISTRATION_INFO_H_ diff --git a/chromium/google_apis/gcm/engine/registration_request.cc b/chromium/google_apis/gcm/engine/registration_request.cc new file mode 100644 index 00000000000..e72e7ba2b79 --- /dev/null +++ b/chromium/google_apis/gcm/engine/registration_request.cc @@ -0,0 +1,267 @@ +// Copyright 2014 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 "google_apis/gcm/engine/registration_request.h" + +#include "base/bind.h" +#include "base/message_loop/message_loop.h" +#include "base/metrics/histogram.h" +#include "base/strings/string_number_conversions.h" +#include "base/values.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" +#include "net/base/escape.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" +#include "url/gurl.h" + +namespace gcm { + +namespace { + +const char kRegistrationRequestContentType[] = + "application/x-www-form-urlencoded"; + +// Request constants. +const char kAppIdKey[] = "app"; +const char kDeviceIdKey[] = "device"; +const char kLoginHeader[] = "AidLogin"; +const char kSenderKey[] = "sender"; + +// Request validation constants. +const size_t kMaxSenders = 100; + +// Response constants. +const char kErrorPrefix[] = "Error="; +const char kTokenPrefix[] = "token="; +const char kDeviceRegistrationError[] = "PHONE_REGISTRATION_ERROR"; +const char kAuthenticationFailed[] = "AUTHENTICATION_FAILED"; +const char kInvalidSender[] = "INVALID_SENDER"; +const char kInvalidParameters[] = "INVALID_PARAMETERS"; + +void BuildFormEncoding(const std::string& key, + const std::string& value, + std::string* out) { + if (!out->empty()) + out->append("&"); + out->append(key + "=" + net::EscapeUrlEncodedData(value, true)); +} + +// Gets correct status from the error message. +RegistrationRequest::Status GetStatusFromError(const std::string& error) { + // TODO(fgorski): Improve error parsing in case there is nore then just an + // Error=ERROR_STRING in response. + if (error.find(kDeviceRegistrationError) != std::string::npos) + return RegistrationRequest::DEVICE_REGISTRATION_ERROR; + if (error.find(kAuthenticationFailed) != std::string::npos) + return RegistrationRequest::AUTHENTICATION_FAILED; + if (error.find(kInvalidSender) != std::string::npos) + return RegistrationRequest::INVALID_SENDER; + if (error.find(kInvalidParameters) != std::string::npos) + return RegistrationRequest::INVALID_PARAMETERS; + return RegistrationRequest::UNKNOWN_ERROR; +} + +// Indicates whether a retry attempt should be made based on the status of the +// last request. +bool ShouldRetryWithStatus(RegistrationRequest::Status status) { + return status == RegistrationRequest::UNKNOWN_ERROR || + status == RegistrationRequest::AUTHENTICATION_FAILED || + status == RegistrationRequest::DEVICE_REGISTRATION_ERROR || + status == RegistrationRequest::HTTP_NOT_OK || + status == RegistrationRequest::URL_FETCHING_FAILED || + status == RegistrationRequest::RESPONSE_PARSING_FAILED; +} + +void RecordRegistrationStatusToUMA(RegistrationRequest::Status status) { + UMA_HISTOGRAM_ENUMERATION("GCM.RegistrationRequestStatus", status, + RegistrationRequest::STATUS_COUNT); +} + +} // namespace + +RegistrationRequest::RequestInfo::RequestInfo( + uint64 android_id, + uint64 security_token, + const std::string& app_id, + const std::vector<std::string>& sender_ids) + : android_id(android_id), + security_token(security_token), + app_id(app_id), + sender_ids(sender_ids) { +} + +RegistrationRequest::RequestInfo::~RequestInfo() {} + +RegistrationRequest::RegistrationRequest( + const GURL& registration_url, + const RequestInfo& request_info, + const net::BackoffEntry::Policy& backoff_policy, + const RegistrationCallback& callback, + int max_retry_count, + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + GCMStatsRecorder* recorder) + : callback_(callback), + request_info_(request_info), + registration_url_(registration_url), + backoff_entry_(&backoff_policy), + request_context_getter_(request_context_getter), + retries_left_(max_retry_count), + recorder_(recorder), + weak_ptr_factory_(this) { + DCHECK_GE(max_retry_count, 0); +} + +RegistrationRequest::~RegistrationRequest() {} + +void RegistrationRequest::Start() { + DCHECK(!callback_.is_null()); + DCHECK(request_info_.android_id != 0UL); + DCHECK(request_info_.security_token != 0UL); + DCHECK(0 < request_info_.sender_ids.size() && + request_info_.sender_ids.size() <= kMaxSenders); + + DCHECK(!url_fetcher_.get()); + url_fetcher_.reset(net::URLFetcher::Create( + registration_url_, net::URLFetcher::POST, this)); + url_fetcher_->SetRequestContext(request_context_getter_); + + std::string android_id = base::Uint64ToString(request_info_.android_id); + std::string auth_header = + std::string(net::HttpRequestHeaders::kAuthorization) + ": " + + kLoginHeader + " " + android_id + ":" + + base::Uint64ToString(request_info_.security_token); + url_fetcher_->SetExtraRequestHeaders(auth_header); + + std::string body; + BuildFormEncoding(kAppIdKey, request_info_.app_id, &body); + BuildFormEncoding(kDeviceIdKey, android_id, &body); + + std::string senders; + for (std::vector<std::string>::const_iterator iter = + request_info_.sender_ids.begin(); + iter != request_info_.sender_ids.end(); + ++iter) { + DCHECK(!iter->empty()); + if (!senders.empty()) + senders.append(","); + senders.append(*iter); + } + BuildFormEncoding(kSenderKey, senders, &body); + UMA_HISTOGRAM_COUNTS("GCM.RegistrationSenderIdCount", + request_info_.sender_ids.size()); + + DVLOG(1) << "Performing registration for: " << request_info_.app_id; + DVLOG(1) << "Registration request: " << body; + url_fetcher_->SetUploadData(kRegistrationRequestContentType, body); + recorder_->RecordRegistrationSent(request_info_.app_id, senders); + request_start_time_ = base::TimeTicks::Now(); + url_fetcher_->Start(); +} + +void RegistrationRequest::RetryWithBackoff(bool update_backoff) { + if (update_backoff) { + DCHECK_GT(retries_left_, 0); + --retries_left_; + url_fetcher_.reset(); + backoff_entry_.InformOfRequest(false); + } + + if (backoff_entry_.ShouldRejectRequest()) { + DVLOG(1) << "Delaying GCM registration of app: " + << request_info_.app_id << ", for " + << backoff_entry_.GetTimeUntilRelease().InMilliseconds() + << " milliseconds."; + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&RegistrationRequest::RetryWithBackoff, + weak_ptr_factory_.GetWeakPtr(), + false), + backoff_entry_.GetTimeUntilRelease()); + return; + } + + Start(); +} + +RegistrationRequest::Status RegistrationRequest::ParseResponse( + const net::URLFetcher* source, std::string* token) { + if (!source->GetStatus().is_success()) { + LOG(ERROR) << "URL fetching failed."; + return URL_FETCHING_FAILED; + } + + std::string response; + if (!source->GetResponseAsString(&response)) { + LOG(ERROR) << "Failed to parse registration response as a string."; + return RESPONSE_PARSING_FAILED; + } + + if (source->GetResponseCode() == net::HTTP_OK) { + size_t token_pos = response.find(kTokenPrefix); + if (token_pos != std::string::npos) { + *token = response.substr(token_pos + arraysize(kTokenPrefix) - 1); + return SUCCESS; + } + } + + // If we are able to parse a meaningful known error, let's do so. Some errors + // will have HTTP_BAD_REQUEST, some will have HTTP_OK response code. + size_t error_pos = response.find(kErrorPrefix); + if (error_pos != std::string::npos) { + std::string error = response.substr( + error_pos + arraysize(kErrorPrefix) - 1); + return GetStatusFromError(error); + } + + // If we cannot tell what the error is, but at least we know response code was + // not OK. + if (source->GetResponseCode() != net::HTTP_OK) { + DLOG(ERROR) << "URL fetching HTTP response code is not OK. It is " + << source->GetResponseCode(); + return HTTP_NOT_OK; + } + + return UNKNOWN_ERROR; +} + +void RegistrationRequest::OnURLFetchComplete(const net::URLFetcher* source) { + std::string token; + Status status = ParseResponse(source, &token); + RecordRegistrationStatusToUMA(status); + recorder_->RecordRegistrationResponse( + request_info_.app_id, + request_info_.sender_ids, + status); + + if (ShouldRetryWithStatus(status)) { + if (retries_left_ > 0) { + recorder_->RecordRegistrationRetryRequested( + request_info_.app_id, + request_info_.sender_ids, + retries_left_); + RetryWithBackoff(true); + return; + } + + status = REACHED_MAX_RETRIES; + recorder_->RecordRegistrationResponse( + request_info_.app_id, + request_info_.sender_ids, + status); + RecordRegistrationStatusToUMA(status); + } + + if (status == SUCCESS) { + UMA_HISTOGRAM_COUNTS("GCM.RegistrationRetryCount", + backoff_entry_.failure_count()); + UMA_HISTOGRAM_TIMES("GCM.RegistrationCompleteTime", + base::TimeTicks::Now() - request_start_time_); + } + callback_.Run(status, token); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/registration_request.h b/chromium/google_apis/gcm/engine/registration_request.h new file mode 100644 index 00000000000..d41f2858f6b --- /dev/null +++ b/chromium/google_apis/gcm/engine/registration_request.h @@ -0,0 +1,128 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_REGISTRATION_REQUEST_H_ +#define GOOGLE_APIS_GCM_ENGINE_REGISTRATION_REQUEST_H_ + +#include <map> +#include <vector> + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "net/base/backoff_entry.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "url/gurl.h" + +namespace net { +class URLRequestContextGetter; +} + +namespace gcm { + +class GCMStatsRecorder; + +// Registration request is used to obtain registration IDs for applications that +// want to use GCM. It requires a set of parameters to be specified to identify +// the Chrome instance, the user, the application and a set of senders that will +// be authorized to address the application using it's assigned registration ID. +class GCM_EXPORT RegistrationRequest : public net::URLFetcherDelegate { + public: + // This enum is also used in an UMA histogram (GCMRegistrationRequestStatus + // enum defined in tools/metrics/histograms/histogram.xml). Hence the entries + // here shouldn't be deleted or re-ordered and new ones should be added to + // the end. + enum Status { + SUCCESS, // Registration completed successfully. + INVALID_PARAMETERS, // One of request paramteres was invalid. + INVALID_SENDER, // One of the provided senders was invalid. + AUTHENTICATION_FAILED, // Authentication failed. + DEVICE_REGISTRATION_ERROR, // Chrome is not properly registered. + UNKNOWN_ERROR, // Unknown error. + URL_FETCHING_FAILED, // URL fetching failed. + HTTP_NOT_OK, // HTTP status was not OK. + RESPONSE_PARSING_FAILED, // Registration response parsing failed. + REACHED_MAX_RETRIES, // Reached maximum number of retries. + // NOTE: always keep this entry at the end. Add new status types only + // immediately above this line. Make sure to update the corresponding + // histogram enum accordingly. + STATUS_COUNT + }; + + // Callback completing the registration request. + typedef base::Callback<void(Status status, + const std::string& registration_id)> + RegistrationCallback; + + // Details of the of the Registration Request. Only user's android ID and + // its serial number are optional and can be set to 0. All other parameters + // have to be specified to successfully complete the call. + struct GCM_EXPORT RequestInfo { + RequestInfo(uint64 android_id, + uint64 security_token, + const std::string& app_id, + const std::vector<std::string>& sender_ids); + ~RequestInfo(); + + // Android ID of the device. + uint64 android_id; + // Security token of the device. + uint64 security_token; + // Application ID. + std::string app_id; + // Certificate of the application. + std::string cert; + // List of IDs of senders. Allowed up to 100. + std::vector<std::string> sender_ids; + }; + + RegistrationRequest( + const GURL& registration_url, + const RequestInfo& request_info, + const net::BackoffEntry::Policy& backoff_policy, + const RegistrationCallback& callback, + int max_retry_count, + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + GCMStatsRecorder* recorder); + virtual ~RegistrationRequest(); + + void Start(); + + // URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + // Schedules a retry attempt, informs the backoff of a previous request's + // failure, when |update_backoff| is true. + void RetryWithBackoff(bool update_backoff); + + // Parse the response returned by the URL fetcher into token, and returns the + // status. + Status ParseResponse(const net::URLFetcher* source, std::string* token); + + RegistrationCallback callback_; + RequestInfo request_info_; + GURL registration_url_; + + net::BackoffEntry backoff_entry_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + scoped_ptr<net::URLFetcher> url_fetcher_; + int retries_left_; + base::TimeTicks request_start_time_; + + // Recorder that records GCM activities for debugging purpose. Not owned. + GCMStatsRecorder* recorder_; + + base::WeakPtrFactory<RegistrationRequest> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(RegistrationRequest); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_REGISTRATION_REQUEST_H_ diff --git a/chromium/google_apis/gcm/engine/registration_request_unittest.cc b/chromium/google_apis/gcm/engine/registration_request_unittest.cc new file mode 100644 index 00000000000..c5b8c767aed --- /dev/null +++ b/chromium/google_apis/gcm/engine/registration_request_unittest.cc @@ -0,0 +1,427 @@ +// Copyright 2014 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 <map> +#include <string> +#include <vector> + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_tokenizer.h" +#include "google_apis/gcm/engine/registration_request.h" +#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_status.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { +const uint64 kAndroidId = 42UL; +const char kAppId[] = "TestAppId"; +const char kDeveloperId[] = "Project1"; +const char kLoginHeader[] = "AidLogin"; +const char kRegistrationURL[] = "http://foo.bar/register"; +const uint64 kSecurityToken = 77UL; + +// Backoff policy for testing registration request. +const net::BackoffEntry::Policy kDefaultBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + // Explicitly set to 2 to skip the delay on the first retry, as we are not + // trying to test the backoff itself, but rather the fact that retry happens. + 2, + + // Initial delay for exponential back-off in ms. + 15000, // 15 seconds. + + // 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.5, // 50%. + + // Maximum amount of time we are willing to delay our request in ms. + 1000 * 60 * 5, // 5 minutes. + + // 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 + +class RegistrationRequestTest : public testing::Test { + public: + RegistrationRequestTest(); + virtual ~RegistrationRequestTest(); + + void RegistrationCallback(RegistrationRequest::Status status, + const std::string& registration_id); + + void CreateRequest(const std::string& sender_ids); + void SetResponseStatusAndString(net::HttpStatusCode status_code, + const std::string& response_body); + void CompleteFetch(); + void set_max_retry_count(int max_retry_count) { + max_retry_count_ = max_retry_count; + } + + protected: + int max_retry_count_; + RegistrationRequest::Status status_; + std::string registration_id_; + bool callback_called_; + std::map<std::string, std::string> extras_; + scoped_ptr<RegistrationRequest> request_; + base::MessageLoop message_loop_; + net::TestURLFetcherFactory url_fetcher_factory_; + scoped_refptr<net::TestURLRequestContextGetter> url_request_context_getter_; + FakeGCMStatsRecorder recorder_; +}; + +RegistrationRequestTest::RegistrationRequestTest() + : max_retry_count_(2), + status_(RegistrationRequest::SUCCESS), + callback_called_(false), + url_request_context_getter_(new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy())) {} + +RegistrationRequestTest::~RegistrationRequestTest() {} + +void RegistrationRequestTest::RegistrationCallback( + RegistrationRequest::Status status, + const std::string& registration_id) { + status_ = status; + registration_id_ = registration_id; + callback_called_ = true; +} + +void RegistrationRequestTest::CreateRequest(const std::string& sender_ids) { + std::vector<std::string> senders; + base::StringTokenizer tokenizer(sender_ids, ","); + while (tokenizer.GetNext()) + senders.push_back(tokenizer.token()); + + request_.reset(new RegistrationRequest( + GURL(kRegistrationURL), + RegistrationRequest::RequestInfo(kAndroidId, + kSecurityToken, + kAppId, + senders), + kDefaultBackoffPolicy, + base::Bind(&RegistrationRequestTest::RegistrationCallback, + base::Unretained(this)), + max_retry_count_, + url_request_context_getter_.get(), + &recorder_)); +} + +void RegistrationRequestTest::SetResponseStatusAndString( + net::HttpStatusCode status_code, + const std::string& response_body) { + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + fetcher->set_response_code(status_code); + fetcher->SetResponseString(response_body); +} + +void RegistrationRequestTest::CompleteFetch() { + registration_id_.clear(); + status_ = RegistrationRequest::SUCCESS; + callback_called_ = false; + + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + fetcher->delegate()->OnURLFetchComplete(fetcher); +} + +TEST_F(RegistrationRequestTest, RequestSuccessful) { + set_max_retry_count(0); + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, RequestDataAndURL) { + CreateRequest(kDeveloperId); + request_->Start(); + + // Get data sent by request. + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + + EXPECT_EQ(GURL(kRegistrationURL), fetcher->GetOriginalURL()); + + // Verify that authorization header was put together properly. + net::HttpRequestHeaders headers; + fetcher->GetExtraRequestHeaders(&headers); + std::string auth_header; + headers.GetHeader(net::HttpRequestHeaders::kAuthorization, &auth_header); + base::StringTokenizer auth_tokenizer(auth_header, " :"); + ASSERT_TRUE(auth_tokenizer.GetNext()); + EXPECT_EQ(kLoginHeader, auth_tokenizer.token()); + ASSERT_TRUE(auth_tokenizer.GetNext()); + EXPECT_EQ(base::Uint64ToString(kAndroidId), auth_tokenizer.token()); + ASSERT_TRUE(auth_tokenizer.GetNext()); + EXPECT_EQ(base::Uint64ToString(kSecurityToken), auth_tokenizer.token()); + + std::map<std::string, std::string> expected_pairs; + expected_pairs["app"] = kAppId; + expected_pairs["sender"] = kDeveloperId; + expected_pairs["device"] = base::Uint64ToString(kAndroidId); + + // Verify data was formatted properly. + std::string upload_data = fetcher->upload_data(); + base::StringTokenizer data_tokenizer(upload_data, "&="); + while (data_tokenizer.GetNext()) { + std::map<std::string, std::string>::iterator iter = + expected_pairs.find(data_tokenizer.token()); + ASSERT_TRUE(iter != expected_pairs.end()); + ASSERT_TRUE(data_tokenizer.GetNext()); + EXPECT_EQ(iter->second, data_tokenizer.token()); + // Ensure that none of the keys appears twice. + expected_pairs.erase(iter); + } + + EXPECT_EQ(0UL, expected_pairs.size()); +} + +TEST_F(RegistrationRequestTest, RequestRegistrationWithMultipleSenderIds) { + CreateRequest("sender1,sender2@gmail.com"); + request_->Start(); + + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + + // Verify data was formatted properly. + std::string upload_data = fetcher->upload_data(); + base::StringTokenizer data_tokenizer(upload_data, "&="); + + // Skip all tokens until you hit entry for senders. + while (data_tokenizer.GetNext() && data_tokenizer.token() != "sender") + continue; + + ASSERT_TRUE(data_tokenizer.GetNext()); + std::string senders(net::UnescapeURLComponent(data_tokenizer.token(), + net::UnescapeRule::URL_SPECIAL_CHARS)); + base::StringTokenizer sender_tokenizer(senders, ","); + ASSERT_TRUE(sender_tokenizer.GetNext()); + EXPECT_EQ("sender1", sender_tokenizer.token()); + ASSERT_TRUE(sender_tokenizer.GetNext()); + EXPECT_EQ("sender2@gmail.com", sender_tokenizer.token()); +} + +TEST_F(RegistrationRequestTest, ResponseParsing) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseHttpStatusNotOK) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, "token=2501"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseMissingRegistrationId) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, ""); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_OK, "some error in response"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + // Ensuring a retry happened and succeeds. + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseDeviceRegistrationError) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "Error=PHONE_REGISTRATION_ERROR"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + // Ensuring a retry happened and succeeds. + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseAuthenticationError) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, + "Error=AUTHENTICATION_FAILED"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + // Ensuring a retry happened and succeeds. + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseInvalidParameters) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "Error=INVALID_PARAMETERS"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::INVALID_PARAMETERS, status_); + EXPECT_EQ(std::string(), registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseInvalidSender) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "Error=INVALID_SENDER"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::INVALID_SENDER, status_); + EXPECT_EQ(std::string(), registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseInvalidSenderBadRequest) { + CreateRequest("sender1"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_BAD_REQUEST, "Error=INVALID_SENDER"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::INVALID_SENDER, status_); + EXPECT_EQ(std::string(), registration_id_); +} + +TEST_F(RegistrationRequestTest, RequestNotSuccessful) { + CreateRequest("sender1,sender2"); + request_->Start(); + + net::URLRequestStatus request_status(net::URLRequestStatus::FAILED, 1); + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + fetcher->set_status(request_status); + + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + // Ensuring a retry happened and succeeded. + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, ResponseHttpNotOk) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + // Ensuring a retry happened and succeeded. + SetResponseStatusAndString(net::HTTP_OK, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::SUCCESS, status_); + EXPECT_EQ("2501", registration_id_); +} + +TEST_F(RegistrationRequestTest, MaximumAttemptsReachedWithZeroRetries) { + set_max_retry_count(0); + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::REACHED_MAX_RETRIES, status_); + EXPECT_EQ(std::string(), registration_id_); +} + +TEST_F(RegistrationRequestTest, MaximumAttemptsReached) { + CreateRequest("sender1,sender2"); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(RegistrationRequest::REACHED_MAX_RETRIES, status_); + EXPECT_EQ(std::string(), registration_id_); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/rmq_store.cc b/chromium/google_apis/gcm/engine/rmq_store.cc deleted file mode 100644 index eaa6dcc8059..00000000000 --- a/chromium/google_apis/gcm/engine/rmq_store.cc +++ /dev/null @@ -1,491 +0,0 @@ -// Copyright 2013 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 "google_apis/gcm/engine/rmq_store.h" - -#include "base/basictypes.h" -#include "base/bind.h" -#include "base/callback.h" -#include "base/files/file_path.h" -#include "base/logging.h" -#include "base/message_loop/message_loop_proxy.h" -#include "base/sequenced_task_runner.h" -#include "base/stl_util.h" -#include "base/strings/string_number_conversions.h" -#include "base/strings/string_piece.h" -#include "base/tracked_objects.h" -#include "components/webdata/encryptor/encryptor.h" -#include "google_apis/gcm/base/mcs_message.h" -#include "google_apis/gcm/base/mcs_util.h" -#include "google_apis/gcm/protocol/mcs.pb.h" -#include "third_party/leveldatabase/src/include/leveldb/db.h" - -namespace gcm { - -namespace { - -// ---- LevelDB keys. ---- -// Key for this device's android id. -const char kDeviceAIDKey[] = "device_aid_key"; -// Key for this device's android security token. -const char kDeviceTokenKey[] = "device_token_key"; -// Lowest lexicographically ordered incoming message key. -// Used for prefixing messages. -const char kIncomingMsgKeyStart[] = "incoming1-"; -// Key guaranteed to be higher than all incoming message keys. -// Used for limiting iteration. -const char kIncomingMsgKeyEnd[] = "incoming2-"; -// Lowest lexicographically ordered outgoing message key. -// Used for prefixing outgoing messages. -const char kOutgoingMsgKeyStart[] = "outgoing1-"; -// Key guaranteed to be higher than all outgoing message keys. -// Used for limiting iteration. -const char kOutgoingMsgKeyEnd[] = "outgoing2-"; - -std::string MakeIncomingKey(const std::string& persistent_id) { - return kIncomingMsgKeyStart + persistent_id; -} - -std::string MakeOutgoingKey(const std::string& persistent_id) { - return kOutgoingMsgKeyStart + persistent_id; -} - -std::string ParseOutgoingKey(const std::string& key) { - return key.substr(arraysize(kOutgoingMsgKeyStart) - 1); -} - -leveldb::Slice MakeSlice(const base::StringPiece& s) { - return leveldb::Slice(s.begin(), s.size()); -} - -} // namespace - -class RMQStore::Backend : public base::RefCountedThreadSafe<RMQStore::Backend> { - public: - Backend(const base::FilePath& path, - scoped_refptr<base::SequencedTaskRunner> foreground_runner); - - // Blocking implementations of RMQStore methods. - void Load(const LoadCallback& callback); - void Destroy(const UpdateCallback& callback); - void SetDeviceCredentials(uint64 device_android_id, - uint64 device_security_token, - const UpdateCallback& callback); - void AddIncomingMessage(const std::string& persistent_id, - const UpdateCallback& callback); - void RemoveIncomingMessages(const PersistentIdList& persistent_ids, - const UpdateCallback& callback); - void AddOutgoingMessage(const std::string& persistent_id, - const MCSMessage& message, - const UpdateCallback& callback); - void RemoveOutgoingMessages(const PersistentIdList& persistent_ids, - const UpdateCallback& callback); - - private: - friend class base::RefCountedThreadSafe<Backend>; - ~Backend(); - - bool LoadDeviceCredentials(uint64* android_id, uint64* security_token); - bool LoadIncomingMessages(std::vector<std::string>* incoming_messages); - bool LoadOutgoingMessages( - std::map<std::string, google::protobuf::MessageLite*>* outgoing_messages); - - const base::FilePath path_; - scoped_refptr<base::SequencedTaskRunner> foreground_task_runner_; - - scoped_ptr<leveldb::DB> db_; -}; - -RMQStore::Backend::Backend( - const base::FilePath& path, - scoped_refptr<base::SequencedTaskRunner> foreground_task_runner) - : path_(path), - foreground_task_runner_(foreground_task_runner) { -} - -RMQStore::Backend::~Backend() { -} - -void RMQStore::Backend::Load(const LoadCallback& callback) { - LoadResult result; - - leveldb::Options options; - options.create_if_missing = true; - leveldb::DB* db; - leveldb::Status status = leveldb::DB::Open(options, - path_.AsUTF8Unsafe(), - &db); - if (!status.ok()) { - LOG(ERROR) << "Failed to open database " << path_.value() - << ": " << status.ToString(); - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, result)); - return; - } - db_.reset(db); - - if (!LoadDeviceCredentials(&result.device_android_id, - &result.device_security_token) || - !LoadIncomingMessages(&result.incoming_messages) || - !LoadOutgoingMessages(&result.outgoing_messages)) { - result.device_android_id = 0; - result.device_security_token = 0; - result.incoming_messages.clear(); - STLDeleteContainerPairSecondPointers(result.outgoing_messages.begin(), - result.outgoing_messages.end()); - result.outgoing_messages.clear(); - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, result)); - return; - } - - DVLOG(1) << "Succeeded in loading " << result.incoming_messages.size() - << " unacknowledged incoming messages and " - << result.outgoing_messages.size() - << " unacknowledged outgoing messages."; - result.success = true; - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, result)); - return; -} - -void RMQStore::Backend::Destroy(const UpdateCallback& callback) { - DVLOG(1) << "Destroying RMQ store."; - const leveldb::Status s = - leveldb::DestroyDB(path_.AsUTF8Unsafe(), - leveldb::Options()); - if (s.ok()) { - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, true)); - return; - } - LOG(ERROR) << "Destroy failed."; - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, false)); -} - -void RMQStore::Backend::SetDeviceCredentials(uint64 device_android_id, - uint64 device_security_token, - const UpdateCallback& callback) { - DVLOG(1) << "Saving device credentials with AID " << device_android_id; - leveldb::WriteOptions write_options; - write_options.sync = true; - - std::string encrypted_token; - Encryptor::EncryptString(base::Uint64ToString(device_security_token), - &encrypted_token); - leveldb::Status s = - db_->Put(write_options, - MakeSlice(kDeviceAIDKey), - MakeSlice(base::Uint64ToString(device_android_id))); - if (s.ok()) { - s = db_->Put(write_options, - MakeSlice(kDeviceTokenKey), - MakeSlice(encrypted_token)); - } - if (s.ok()) { - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, true)); - return; - } - LOG(ERROR) << "LevelDB put failed: " << s.ToString(); - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, false)); -} - -void RMQStore::Backend::AddIncomingMessage(const std::string& persistent_id, - const UpdateCallback& callback) { - DVLOG(1) << "Saving incoming message with id " << persistent_id; - leveldb::WriteOptions write_options; - write_options.sync = true; - - const leveldb::Status s = - db_->Put(write_options, - MakeSlice(MakeIncomingKey(persistent_id)), - MakeSlice(persistent_id)); - if (s.ok()) { - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, true)); - return; - } - LOG(ERROR) << "LevelDB put failed: " << s.ToString(); - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, false)); -} - -void RMQStore::Backend::RemoveIncomingMessages( - const PersistentIdList& persistent_ids, - const UpdateCallback& callback) { - leveldb::WriteOptions write_options; - write_options.sync = true; - - leveldb::Status s; - for (PersistentIdList::const_iterator iter = persistent_ids.begin(); - iter != persistent_ids.end(); ++iter){ - DVLOG(1) << "Removing incoming message with id " << *iter; - s = db_->Delete(write_options, - MakeSlice(MakeIncomingKey(*iter))); - if (!s.ok()) - break; - } - if (s.ok()) { - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, true)); - return; - } - LOG(ERROR) << "LevelDB remove failed: " << s.ToString(); - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, false)); -} - -void RMQStore::Backend::AddOutgoingMessage( - const std::string& persistent_id, - const MCSMessage& message, - const UpdateCallback& callback) { - DVLOG(1) << "Saving outgoing message with id " << persistent_id; - leveldb::WriteOptions write_options; - write_options.sync = true; - - std::string data = static_cast<char>(message.tag()) + - message.SerializeAsString(); - const leveldb::Status s = - db_->Put(write_options, - MakeSlice(MakeOutgoingKey(persistent_id)), - MakeSlice(data)); - if (s.ok()) { - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, true)); - return; - } - LOG(ERROR) << "LevelDB put failed: " << s.ToString(); - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, false)); - -} - -void RMQStore::Backend::RemoveOutgoingMessages( - const PersistentIdList& persistent_ids, - const UpdateCallback& callback) { - leveldb::WriteOptions write_options; - write_options.sync = true; - - leveldb::Status s; - for (PersistentIdList::const_iterator iter = persistent_ids.begin(); - iter != persistent_ids.end(); ++iter){ - DVLOG(1) << "Removing outgoing message with id " << *iter; - s = db_->Delete(write_options, - MakeSlice(MakeOutgoingKey(*iter))); - if (!s.ok()) - break; - } - if (s.ok()) { - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, true)); - return; - } - LOG(ERROR) << "LevelDB remove failed: " << s.ToString(); - foreground_task_runner_->PostTask(FROM_HERE, - base::Bind(callback, false)); -} - -bool RMQStore::Backend::LoadDeviceCredentials(uint64* android_id, - uint64* security_token) { - leveldb::ReadOptions read_options; - read_options.verify_checksums = true; - - std::string result; - leveldb::Status s = db_->Get(read_options, - MakeSlice(kDeviceAIDKey), - &result); - if (s.ok()) { - if (!base::StringToUint64(result, android_id)) { - LOG(ERROR) << "Failed to restore device id."; - return false; - } - result.clear(); - s = db_->Get(read_options, - MakeSlice(kDeviceTokenKey), - &result); - } - if (s.ok()) { - std::string decrypted_token; - Encryptor::DecryptString(result, &decrypted_token); - if (!base::StringToUint64(decrypted_token, security_token)) { - LOG(ERROR) << "Failed to restore security token."; - return false; - } - return true; - } - - if (s.IsNotFound()) { - DVLOG(1) << "No credentials found."; - return true; - } - - LOG(ERROR) << "Error reading credentials from store."; - return false; -} - -bool RMQStore::Backend::LoadIncomingMessages( - std::vector<std::string>* incoming_messages) { - leveldb::ReadOptions read_options; - read_options.verify_checksums = true; - - scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); - for (iter->Seek(MakeSlice(kIncomingMsgKeyStart)); - iter->Valid() && iter->key().ToString() < kIncomingMsgKeyEnd; - iter->Next()) { - leveldb::Slice s = iter->value(); - if (s.empty()) { - LOG(ERROR) << "Error reading incoming message with key " - << iter->key().ToString(); - return false; - } - DVLOG(1) << "Found incoming message with id " << s.ToString(); - incoming_messages->push_back(s.ToString()); - } - - return true; -} - -bool RMQStore::Backend::LoadOutgoingMessages( - std::map<std::string, google::protobuf::MessageLite*>* - outgoing_messages) { - leveldb::ReadOptions read_options; - read_options.verify_checksums = true; - - scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options)); - for (iter->Seek(MakeSlice(kOutgoingMsgKeyStart)); - iter->Valid() && iter->key().ToString() < kOutgoingMsgKeyEnd; - iter->Next()) { - leveldb::Slice s = iter->value(); - if (s.size() <= 1) { - LOG(ERROR) << "Error reading incoming message with key " << s.ToString(); - return false; - } - uint8 tag = iter->value().data()[0]; - std::string id = ParseOutgoingKey(iter->key().ToString()); - scoped_ptr<google::protobuf::MessageLite> message( - BuildProtobufFromTag(tag)); - if (!message.get() || - !message->ParseFromString(iter->value().ToString().substr(1))) { - LOG(ERROR) << "Failed to parse outgoing message with id " - << id << " and tag " << tag; - return false; - } - DVLOG(1) << "Found outgoing message with id " << id << " of type " - << base::IntToString(tag); - (*outgoing_messages)[id] = message.release(); - } - - return true; -} - -RMQStore::LoadResult::LoadResult() - : success(false), - device_android_id(0), - device_security_token(0) { -} -RMQStore::LoadResult::~LoadResult() {} - -RMQStore::RMQStore( - const base::FilePath& path, - scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) - : backend_(new Backend(path, base::MessageLoopProxy::current())), - blocking_task_runner_(blocking_task_runner) { -} - -RMQStore::~RMQStore() { -} - -void RMQStore::Load(const LoadCallback& callback) { - blocking_task_runner_->PostTask(FROM_HERE, - base::Bind(&RMQStore::Backend::Load, - backend_, - callback)); -} - -void RMQStore::Destroy(const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::Destroy, - backend_, - callback)); -} - -void RMQStore::SetDeviceCredentials(uint64 device_android_id, - uint64 device_security_token, - const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::SetDeviceCredentials, - backend_, - device_android_id, - device_security_token, - callback)); -} - -void RMQStore::AddIncomingMessage(const std::string& persistent_id, - const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::AddIncomingMessage, - backend_, - persistent_id, - callback)); -} - -void RMQStore::RemoveIncomingMessage(const std::string& persistent_id, - const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::RemoveIncomingMessages, - backend_, - PersistentIdList(1, persistent_id), - callback)); -} - -void RMQStore::RemoveIncomingMessages(const PersistentIdList& persistent_ids, - const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::RemoveIncomingMessages, - backend_, - persistent_ids, - callback)); -} - -void RMQStore::AddOutgoingMessage(const std::string& persistent_id, - const MCSMessage& message, - const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::AddOutgoingMessage, - backend_, - persistent_id, - message, - callback)); -} - -void RMQStore::RemoveOutgoingMessage(const std::string& persistent_id, - const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::RemoveOutgoingMessages, - backend_, - PersistentIdList(1, persistent_id), - callback)); -} - -void RMQStore::RemoveOutgoingMessages(const PersistentIdList& persistent_ids, - const UpdateCallback& callback) { - blocking_task_runner_->PostTask( - FROM_HERE, - base::Bind(&RMQStore::Backend::RemoveOutgoingMessages, - backend_, - persistent_ids, - callback)); -} - -} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/rmq_store.h b/chromium/google_apis/gcm/engine/rmq_store.h deleted file mode 100644 index d3762a199c7..00000000000 --- a/chromium/google_apis/gcm/engine/rmq_store.h +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2013 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. - -#ifndef GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_ -#define GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_ - -#include <map> -#include <string> -#include <vector> - -#include "base/basictypes.h" -#include "base/callback_forward.h" -#include "base/memory/ref_counted.h" -#include "google_apis/gcm/base/gcm_export.h" - -namespace base { -class FilePath; -class SequencedTaskRunner; -} // namespace base - -namespace google { -namespace protobuf { -class MessageLite; -} // namespace protobuf -} // namespace google - -namespace gcm { - -class MCSMessage; - -// A Reliable Message Queue store. -// Will perform all blocking operations on the blocking task runner, and will -// post all callbacks to the thread on which the RMQStore is created. -class GCM_EXPORT RMQStore { - public: - // Container for Load(..) results. - struct GCM_EXPORT LoadResult { - LoadResult(); - ~LoadResult(); - - bool success; - uint64 device_android_id; - uint64 device_security_token; - std::vector<std::string> incoming_messages; - std::map<std::string, google::protobuf::MessageLite*> - outgoing_messages; - }; - - typedef std::vector<std::string> PersistentIdList; - // Note: callee receives ownership of |outgoing_messages|' values. - typedef base::Callback<void(const LoadResult& result)> LoadCallback; - typedef base::Callback<void(bool success)> UpdateCallback; - - RMQStore(const base::FilePath& path, - scoped_refptr<base::SequencedTaskRunner> blocking_task_runner); - ~RMQStore(); - - // Load the directory and pass the initial state back to caller. - void Load(const LoadCallback& callback); - - // Clears the RMQ store of all data and destroys any LevelDB files associated - // with this store. - // WARNING: this will permanently destroy any pending outgoing messages - // and require the device to re-create credentials. - void Destroy(const UpdateCallback& callback); - - // Sets this device's messaging credentials. - void SetDeviceCredentials(uint64 device_android_id, - uint64 device_security_token, - const UpdateCallback& callback); - - // Unacknowledged incoming message handling. - void AddIncomingMessage(const std::string& persistent_id, - const UpdateCallback& callback); - void RemoveIncomingMessage(const std::string& persistent_id, - const UpdateCallback& callback); - void RemoveIncomingMessages(const PersistentIdList& persistent_ids, - const UpdateCallback& callback); - - // Unacknowledged outgoing messages handling. - // TODO(zea): implement per-app limits on the number of outgoing messages. - void AddOutgoingMessage(const std::string& persistent_id, - const MCSMessage& message, - const UpdateCallback& callback); - void RemoveOutgoingMessage(const std::string& persistent_id, - const UpdateCallback& callback); - void RemoveOutgoingMessages(const PersistentIdList& persistent_ids, - const UpdateCallback& callback); - - private: - class Backend; - - scoped_refptr<Backend> backend_; - scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; - - DISALLOW_COPY_AND_ASSIGN(RMQStore); -}; - -} // namespace gcm - -#endif // GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_ diff --git a/chromium/google_apis/gcm/engine/rmq_store_unittest.cc b/chromium/google_apis/gcm/engine/rmq_store_unittest.cc deleted file mode 100644 index 1fd55bcf14b..00000000000 --- a/chromium/google_apis/gcm/engine/rmq_store_unittest.cc +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright 2013 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 "google_apis/gcm/engine/rmq_store.h" - -#include <string> -#include <vector> - -#include "base/bind.h" -#include "base/files/file_path.h" -#include "base/files/scoped_temp_dir.h" -#include "base/memory/scoped_ptr.h" -#include "base/message_loop/message_loop.h" -#include "base/run_loop.h" -#include "base/strings/string_number_conversions.h" -#include "components/webdata/encryptor/encryptor.h" -#include "google_apis/gcm/base/mcs_message.h" -#include "google_apis/gcm/base/mcs_util.h" -#include "google_apis/gcm/protocol/mcs.pb.h" -#include "testing/gtest/include/gtest/gtest.h" - -namespace gcm { - -namespace { - -// Number of persistent ids to use in tests. -const int kNumPersistentIds = 10; - -const uint64 kDeviceId = 22; -const uint64 kDeviceToken = 55; - -class RMQStoreTest : public testing::Test { - public: - RMQStoreTest(); - virtual ~RMQStoreTest(); - - scoped_ptr<RMQStore> BuildRMQStore(); - - std::string GetNextPersistentId(); - - void PumpLoop(); - - void LoadCallback(RMQStore::LoadResult* result_dst, - const RMQStore::LoadResult& result); - void UpdateCallback(bool success); - - private: - base::MessageLoop message_loop_; - base::ScopedTempDir temp_directory_; - scoped_ptr<base::RunLoop> run_loop_; -}; - -RMQStoreTest::RMQStoreTest() { - EXPECT_TRUE(temp_directory_.CreateUniqueTempDir()); - run_loop_.reset(new base::RunLoop()); - - // On OSX, prevent the Keychain permissions popup during unit tests. - #if defined(OS_MACOSX) - Encryptor::UseMockKeychain(true); - #endif -} - -RMQStoreTest::~RMQStoreTest() { -} - -scoped_ptr<RMQStore> RMQStoreTest::BuildRMQStore() { - return scoped_ptr<RMQStore>(new RMQStore(temp_directory_.path(), - message_loop_.message_loop_proxy())); -} - -std::string RMQStoreTest::GetNextPersistentId() { - return base::Uint64ToString(base::Time::Now().ToInternalValue()); -} - -void RMQStoreTest::PumpLoop() { - message_loop_.RunUntilIdle(); -} - -void RMQStoreTest::LoadCallback(RMQStore::LoadResult* result_dst, - const RMQStore::LoadResult& result) { - ASSERT_TRUE(result.success); - *result_dst = result; - run_loop_->Quit(); - run_loop_.reset(new base::RunLoop()); -} - -void RMQStoreTest::UpdateCallback(bool success) { - ASSERT_TRUE(success); -} - -// Verify creating a new database and loading it. -TEST_F(RMQStoreTest, LoadNew) { - scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); - RMQStore::LoadResult load_result; - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_EQ(0U, load_result.device_android_id); - ASSERT_EQ(0U, load_result.device_security_token); - ASSERT_TRUE(load_result.incoming_messages.empty()); - ASSERT_TRUE(load_result.outgoing_messages.empty()); -} - -TEST_F(RMQStoreTest, DeviceCredentials) { - scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); - RMQStore::LoadResult load_result; - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - rmq_store->SetDeviceCredentials(kDeviceId, - kDeviceToken, - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - - rmq_store = BuildRMQStore().Pass(); - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_EQ(kDeviceId, load_result.device_android_id); - ASSERT_EQ(kDeviceToken, load_result.device_security_token); -} - -// Verify saving some incoming messages, reopening the directory, and then -// removing those incoming messages. -TEST_F(RMQStoreTest, IncomingMessages) { - scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); - RMQStore::LoadResult load_result; - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - std::vector<std::string> persistent_ids; - for (int i = 0; i < kNumPersistentIds; ++i) { - persistent_ids.push_back(GetNextPersistentId()); - rmq_store->AddIncomingMessage(persistent_ids.back(), - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - } - - rmq_store = BuildRMQStore().Pass(); - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_EQ(persistent_ids, load_result.incoming_messages); - ASSERT_TRUE(load_result.outgoing_messages.empty()); - - rmq_store->RemoveIncomingMessages(persistent_ids, - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - - rmq_store = BuildRMQStore().Pass(); - load_result.incoming_messages.clear(); - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_TRUE(load_result.incoming_messages.empty()); - ASSERT_TRUE(load_result.outgoing_messages.empty()); -} - -// Verify saving some outgoing messages, reopening the directory, and then -// removing those outgoing messages. -TEST_F(RMQStoreTest, OutgoingMessages) { - scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); - RMQStore::LoadResult load_result; - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - std::vector<std::string> persistent_ids; - const int kNumPersistentIds = 10; - for (int i = 0; i < kNumPersistentIds; ++i) { - persistent_ids.push_back(GetNextPersistentId()); - mcs_proto::DataMessageStanza message; - message.set_from(persistent_ids.back()); - message.set_category(persistent_ids.back()); - rmq_store->AddOutgoingMessage(persistent_ids.back(), - MCSMessage(message), - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - } - - rmq_store = BuildRMQStore().Pass(); - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_TRUE(load_result.incoming_messages.empty()); - ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size()); - for (int i =0 ; i < kNumPersistentIds; ++i) { - std::string id = persistent_ids[i]; - ASSERT_TRUE(load_result.outgoing_messages[id]); - const mcs_proto::DataMessageStanza* message = - reinterpret_cast<mcs_proto::DataMessageStanza *>( - load_result.outgoing_messages[id]); - ASSERT_EQ(message->from(), id); - ASSERT_EQ(message->category(), id); - } - - rmq_store->RemoveOutgoingMessages(persistent_ids, - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - - rmq_store = BuildRMQStore().Pass(); - load_result.outgoing_messages.clear(); - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_TRUE(load_result.incoming_messages.empty()); - ASSERT_TRUE(load_result.outgoing_messages.empty()); -} - -// Verify incoming and outgoing messages don't conflict. -TEST_F(RMQStoreTest, IncomingAndOutgoingMessages) { - scoped_ptr<RMQStore> rmq_store(BuildRMQStore()); - RMQStore::LoadResult load_result; - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - std::vector<std::string> persistent_ids; - const int kNumPersistentIds = 10; - for (int i = 0; i < kNumPersistentIds; ++i) { - persistent_ids.push_back(GetNextPersistentId()); - rmq_store->AddIncomingMessage(persistent_ids.back(), - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - - mcs_proto::DataMessageStanza message; - message.set_from(persistent_ids.back()); - message.set_category(persistent_ids.back()); - rmq_store->AddOutgoingMessage(persistent_ids.back(), - MCSMessage(message), - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - } - - - rmq_store = BuildRMQStore().Pass(); - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_EQ(persistent_ids, load_result.incoming_messages); - ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size()); - for (int i =0 ; i < kNumPersistentIds; ++i) { - std::string id = persistent_ids[i]; - ASSERT_TRUE(load_result.outgoing_messages[id]); - const mcs_proto::DataMessageStanza* message = - reinterpret_cast<mcs_proto::DataMessageStanza *>( - load_result.outgoing_messages[id]); - ASSERT_EQ(message->from(), id); - ASSERT_EQ(message->category(), id); - } - - rmq_store->RemoveIncomingMessages(persistent_ids, - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - rmq_store->RemoveOutgoingMessages(persistent_ids, - base::Bind(&RMQStoreTest::UpdateCallback, - base::Unretained(this))); - PumpLoop(); - - rmq_store = BuildRMQStore().Pass(); - load_result.incoming_messages.clear(); - load_result.outgoing_messages.clear(); - rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback, - base::Unretained(this), - &load_result)); - PumpLoop(); - - ASSERT_TRUE(load_result.incoming_messages.empty()); - ASSERT_TRUE(load_result.outgoing_messages.empty()); -} - -} // namespace - -} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/unregistration_request.cc b/chromium/google_apis/gcm/engine/unregistration_request.cc new file mode 100644 index 00000000000..2b8c97dadf1 --- /dev/null +++ b/chromium/google_apis/gcm/engine/unregistration_request.cc @@ -0,0 +1,224 @@ +// Copyright 2014 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 "google_apis/gcm/engine/unregistration_request.h" + +#include "base/bind.h" +#include "base/message_loop/message_loop.h" +#include "base/metrics/histogram.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/values.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" +#include "net/base/escape.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +namespace gcm { + +namespace { + +const char kRequestContentType[] = "application/x-www-form-urlencoded"; + +// Request constants. +const char kAppIdKey[] = "app"; +const char kDeleteKey[] = "delete"; +const char kDeleteValue[] = "true"; +const char kDeviceIdKey[] = "device"; +const char kLoginHeader[] = "AidLogin"; +const char kUnregistrationCallerKey[] = "gcm_unreg_caller"; +// We are going to set the value to "false" in order to forcefully unregister +// the application. +const char kUnregistrationCallerValue[] = "false"; + +// Response constants. +const char kDeletedPrefix[] = "deleted="; +const char kErrorPrefix[] = "Error="; +const char kInvalidParameters[] = "INVALID_PARAMETERS"; + + +void BuildFormEncoding(const std::string& key, + const std::string& value, + std::string* out) { + if (!out->empty()) + out->append("&"); + out->append(key + "=" + net::EscapeUrlEncodedData(value, true)); +} + +UnregistrationRequest::Status ParseFetcherResponse( + const net::URLFetcher* source, + std::string request_app_id) { + if (!source->GetStatus().is_success()) { + DVLOG(1) << "Fetcher failed"; + return UnregistrationRequest::URL_FETCHING_FAILED; + } + + net::HttpStatusCode response_status = static_cast<net::HttpStatusCode>( + source->GetResponseCode()); + if (response_status != net::HTTP_OK) { + DVLOG(1) << "HTTP Status code is not OK, but: " << response_status; + if (response_status == net::HTTP_SERVICE_UNAVAILABLE) + return UnregistrationRequest::SERVICE_UNAVAILABLE; + else if (response_status == net::HTTP_INTERNAL_SERVER_ERROR) + return UnregistrationRequest::INTERNAL_SERVER_ERROR; + return UnregistrationRequest::HTTP_NOT_OK; + } + + std::string response; + if (!source->GetResponseAsString(&response)) { + DVLOG(1) << "Failed to get response body."; + return UnregistrationRequest::NO_RESPONSE_BODY; + } + + DVLOG(1) << "Parsing unregistration response."; + if (response.find(kDeletedPrefix) != std::string::npos) { + std::string app_id = response.substr( + response.find(kDeletedPrefix) + arraysize(kDeletedPrefix) - 1); + if (app_id == request_app_id) + return UnregistrationRequest::SUCCESS; + return UnregistrationRequest::INCORRECT_APP_ID; + } + + if (response.find(kErrorPrefix) != std::string::npos) { + std::string error = response.substr( + response.find(kErrorPrefix) + arraysize(kErrorPrefix) - 1); + if (error == kInvalidParameters) + return UnregistrationRequest::INVALID_PARAMETERS; + return UnregistrationRequest::UNKNOWN_ERROR; + } + + DVLOG(1) << "Not able to parse a meaningful output from response body." + << response; + return UnregistrationRequest::RESPONSE_PARSING_FAILED; +} + +} // namespace + +UnregistrationRequest::RequestInfo::RequestInfo( + uint64 android_id, + uint64 security_token, + const std::string& app_id) + : android_id(android_id), + security_token(security_token), + app_id(app_id) { +} + +UnregistrationRequest::RequestInfo::~RequestInfo() {} + +UnregistrationRequest::UnregistrationRequest( + const GURL& registration_url, + const RequestInfo& request_info, + const net::BackoffEntry::Policy& backoff_policy, + const UnregistrationCallback& callback, + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + GCMStatsRecorder* recorder) + : callback_(callback), + request_info_(request_info), + registration_url_(registration_url), + backoff_entry_(&backoff_policy), + request_context_getter_(request_context_getter), + recorder_(recorder), + weak_ptr_factory_(this) { +} + +UnregistrationRequest::~UnregistrationRequest() {} + +void UnregistrationRequest::Start() { + DCHECK(!callback_.is_null()); + DCHECK(request_info_.android_id != 0UL); + DCHECK(request_info_.security_token != 0UL); + DCHECK(!url_fetcher_.get()); + + url_fetcher_.reset(net::URLFetcher::Create( + registration_url_, net::URLFetcher::POST, this)); + url_fetcher_->SetRequestContext(request_context_getter_); + + std::string android_id = base::Uint64ToString(request_info_.android_id); + std::string auth_header = + std::string(kLoginHeader) + " " + android_id + ":" + + base::Uint64ToString(request_info_.security_token); + net::HttpRequestHeaders headers; + headers.SetHeader(net::HttpRequestHeaders::kAuthorization, auth_header); + headers.SetHeader(kAppIdKey, request_info_.app_id); + url_fetcher_->SetExtraRequestHeaders(headers.ToString()); + + std::string body; + BuildFormEncoding(kAppIdKey, request_info_.app_id, &body); + BuildFormEncoding(kDeviceIdKey, android_id, &body); + BuildFormEncoding(kDeleteKey, kDeleteValue, &body); + BuildFormEncoding(kUnregistrationCallerKey, + kUnregistrationCallerValue, + &body); + + DVLOG(1) << "Unregistration request: " << body; + url_fetcher_->SetUploadData(kRequestContentType, body); + + DVLOG(1) << "Performing unregistration for: " << request_info_.app_id; + recorder_->RecordUnregistrationSent(request_info_.app_id); + request_start_time_ = base::TimeTicks::Now(); + url_fetcher_->Start(); +} + +void UnregistrationRequest::RetryWithBackoff(bool update_backoff) { + if (update_backoff) { + url_fetcher_.reset(); + backoff_entry_.InformOfRequest(false); + } + + if (backoff_entry_.ShouldRejectRequest()) { + DVLOG(1) << "Delaying GCM unregistration of app: " + << request_info_.app_id << ", for " + << backoff_entry_.GetTimeUntilRelease().InMilliseconds() + << " milliseconds."; + recorder_->RecordUnregistrationRetryDelayed( + request_info_.app_id, + backoff_entry_.GetTimeUntilRelease().InMilliseconds()); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&UnregistrationRequest::RetryWithBackoff, + weak_ptr_factory_.GetWeakPtr(), + false), + backoff_entry_.GetTimeUntilRelease()); + return; + } + + Start(); +} + +void UnregistrationRequest::OnURLFetchComplete(const net::URLFetcher* source) { + UnregistrationRequest::Status status = + ParseFetcherResponse(source, request_info_.app_id); + + DVLOG(1) << "UnregistrationRequestStauts: " << status; + UMA_HISTOGRAM_ENUMERATION("GCM.UnregistrationRequestStatus", + status, + UNREGISTRATION_STATUS_COUNT); + recorder_->RecordUnregistrationResponse(request_info_.app_id, status); + + if (status == URL_FETCHING_FAILED || + status == SERVICE_UNAVAILABLE || + status == INTERNAL_SERVER_ERROR || + status == INCORRECT_APP_ID || + status == RESPONSE_PARSING_FAILED) { + RetryWithBackoff(true); + return; + } + + // status == SUCCESS || HTTP_NOT_OK || NO_RESPONSE_BODY || + // INVALID_PARAMETERS || UNKNOWN_ERROR + + if (status == SUCCESS) { + UMA_HISTOGRAM_COUNTS("GCM.UnregistrationRetryCount", + backoff_entry_.failure_count()); + UMA_HISTOGRAM_TIMES("GCM.UnregistrationCompleteTime", + base::TimeTicks::Now() - request_start_time_); + } + + callback_.Run(status); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/engine/unregistration_request.h b/chromium/google_apis/gcm/engine/unregistration_request.h new file mode 100644 index 00000000000..aec3331d18a --- /dev/null +++ b/chromium/google_apis/gcm/engine/unregistration_request.h @@ -0,0 +1,115 @@ +// Copyright 2014 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. + +#ifndef GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_ +#define GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_ + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "net/base/backoff_entry.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "url/gurl.h" + +namespace net { +class URLRequestContextGetter; +} + +namespace gcm { + +class GCMStatsRecorder; + +// Unregistration request is used to revoke registration IDs for applications +// that were uninstalled and should no longer receive GCM messages. In case an +// attempt to unregister fails, it will retry using the backoff policy. +// TODO(fgorski): Consider sharing code with RegistrationRequest if possible. +class GCM_EXPORT UnregistrationRequest : public net::URLFetcherDelegate { + public: + // Outcome of the response parsing. Note that these enums are consumed by a + // histogram, so ordering should not be modified. + enum Status { + SUCCESS, // Unregistration completed successfully. + URL_FETCHING_FAILED, // URL fetching failed. + NO_RESPONSE_BODY, // No response body. + RESPONSE_PARSING_FAILED, // Failed to parse a meaningful output from + // response + // body. + INCORRECT_APP_ID, // App ID returned by the fetcher does not match + // request. + INVALID_PARAMETERS, // Request parameters were invalid. + SERVICE_UNAVAILABLE, // Unregistration service unavailable. + INTERNAL_SERVER_ERROR, // Internal server error happened during request. + HTTP_NOT_OK, // HTTP response code was not OK. + UNKNOWN_ERROR, // Unknown error. + // NOTE: Always keep this entry at the end. Add new status types only + // immediately above this line. Make sure to update the corresponding + // histogram enum accordingly. + UNREGISTRATION_STATUS_COUNT, + }; + + // Callback completing the unregistration request. + typedef base::Callback<void(Status success)> UnregistrationCallback; + + // Details of the of the Unregistration Request. All parameters are mandatory. + struct GCM_EXPORT RequestInfo { + RequestInfo(uint64 android_id, + uint64 security_token, + const std::string& app_id); + ~RequestInfo(); + + // Android ID of the device. + uint64 android_id; + // Security token of the device. + uint64 security_token; + // Application ID. + std::string app_id; + }; + + // Creates an instance of UnregistrationRequest. |callback| will be called + // once registration has been revoked or there has been an error that makes + // further retries pointless. + UnregistrationRequest( + const GURL& registration_url, + const RequestInfo& request_info, + const net::BackoffEntry::Policy& backoff_policy, + const UnregistrationCallback& callback, + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + GCMStatsRecorder* recorder); + virtual ~UnregistrationRequest(); + + // Starts an unregistration request. + void Start(); + + // URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + // Schedules a retry attempt and informs the backoff of previous request's + // failure, when |update_backoff| is true. + void RetryWithBackoff(bool update_backoff); + + UnregistrationCallback callback_; + RequestInfo request_info_; + GURL registration_url_; + + net::BackoffEntry backoff_entry_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + scoped_ptr<net::URLFetcher> url_fetcher_; + base::TimeTicks request_start_time_; + + // Recorder that records GCM activities for debugging purpose. Not owned. + GCMStatsRecorder* recorder_; + + base::WeakPtrFactory<UnregistrationRequest> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(UnregistrationRequest); +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_ diff --git a/chromium/google_apis/gcm/engine/unregistration_request_unittest.cc b/chromium/google_apis/gcm/engine/unregistration_request_unittest.cc new file mode 100644 index 00000000000..247150390ae --- /dev/null +++ b/chromium/google_apis/gcm/engine/unregistration_request_unittest.cc @@ -0,0 +1,295 @@ +// Copyright 2014 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 <map> +#include <string> +#include <vector> + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_tokenizer.h" +#include "google_apis/gcm/engine/unregistration_request.h" +#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { +const uint64 kAndroidId = 42UL; +const char kLoginHeader[] = "AidLogin"; +const char kAppId[] = "TestAppId"; +const char kDeletedAppId[] = "deleted=TestAppId"; +const char kRegistrationURL[] = "http://foo.bar/register"; +const uint64 kSecurityToken = 77UL; + +// Backoff policy for testing registration request. +const net::BackoffEntry::Policy kDefaultBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + // Explicitly set to 2 to skip the delay on the first retry, as we are not + // trying to test the backoff itself, but rather the fact that retry happens. + 1, + + // Initial delay for exponential back-off in ms. + 15000, // 15 seconds. + + // 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.5, // 50%. + + // Maximum amount of time we are willing to delay our request in ms. + 1000 * 60 * 5, // 5 minutes. + + // 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 + +class UnregistrationRequestTest : public testing::Test { + public: + UnregistrationRequestTest(); + virtual ~UnregistrationRequestTest(); + + void UnregistrationCallback(UnregistrationRequest::Status status); + + void CreateRequest(); + void SetResponseStatusAndString(net::HttpStatusCode status_code, + const std::string& response_body); + void CompleteFetch(); + + protected: + bool callback_called_; + UnregistrationRequest::Status status_; + scoped_ptr<UnregistrationRequest> request_; + base::MessageLoop message_loop_; + net::TestURLFetcherFactory url_fetcher_factory_; + scoped_refptr<net::TestURLRequestContextGetter> url_request_context_getter_; + FakeGCMStatsRecorder recorder_; +}; + +UnregistrationRequestTest::UnregistrationRequestTest() + : callback_called_(false), + status_(UnregistrationRequest::UNREGISTRATION_STATUS_COUNT), + url_request_context_getter_(new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy())) {} + +UnregistrationRequestTest::~UnregistrationRequestTest() {} + +void UnregistrationRequestTest::UnregistrationCallback( + UnregistrationRequest::Status status) { + callback_called_ = true; + status_ = status; +} + +void UnregistrationRequestTest::CreateRequest() { + request_.reset(new UnregistrationRequest( + GURL(kRegistrationURL), + UnregistrationRequest::RequestInfo(kAndroidId, + kSecurityToken, + kAppId), + kDefaultBackoffPolicy, + base::Bind(&UnregistrationRequestTest::UnregistrationCallback, + base::Unretained(this)), + url_request_context_getter_.get(), + &recorder_)); +} + +void UnregistrationRequestTest::SetResponseStatusAndString( + net::HttpStatusCode status_code, + const std::string& response_body) { + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + fetcher->set_response_code(status_code); + fetcher->SetResponseString(response_body); +} + +void UnregistrationRequestTest::CompleteFetch() { + status_ = UnregistrationRequest::UNREGISTRATION_STATUS_COUNT; + callback_called_ = false; + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + fetcher->delegate()->OnURLFetchComplete(fetcher); +} + +TEST_F(UnregistrationRequestTest, RequestDataPassedToFetcher) { + CreateRequest(); + request_->Start(); + + // Get data sent by request. + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + + EXPECT_EQ(GURL(kRegistrationURL), fetcher->GetOriginalURL()); + + // Verify that authorization header was put together properly. + net::HttpRequestHeaders headers; + fetcher->GetExtraRequestHeaders(&headers); + std::string auth_header; + headers.GetHeader(net::HttpRequestHeaders::kAuthorization, &auth_header); + base::StringTokenizer auth_tokenizer(auth_header, " :"); + ASSERT_TRUE(auth_tokenizer.GetNext()); + EXPECT_EQ(kLoginHeader, auth_tokenizer.token()); + ASSERT_TRUE(auth_tokenizer.GetNext()); + EXPECT_EQ(base::Uint64ToString(kAndroidId), auth_tokenizer.token()); + ASSERT_TRUE(auth_tokenizer.GetNext()); + EXPECT_EQ(base::Uint64ToString(kSecurityToken), auth_tokenizer.token()); + std::string app_id_header; + headers.GetHeader("app", &app_id_header); + EXPECT_EQ(kAppId, app_id_header); + + std::map<std::string, std::string> expected_pairs; + expected_pairs["app"] = kAppId; + expected_pairs["device"] = base::Uint64ToString(kAndroidId); + expected_pairs["delete"] = "true"; + expected_pairs["gcm_unreg_caller"] = "false"; + + // Verify data was formatted properly. + std::string upload_data = fetcher->upload_data(); + base::StringTokenizer data_tokenizer(upload_data, "&="); + while (data_tokenizer.GetNext()) { + std::map<std::string, std::string>::iterator iter = + expected_pairs.find(data_tokenizer.token()); + ASSERT_TRUE(iter != expected_pairs.end()) << data_tokenizer.token(); + ASSERT_TRUE(data_tokenizer.GetNext()) << data_tokenizer.token(); + EXPECT_EQ(iter->second, data_tokenizer.token()); + // Ensure that none of the keys appears twice. + expected_pairs.erase(iter); + } + + EXPECT_EQ(0UL, expected_pairs.size()); +} + +TEST_F(UnregistrationRequestTest, SuccessfulUnregistration) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::SUCCESS, status_); +} + +TEST_F(UnregistrationRequestTest, ResponseHttpStatusNotOK) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, ""); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::HTTP_NOT_OK, status_); +} + +TEST_F(UnregistrationRequestTest, ResponseEmpty) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, ""); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::SUCCESS, status_); +} + +TEST_F(UnregistrationRequestTest, InvalidParametersError) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "Error=INVALID_PARAMETERS"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::INVALID_PARAMETERS, status_); +} + +TEST_F(UnregistrationRequestTest, UnkwnownError) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "Error=XXX"); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::UNKNOWN_ERROR, status_); +} + +TEST_F(UnregistrationRequestTest, ServiceUnavailable) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_SERVICE_UNAVAILABLE, ""); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::SUCCESS, status_); +} + +TEST_F(UnregistrationRequestTest, InternalServerError) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_INTERNAL_SERVER_ERROR, ""); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::SUCCESS, status_); +} + +TEST_F(UnregistrationRequestTest, IncorrectAppId) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "deleted=OtherTestAppId"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::SUCCESS, status_); +} + +TEST_F(UnregistrationRequestTest, ResponseParsingFailed) { + CreateRequest(); + request_->Start(); + + SetResponseStatusAndString(net::HTTP_OK, "some malformed response"); + CompleteFetch(); + + EXPECT_FALSE(callback_called_); + + SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId); + CompleteFetch(); + + EXPECT_TRUE(callback_called_); + EXPECT_EQ(UnregistrationRequest::SUCCESS, status_); +} + +} // namespace gcm |