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 | |
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')
149 files changed, 14004 insertions, 6792 deletions
diff --git a/chromium/google_apis/BUILD.gn b/chromium/google_apis/BUILD.gn new file mode 100644 index 00000000000..6dbeab10039 --- /dev/null +++ b/chromium/google_apis/BUILD.gn @@ -0,0 +1,246 @@ +# 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. + +import("//build/config/crypto.gni") + +declare_args() { + # You can set the variable 'use_official_google_api_keys' to true + # to use the Google-internal file containing official API keys + # for Google Chrome even in a developer build. Setting this + # variable explicitly to true will cause your build to fail if the + # internal file is missing. + # + # The variable is documented here, but not handled in this file; + # see //google_apis/determine_use_official_keys.gypi for the + # implementation. + # + # Set the variable to false to not use the internal file, even when + # it exists in your checkout. + # + # Leave it unset or set to "" to have the variable + # implicitly set to true if you have + # src/google_apis/internal/google_chrome_api_keys.h in your + # checkout, and implicitly set to false if not. + # + # Note that official builds always behave as if the variable + # was explicitly set to true, i.e. they always use official keys, + # and will fail to build if the internal file is missing. + use_official_google_api_keys = "" + + # Set these to bake the specified API keys and OAuth client + # IDs/secrets into your build. + # + # If you create a build without values baked in, you can instead + # set environment variables to provide the keys at runtime (see + # src/google_apis/google_api_keys.h for details). Features that + # require server-side APIs may fail to work if no keys are + # provided. + # + # Note that if you are building an official build or if + # use_official_google_api_keys has been set to trie (explicitly or + # implicitly), these values will be ignored and the official + # keys will be used instead. + google_api_key = "" + + # See google_api_key. + google_default_client_id = "" + + # See google_api_key. + google_default_client_secret = "" +} + +if (use_official_google_api_keys == "") { + # Default behavior, check if the key file exists. + check_internal_result = exec_script( + "build/check_internal.py", + [ rebase_path("internal/google_chrome_api_keys.h", root_build_dir) ], + "value") + use_official_google_api_keys = (check_internal_result == 1) +} + +config("key_defines") { + defines = [] + if (use_official_google_api_keys) { + defines += [ "USE_OFFICIAL_GOOGLE_API_KEYS=1" ] + } + if (google_api_key != "") { + defines += [ "GOOGLE_API_KEY=$google_api_key" ] + } + if (google_default_client_id != "") { + defines += [ "GOOGLE_DEFAULT_CLIENT_ID=$google_default_client_id" ] + } + if (google_default_client_secret != "") { + defines += [ "GOOGLE_DEFAULT_CLIENT_SECRET=$google_default_client_secret" ] + } +} + +source_set("google_apis") { + sources = [ + "cup/client_update_protocol.cc", + "cup/client_update_protocol.h", + "drive/auth_service.cc", + "drive/auth_service.h", + "drive/auth_service_interface.h", + "drive/auth_service_observer.h", + "drive/base_requests.cc", + "drive/base_requests.h", + "drive/drive_api_parser.cc", + "drive/drive_api_parser.h", + "drive/drive_api_requests.cc", + "drive/drive_api_requests.h", + "drive/drive_api_url_generator.cc", + "drive/drive_api_url_generator.h", + "drive/drive_common_callbacks.h", + "drive/drive_entry_kinds.h", + "drive/gdata_errorcode.cc", + "drive/gdata_errorcode.h", + "drive/gdata_wapi_requests.cc", + "drive/gdata_wapi_requests.h", + "drive/gdata_wapi_parser.cc", + "drive/gdata_wapi_parser.h", + "drive/gdata_wapi_url_generator.cc", + "drive/gdata_wapi_url_generator.h", + "drive/request_sender.cc", + "drive/request_sender.h", + "drive/request_util.cc", + "drive/request_util.h", + "drive/task_util.cc", + "drive/task_util.h", + "drive/time_util.cc", + "drive/time_util.h", + "gaia/gaia_auth_consumer.cc", + "gaia/gaia_auth_consumer.h", + "gaia/gaia_auth_fetcher.cc", + "gaia/gaia_auth_fetcher.h", + "gaia/gaia_auth_util.cc", + "gaia/gaia_auth_util.h", + "gaia/gaia_constants.cc", + "gaia/gaia_constants.h", + "gaia/gaia_oauth_client.cc", + "gaia/gaia_oauth_client.h", + "gaia/gaia_switches.cc", + "gaia/gaia_switches.h", + "gaia/gaia_urls.cc", + "gaia/gaia_urls.h", + "gaia/google_service_auth_error.cc", + "gaia/google_service_auth_error.h", + "gaia/identity_provider.cc", + "gaia/identity_provider.h", + "gaia/merge_session_helper.cc", + "gaia/merge_session_helper.h", + "gaia/oauth_request_signer.cc", + "gaia/oauth_request_signer.h", + "gaia/oauth2_access_token_consumer.h", + "gaia/oauth2_access_token_fetcher.h", + "gaia/oauth2_access_token_fetcher.cc", + "gaia/oauth2_access_token_fetcher_impl.cc", + "gaia/oauth2_access_token_fetcher_impl.h", + "gaia/oauth2_api_call_flow.cc", + "gaia/oauth2_api_call_flow.h", + "gaia/oauth2_mint_token_flow.cc", + "gaia/oauth2_mint_token_flow.h", + "gaia/oauth2_token_service.cc", + "gaia/oauth2_token_service.h", + "gaia/ubertoken_fetcher.cc", + "gaia/ubertoken_fetcher.h", + "google_api_keys.cc", + "google_api_keys.h", + ] + + if (is_win) { + cflags = [ "/wd4267" ] # size_t -> int + } + + configs += [ ":key_defines" ] + + deps = [ + "//base", + "//crypto", + "//crypto:platform", + "//net", + "//third_party/libxml", + ] + + if (use_openssl) { + sources += [ "cup/client_update_protocol_openssl.cc" ] + } else { + sources += [ "cup/client_update_protocol_nss.cc" ] + } +} + +source_set("test_support") { + sources = [ + "drive/dummy_auth_service.cc", + "drive/dummy_auth_service.h", + "drive/test_util.cc", + "drive/test_util.h", + "gaia/fake_gaia.cc", + "gaia/fake_gaia.h", + "gaia/fake_identity_provider.cc", + "gaia/fake_identity_provider.h", + "gaia/fake_oauth2_token_service.cc", + "gaia/fake_oauth2_token_service.h", + "gaia/mock_url_fetcher_factory.h", + "gaia/oauth2_token_service_test_util.cc", + "gaia/oauth2_token_service_test_util.h", + ] + + deps = [ + "//base", + "//base/test:test_support", + "//net", + "//net:test_support", + ] + forward_dependent_configs_from = deps +} + +test("google_apis_unittest") { + sources = [ + "google_api_keys_unittest.cc", + "cup/client_update_protocol_unittest.cc", + "drive/base_requests_unittest.cc", + "drive/base_requests_server_unittest.cc", + "drive/drive_api_requests_unittest.cc", + "drive/drive_api_parser_unittest.cc", + "drive/drive_api_url_generator_unittest.cc", + "drive/gdata_wapi_parser_unittest.cc", + "drive/gdata_wapi_requests_unittest.cc", + "drive/gdata_wapi_url_generator_unittest.cc", + "drive/request_sender_unittest.cc", + "drive/request_util_unittest.cc", + "drive/time_util_unittest.cc", + "gaia/gaia_auth_fetcher_unittest.cc", + "gaia/gaia_auth_util_unittest.cc", + "gaia/gaia_oauth_client_unittest.cc", + "gaia/google_service_auth_error_unittest.cc", + "gaia/merge_session_helper_unittest.cc", + "gaia/oauth_request_signer_unittest.cc", + "gaia/oauth2_access_token_fetcher_impl_unittest.cc", + "gaia/oauth2_api_call_flow_unittest.cc", + "gaia/oauth2_mint_token_flow_unittest.cc", + "gaia/oauth2_token_service_unittest.cc", + "gaia/ubertoken_fetcher_unittest.cc", + ] + + configs += [ ":key_defines" ] + + deps = [ + ":google_apis", + ":test_support", + "//base", + "//base/test:run_all_unittests", + "//testing/gmock", + "//testing/gtest", + ] + + if (is_android) { + sources -= [ + "drive/base_requests_server_unittest.cc", + "drive/drive_api_parser_unittest.cc", + "drive/drive_api_requests_unittest.cc", + "drive/gdata_wapi_parser_unittest.cc", + "drive/gdata_wapi_requests_unittest.cc", + ] + } +} diff --git a/chromium/google_apis/OWNERS b/chromium/google_apis/OWNERS index e3221f4c57c..258354a3415 100644 --- a/chromium/google_apis/OWNERS +++ b/chromium/google_apis/OWNERS @@ -1,2 +1 @@ -joi@chromium.org rogerta@chromium.org diff --git a/chromium/google_apis/cup/client_update_protocol_nss.cc b/chromium/google_apis/cup/client_update_protocol_nss.cc index b369c13d154..1d3ff24d4f7 100644 --- a/chromium/google_apis/cup/client_update_protocol_nss.cc +++ b/chromium/google_apis/cup/client_update_protocol_nss.cc @@ -12,10 +12,9 @@ #include "crypto/nss_util.h" #include "crypto/scoped_nss_types.h" -typedef scoped_ptr_malloc< - CERTSubjectPublicKeyInfo, - crypto::NSSDestroyer<CERTSubjectPublicKeyInfo, - SECKEY_DestroySubjectPublicKeyInfo> > +typedef scoped_ptr<CERTSubjectPublicKeyInfo, + crypto::NSSDestroyer<CERTSubjectPublicKeyInfo, + SECKEY_DestroySubjectPublicKeyInfo> > ScopedCERTSubjectPublicKeyInfo; ClientUpdateProtocol::~ClientUpdateProtocol() { diff --git a/chromium/google_apis/drive/auth_service.cc b/chromium/google_apis/drive/auth_service.cc index 18623686f94..51229f92b6a 100644 --- a/chromium/google_apis/drive/auth_service.cc +++ b/chromium/google_apis/drive/auth_service.cc @@ -58,7 +58,8 @@ AuthRequest::AuthRequest( net::URLRequestContextGetter* url_request_context_getter, const AuthStatusCallback& callback, const std::vector<std::string>& scopes) - : callback_(callback) { + : OAuth2TokenService::Consumer("auth_service"), + callback_(callback) { DCHECK(!callback_.is_null()); request_ = oauth2_token_service-> StartRequestWithContext( diff --git a/chromium/google_apis/drive/base_requests.cc b/chromium/google_apis/drive/base_requests.cc index be64f783c24..0c1a238057e 100644 --- a/chromium/google_apis/drive/base_requests.cc +++ b/chromium/google_apis/drive/base_requests.cc @@ -358,6 +358,7 @@ void UrlFetchRequestBase::OnURLFetchComplete(const URLFetcher* source) { const char kErrorMessageKey[] = "message"; const char kErrorReasonRateLimitExceeded[] = "rateLimitExceeded"; const char kErrorReasonUserRateLimitExceeded[] = "userRateLimitExceeded"; + const char kErrorReasonQuotaExceeded[] = "quotaExceeded"; scoped_ptr<base::Value> value(ParseJsonInternal(response_writer_->data())); base::DictionaryValue* dictionary = NULL; @@ -380,6 +381,8 @@ void UrlFetchRequestBase::OnURLFetchComplete(const URLFetcher* source) { if (reason == kErrorReasonRateLimitExceeded || reason == kErrorReasonUserRateLimitExceeded) error_code_ = HTTP_SERVICE_UNAVAILABLE; + if (reason == kErrorReasonQuotaExceeded) + error_code_ = GDATA_NO_SPACE; } } } diff --git a/chromium/google_apis/drive/base_requests_unittest.cc b/chromium/google_apis/drive/base_requests_unittest.cc index 3032d2abb37..f3a97c19156 100644 --- a/chromium/google_apis/drive/base_requests_unittest.cc +++ b/chromium/google_apis/drive/base_requests_unittest.cc @@ -115,7 +115,7 @@ TEST_F(BaseRequestsTest, ParseValidJson) { base::Bind(test_util::CreateCopyResultCallback(&json))); base::RunLoop().RunUntilIdle(); - DictionaryValue* root_dict = NULL; + base::DictionaryValue* root_dict = NULL; ASSERT_TRUE(json); ASSERT_TRUE(json->GetAsDictionary(&root_dict)); diff --git a/chromium/google_apis/drive/drive_api_parser.cc b/chromium/google_apis/drive/drive_api_parser.cc index 4b05e8b5844..c22070eb458 100644 --- a/chromium/google_apis/drive/drive_api_parser.cc +++ b/chromium/google_apis/drive/drive_api_parser.cc @@ -38,6 +38,28 @@ bool GetGURLFromString(const base::StringPiece& url_string, GURL* result) { return true; } +// Converts |value| to |result|. +bool GetParentsFromValue(const base::Value* value, + std::vector<ParentReference>* result) { + DCHECK(value); + DCHECK(result); + + const base::ListValue* list_value = NULL; + if (!value->GetAsList(&list_value)) + return false; + + base::JSONValueConverter<ParentReference> converter; + result->resize(list_value->GetSize()); + for (size_t i = 0; i < list_value->GetSize(); ++i) { + const base::Value* parent_value = NULL; + if (!list_value->Get(i, &parent_value) || + !converter.Convert(*parent_value, &(*result)[i])) + return false; + } + + return true; +} + // Converts |value| to |result|. The key of |value| is app_id, and its value // is URL to open the resource on the web app. bool GetOpenWithLinksFromDictionaryValue( @@ -75,7 +97,6 @@ bool GetOpenWithLinksFromDictionaryValue( const char kKind[] = "kind"; const char kId[] = "id"; const char kETag[] = "etag"; -const char kSelfLink[] = "selfLink"; const char kItems[] = "items"; const char kLargestChangeId[] = "largestChangeId"; @@ -97,16 +118,15 @@ const char kIconUrl[] = "iconUrl"; const char kAppKind[] = "drive#app"; const char kName[] = "name"; const char kObjectType[] = "objectType"; +const char kProductId[] = "productId"; const char kSupportsCreate[] = "supportsCreate"; -const char kSupportsImport[] = "supportsImport"; -const char kInstalled[] = "installed"; -const char kAuthorized[] = "authorized"; -const char kProductUrl[] = "productUrl"; +const char kRemovable[] = "removable"; const char kPrimaryMimeTypes[] = "primaryMimeTypes"; const char kSecondaryMimeTypes[] = "secondaryMimeTypes"; const char kPrimaryFileExtensions[] = "primaryFileExtensions"; const char kSecondaryFileExtensions[] = "secondaryFileExtensions"; const char kIcons[] = "icons"; +const char kCreateUrl[] = "createUrl"; // Apps List // https://developers.google.com/drive/v2/reference/apps/list @@ -116,7 +136,6 @@ const char kAppListKind[] = "drive#appList"; // https://developers.google.com/drive/v2/reference/parents const char kParentReferenceKind[] = "drive#parentReference"; const char kParentLink[] = "parentLink"; -const char kIsRoot[] = "isRoot"; // File Resource // https://developers.google.com/drive/v2/reference/files @@ -124,29 +143,20 @@ const char kFileKind[] = "drive#file"; const char kTitle[] = "title"; const char kMimeType[] = "mimeType"; const char kCreatedDate[] = "createdDate"; +const char kModificationDate[] = "modificationDate"; const char kModifiedDate[] = "modifiedDate"; -const char kModifiedByMeDate[] = "modifiedByMeDate"; const char kLastViewedByMeDate[] = "lastViewedByMeDate"; const char kSharedWithMeDate[] = "sharedWithMeDate"; -const char kDownloadUrl[] = "downloadUrl"; -const char kFileExtension[] = "fileExtension"; const char kMd5Checksum[] = "md5Checksum"; const char kFileSize[] = "fileSize"; const char kAlternateLink[] = "alternateLink"; -const char kEmbedLink[] = "embedLink"; const char kParents[] = "parents"; -const char kThumbnailLink[] = "thumbnailLink"; -const char kWebContentLink[] = "webContentLink"; const char kOpenWithLinks[] = "openWithLinks"; const char kLabels[] = "labels"; const char kImageMediaMetadata[] = "imageMediaMetadata"; const char kShared[] = "shared"; // These 5 flags are defined under |labels|. -const char kLabelStarred[] = "starred"; -const char kLabelHidden[] = "hidden"; const char kLabelTrashed[] = "trashed"; -const char kLabelRestricted[] = "restricted"; -const char kLabelViewed[] = "viewed"; // These 3 flags are defined under |imageMediaMetadata|. const char kImageMediaMetadataWidth[] = "width"; const char kImageMediaMetadataHeight[] = "height"; @@ -157,7 +167,6 @@ const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder"; // Files List // https://developers.google.com/drive/v2/reference/files/list const char kFileListKind[] = "drive#fileList"; -const char kNextPageToken[] = "nextPageToken"; const char kNextLink[] = "nextLink"; // Change Resource @@ -299,9 +308,7 @@ bool DriveAppIcon::GetIconCategory(const base::StringPiece& category, AppResource::AppResource() : supports_create_(false), - supports_import_(false), - installed_(false), - authorized_(false) { + removable_(false) { } AppResource::~AppResource() {} @@ -312,13 +319,9 @@ void AppResource::RegisterJSONConverter( converter->RegisterStringField(kId, &AppResource::application_id_); converter->RegisterStringField(kName, &AppResource::name_); converter->RegisterStringField(kObjectType, &AppResource::object_type_); + converter->RegisterStringField(kProductId, &AppResource::product_id_); converter->RegisterBoolField(kSupportsCreate, &AppResource::supports_create_); - converter->RegisterBoolField(kSupportsImport, &AppResource::supports_import_); - converter->RegisterBoolField(kInstalled, &AppResource::installed_); - converter->RegisterBoolField(kAuthorized, &AppResource::authorized_); - converter->RegisterCustomField<GURL>(kProductUrl, - &AppResource::product_url_, - GetGURLFromString); + converter->RegisterBoolField(kRemovable, &AppResource::removable_); converter->RegisterRepeatedString(kPrimaryMimeTypes, &AppResource::primary_mimetypes_); converter->RegisterRepeatedString(kSecondaryMimeTypes, @@ -328,6 +331,9 @@ void AppResource::RegisterJSONConverter( converter->RegisterRepeatedString(kSecondaryFileExtensions, &AppResource::secondary_file_extensions_); converter->RegisterRepeatedMessage(kIcons, &AppResource::icons_); + converter->RegisterCustomField<GURL>(kCreateUrl, + &AppResource::create_url_, + GetGURLFromString); } // static @@ -386,7 +392,7 @@ bool AppList::Parse(const base::Value& value) { //////////////////////////////////////////////////////////////////////////////// // ParentReference implementation -ParentReference::ParentReference() : is_root_(false) {} +ParentReference::ParentReference() {} ParentReference::~ParentReference() {} @@ -397,7 +403,6 @@ void ParentReference::RegisterJSONConverter( converter->RegisterCustomField<GURL>(kParentLink, &ParentReference::parent_link_, GetGURLFromString); - converter->RegisterBoolField(kIsRoot, &ParentReference::is_root_); } // static @@ -433,9 +438,6 @@ void FileResource::RegisterJSONConverter( base::JSONValueConverter<FileResource>* converter) { converter->RegisterStringField(kId, &FileResource::file_id_); converter->RegisterStringField(kETag, &FileResource::etag_); - converter->RegisterCustomField<GURL>(kSelfLink, - &FileResource::self_link_, - GetGURLFromString); converter->RegisterStringField(kTitle, &FileResource::title_); converter->RegisterStringField(kMimeType, &FileResource::mime_type_); converter->RegisterNestedField(kLabels, &FileResource::labels_); @@ -450,10 +452,6 @@ void FileResource::RegisterJSONConverter( &FileResource::modified_date_, &util::GetTimeFromString); converter->RegisterCustomField<base::Time>( - kModifiedByMeDate, - &FileResource::modified_by_me_date_, - &util::GetTimeFromString); - converter->RegisterCustomField<base::Time>( kLastViewedByMeDate, &FileResource::last_viewed_by_me_date_, &util::GetTimeFromString); @@ -462,11 +460,6 @@ void FileResource::RegisterJSONConverter( &FileResource::shared_with_me_date_, &util::GetTimeFromString); converter->RegisterBoolField(kShared, &FileResource::shared_); - converter->RegisterCustomField<GURL>(kDownloadUrl, - &FileResource::download_url_, - GetGURLFromString); - converter->RegisterStringField(kFileExtension, - &FileResource::file_extension_); converter->RegisterStringField(kMd5Checksum, &FileResource::md5_checksum_); converter->RegisterCustomField<int64>(kFileSize, &FileResource::file_size_, @@ -474,17 +467,10 @@ void FileResource::RegisterJSONConverter( converter->RegisterCustomField<GURL>(kAlternateLink, &FileResource::alternate_link_, GetGURLFromString); - converter->RegisterCustomField<GURL>(kEmbedLink, - &FileResource::embed_link_, - GetGURLFromString); - converter->RegisterRepeatedMessage<ParentReference>(kParents, - &FileResource::parents_); - converter->RegisterCustomField<GURL>(kThumbnailLink, - &FileResource::thumbnail_link_, - GetGURLFromString); - converter->RegisterCustomField<GURL>(kWebContentLink, - &FileResource::web_content_link_, - GetGURLFromString); + converter->RegisterCustomValueField<std::vector<ParentReference> >( + kParents, + &FileResource::parents_, + GetParentsFromValue); converter->RegisterCustomValueField<std::vector<OpenWithLink> >( kOpenWithLinks, &FileResource::open_with_links_, @@ -524,8 +510,6 @@ FileList::~FileList() {} // static void FileList::RegisterJSONConverter( base::JSONValueConverter<FileList>* converter) { - converter->RegisterStringField(kETag, &FileList::etag_); - converter->RegisterStringField(kNextPageToken, &FileList::next_page_token_); converter->RegisterCustomField<GURL>(kNextLink, &FileList::next_link_, GetGURLFromString); @@ -574,6 +558,9 @@ void ChangeResource::RegisterJSONConverter( converter->RegisterBoolField(kDeleted, &ChangeResource::deleted_); converter->RegisterCustomValueField(kFile, &ChangeResource::file_, &CreateFileResourceFromValue); + converter->RegisterCustomField<base::Time>( + kModificationDate, &ChangeResource::modification_date_, + &util::GetTimeFromString); } // static @@ -606,8 +593,6 @@ ChangeList::~ChangeList() {} // static void ChangeList::RegisterJSONConverter( base::JSONValueConverter<ChangeList>* converter) { - converter->RegisterStringField(kETag, &ChangeList::etag_); - converter->RegisterStringField(kNextPageToken, &ChangeList::next_page_token_); converter->RegisterCustomField<GURL>(kNextLink, &ChangeList::next_link_, GetGURLFromString); @@ -646,23 +631,14 @@ bool ChangeList::Parse(const base::Value& value) { //////////////////////////////////////////////////////////////////////////////// // FileLabels implementation -FileLabels::FileLabels() - : starred_(false), - hidden_(false), - trashed_(false), - restricted_(false), - viewed_(false) {} +FileLabels::FileLabels() : trashed_(false) {} FileLabels::~FileLabels() {} // static void FileLabels::RegisterJSONConverter( base::JSONValueConverter<FileLabels>* converter) { - converter->RegisterBoolField(kLabelStarred, &FileLabels::starred_); - converter->RegisterBoolField(kLabelHidden, &FileLabels::hidden_); converter->RegisterBoolField(kLabelTrashed, &FileLabels::trashed_); - converter->RegisterBoolField(kLabelRestricted, &FileLabels::restricted_); - converter->RegisterBoolField(kLabelViewed, &FileLabels::viewed_); } // static @@ -717,7 +693,6 @@ scoped_ptr<ImageMediaMetadata> ImageMediaMetadata::CreateFrom( } bool ImageMediaMetadata::Parse(const base::Value& value) { - return true; base::JSONValueConverter<ImageMediaMetadata> converter; if (!converter.Convert(value, this)) { LOG(ERROR) << "Unable to parse: Invalid ImageMediaMetadata."; diff --git a/chromium/google_apis/drive/drive_api_parser.h b/chromium/google_apis/drive/drive_api_parser.h index ef02035d595..babb4cdc5c1 100644 --- a/chromium/google_apis/drive/drive_api_parser.h +++ b/chromium/google_apis/drive/drive_api_parser.h @@ -168,21 +168,17 @@ class AppResource { // If empty, application name is used instead. const std::string& object_type() const { return object_type_; } + // Returns the product ID. + const std::string& product_id() const { return product_id_; } + // Returns whether this application supports creating new objects. bool supports_create() const { return supports_create_; } - // Returns whether this application supports importing Google Docs. - bool supports_import() const { return supports_import_; } - - // Returns whether this application is installed. - bool is_installed() const { return installed_; } - - // Returns whether this application is authorized to access data on the - // user's Drive. - bool is_authorized() const { return authorized_; } + // Returns whether this application is removable by apps.delete API. + bool is_removable() const { return removable_; } - // Returns the product URL, e.g. at Chrome Web Store. - const GURL& product_url() const { return product_url_; } + // Returns the create URL, i.e., the URL for opening a new file by the app. + const GURL& create_url() const { return create_url_; } // List of primary mime types supported by this WebApp. Primary status should // trigger this WebApp becoming the default handler of file instances that @@ -226,17 +222,11 @@ class AppResource { void set_object_type(const std::string& object_type) { object_type_ = object_type; } + void set_product_id(const std::string& id) { product_id_ = id; } void set_supports_create(bool supports_create) { supports_create_ = supports_create; } - void set_supports_import(bool supports_import) { - supports_import_ = supports_import; - } - void set_installed(bool installed) { installed_ = installed; } - void set_authorized(bool authorized) { authorized_ = authorized; } - void set_product_url(const GURL& product_url) { - product_url_ = product_url; - } + void set_removable(bool removable) { removable_ = removable; } void set_primary_mimetypes( ScopedVector<std::string> primary_mimetypes) { primary_mimetypes_ = primary_mimetypes.Pass(); @@ -256,6 +246,9 @@ class AppResource { void set_icons(ScopedVector<DriveAppIcon> icons) { icons_ = icons.Pass(); } + void set_create_url(const GURL& url) { + create_url_ = url; + } private: friend class base::internal::RepeatedMessageConverter<AppResource>; @@ -268,11 +261,10 @@ class AppResource { std::string application_id_; std::string name_; std::string object_type_; + std::string product_id_; bool supports_create_; - bool supports_import_; - bool installed_; - bool authorized_; - GURL product_url_; + bool removable_; + GURL create_url_; ScopedVector<std::string> primary_mimetypes_; ScopedVector<std::string> secondary_mimetypes_; ScopedVector<std::string> primary_file_extensions_; @@ -345,27 +337,18 @@ class ParentReference { // Returns the URL for the parent in Drive. const GURL& parent_link() const { return parent_link_; } - // Returns true if the reference is root directory. - bool is_root() const { return is_root_; } - void set_file_id(const std::string& file_id) { file_id_ = file_id; } void set_parent_link(const GURL& parent_link) { parent_link_ = parent_link; } - void set_is_root(bool is_root) { is_root_ = is_root; } private: - friend class base::internal::RepeatedMessageConverter<ParentReference>; - // Parses and initializes data members from content of |value|. // Return false if parsing fails. bool Parse(const base::Value& value); std::string file_id_; GURL parent_link_; - bool is_root_; - - DISALLOW_COPY_AND_ASSIGN(ParentReference); }; // FileLabels represents labels for file or folder. @@ -383,22 +366,10 @@ class FileLabels { // Creates about resource from parsed JSON. static scoped_ptr<FileLabels> CreateFrom(const base::Value& value); - // Whether this file is starred by the user. - bool is_starred() const { return starred_; } - // Whether this file is hidden from the user. - bool is_hidden() const { return hidden_; } // Whether this file has been trashed. bool is_trashed() const { return trashed_; } - // Whether viewers are prevented from downloading this file. - bool is_restricted() const { return restricted_; } - // Whether this file has been viewed by this user. - bool is_viewed() const { return viewed_; } - void set_starred(bool starred) { starred_ = starred; } - void set_hidden(bool hidden) { hidden_ = hidden; } void set_trashed(bool trashed) { trashed_ = trashed; } - void set_restricted(bool restricted) { restricted_ = restricted; } - void set_viewed(bool viewed) { viewed_ = viewed; } private: friend class FileResource; @@ -407,13 +378,7 @@ class FileLabels { // Return false if parsing fails. bool Parse(const base::Value& value); - bool starred_; - bool hidden_; bool trashed_; - bool restricted_; - bool viewed_; - - DISALLOW_COPY_AND_ASSIGN(FileLabels); }; // ImageMediaMetadata represents image metadata for a file. @@ -452,8 +417,6 @@ class ImageMediaMetadata { int width_; int height_; int rotation_; - - DISALLOW_COPY_AND_ASSIGN(ImageMediaMetadata); }; @@ -489,9 +452,6 @@ class FileResource { // Returns ETag for this file. const std::string& etag() const { return etag_; } - // Returns the link to JSON of this file itself. - const GURL& self_link() const { return self_link_; } - // Returns the title of this file. const std::string& title() const { return title_; } @@ -512,9 +472,6 @@ class FileResource { // Returns modified time of this file. const base::Time& modified_date() const { return modified_date_; } - // Returns modification time by the user. - const base::Time& modified_by_me_date() const { return modified_by_me_date_; } - // Returns last access time by the user. const base::Time& last_viewed_by_me_date() const { return last_viewed_by_me_date_; @@ -528,13 +485,6 @@ class FileResource { // Returns the 'shared' attribute of the file. bool shared() const { return shared_; } - // Returns the short-lived download URL for the file. This field exists - // only when the file content is stored in Drive. - const GURL& download_url() const { return download_url_; } - - // Returns the extension part of the filename. - const std::string& file_extension() const { return file_extension_; } - // Returns MD5 checksum of this file. const std::string& md5_checksum() const { return md5_checksum_; } @@ -545,18 +495,8 @@ class FileResource { // E.g. Google Document, Google Spreadsheet. const GURL& alternate_link() const { return alternate_link_; } - // Returns the link for embedding the file. - const GURL& embed_link() const { return embed_link_; } - // Returns parent references (directories) of this file. - const ScopedVector<ParentReference>& parents() const { return parents_; } - - // Returns the link to the file's thumbnail. - const GURL& thumbnail_link() const { return thumbnail_link_; } - - // Returns the link to open its downloadable content, using cookie based - // authentication. - const GURL& web_content_link() const { return web_content_link_; } + const std::vector<ParentReference>& parents() const { return parents_; } // Returns the list of links to open the resource with a web app. const std::vector<OpenWithLink>& open_with_links() const { @@ -569,9 +509,6 @@ class FileResource { void set_etag(const std::string& etag) { etag_ = etag; } - void set_self_link(const GURL& self_link) { - self_link_ = self_link; - } void set_title(const std::string& title) { title_ = title; } @@ -590,9 +527,6 @@ class FileResource { void set_modified_date(const base::Time& modified_date) { modified_date_ = modified_date; } - void set_modified_by_me_date(const base::Time& modified_by_me_date) { - modified_by_me_date_ = modified_by_me_date; - } void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) { last_viewed_by_me_date_ = last_viewed_by_me_date; } @@ -602,12 +536,6 @@ class FileResource { void set_shared(bool shared) { shared_ = shared; } - void set_download_url(const GURL& download_url) { - download_url_ = download_url; - } - void set_file_extension(const std::string& file_extension) { - file_extension_ = file_extension; - } void set_md5_checksum(const std::string& md5_checksum) { md5_checksum_ = md5_checksum; } @@ -617,17 +545,9 @@ class FileResource { void set_alternate_link(const GURL& alternate_link) { alternate_link_ = alternate_link; } - void set_embed_link(const GURL& embed_link) { - embed_link_ = embed_link; - } - void set_parents(ScopedVector<ParentReference> parents) { - parents_ = parents.Pass(); - } - void set_thumbnail_link(const GURL& thumbnail_link) { - thumbnail_link_ = thumbnail_link; - } - void set_web_content_link(const GURL& web_content_link) { - web_content_link_ = web_content_link; + std::vector<ParentReference>* mutable_parents() { return &parents_; } + std::vector<OpenWithLink>* mutable_open_with_links() { + return &open_with_links_; } private: @@ -641,29 +561,20 @@ class FileResource { std::string file_id_; std::string etag_; - GURL self_link_; std::string title_; std::string mime_type_; FileLabels labels_; ImageMediaMetadata image_media_metadata_; base::Time created_date_; base::Time modified_date_; - base::Time modified_by_me_date_; base::Time last_viewed_by_me_date_; base::Time shared_with_me_date_; bool shared_; - GURL download_url_; - std::string file_extension_; std::string md5_checksum_; int64 file_size_; GURL alternate_link_; - GURL embed_link_; - ScopedVector<ParentReference> parents_; - GURL thumbnail_link_; - GURL web_content_link_; + std::vector<ParentReference> parents_; std::vector<OpenWithLink> open_with_links_; - - DISALLOW_COPY_AND_ASSIGN(FileResource); }; // FileList represents a collection of files and folders. @@ -684,32 +595,17 @@ class FileList { // Creates file list from parsed JSON. static scoped_ptr<FileList> CreateFrom(const base::Value& value); - // Returns the ETag of the list. - const std::string& etag() const { return etag_; } - - // Returns the page token for the next page of files, if the list is large - // to fit in one response. If this is empty, there is no more file lists. - const std::string& next_page_token() const { return next_page_token_; } - // Returns a link to the next page of files. The URL includes the next page // token. const GURL& next_link() const { return next_link_; } // Returns a set of files in this list. const ScopedVector<FileResource>& items() const { return items_; } + ScopedVector<FileResource>* mutable_items() { return &items_; } - void set_etag(const std::string& etag) { - etag_ = etag; - } - void set_next_page_token(const std::string& next_page_token) { - next_page_token_ = next_page_token; - } void set_next_link(const GURL& next_link) { next_link_ = next_link; } - void set_items(ScopedVector<FileResource> items) { - items_ = items.Pass(); - } private: friend class DriveAPIParserTest; @@ -719,8 +615,6 @@ class FileList { // Return false if parsing fails. bool Parse(const base::Value& value); - std::string etag_; - std::string next_page_token_; GURL next_link_; ScopedVector<FileResource> items_; @@ -754,6 +648,10 @@ class ChangeResource { // Returns FileResource of the file which the change refers to. const FileResource* file() const { return file_.get(); } + FileResource* mutable_file() { return file_.get(); } + + // Returns the time of this modification. + const base::Time& modification_date() const { return modification_date_; } void set_change_id(int64 change_id) { change_id_ = change_id; @@ -767,6 +665,9 @@ class ChangeResource { void set_file(scoped_ptr<FileResource> file) { file_ = file.Pass(); } + void set_modification_date(const base::Time& modification_date) { + modification_date_ = modification_date; + } private: friend class base::internal::RepeatedMessageConverter<ChangeResource>; @@ -780,6 +681,7 @@ class ChangeResource { std::string file_id_; bool deleted_; scoped_ptr<FileResource> file_; + base::Time modification_date_; DISALLOW_COPY_AND_ASSIGN(ChangeResource); }; @@ -802,13 +704,6 @@ class ChangeList { // Creates change list from parsed JSON. static scoped_ptr<ChangeList> CreateFrom(const base::Value& value); - // Returns the ETag of the list. - const std::string& etag() const { return etag_; } - - // Returns the page token for the next page of files, if the list is large - // to fit in one response. If this is empty, there is no more file lists. - const std::string& next_page_token() const { return next_page_token_; } - // Returns a link to the next page of files. The URL includes the next page // token. const GURL& next_link() const { return next_link_; } @@ -818,22 +713,14 @@ class ChangeList { // Returns a set of changes in this list. const ScopedVector<ChangeResource>& items() const { return items_; } + ScopedVector<ChangeResource>* mutable_items() { return &items_; } - void set_etag(const std::string& etag) { - etag_ = etag; - } - void set_next_page_token(const std::string& next_page_token) { - next_page_token_ = next_page_token; - } void set_next_link(const GURL& next_link) { next_link_ = next_link; } void set_largest_change_id(int64 largest_change_id) { largest_change_id_ = largest_change_id; } - void set_items(ScopedVector<ChangeResource> items) { - items_ = items.Pass(); - } private: friend class DriveAPIParserTest; @@ -843,8 +730,6 @@ class ChangeList { // Return false if parsing fails. bool Parse(const base::Value& value); - std::string etag_; - std::string next_page_token_; GURL next_link_; int64 largest_change_id_; ScopedVector<ChangeResource> items_; diff --git a/chromium/google_apis/drive/drive_api_parser_unittest.cc b/chromium/google_apis/drive/drive_api_parser_unittest.cc index 0960b275bf5..6f9ee65e109 100644 --- a/chromium/google_apis/drive/drive_api_parser_unittest.cc +++ b/chromium/google_apis/drive/drive_api_parser_unittest.cc @@ -50,12 +50,8 @@ TEST(DriveAPIParserTest, AppListParser) { EXPECT_EQ("Drive app 1", app1.name()); EXPECT_EQ("", app1.object_type()); EXPECT_TRUE(app1.supports_create()); - EXPECT_TRUE(app1.supports_import()); - EXPECT_TRUE(app1.is_installed()); - EXPECT_FALSE(app1.is_authorized()); - EXPECT_EQ("https://chrome.google.com/webstore/detail/" - "abcdefghabcdefghabcdefghabcdefgh", - app1.product_url().spec()); + EXPECT_TRUE(app1.is_removable()); + EXPECT_EQ("abcdefghabcdefghabcdefghabcdefgh", app1.product_id()); ASSERT_EQ(1U, app1.primary_mimetypes().size()); EXPECT_EQ("application/vnd.google-apps.drive-sdk.123456788192", @@ -82,18 +78,16 @@ TEST(DriveAPIParserTest, AppListParser) { EXPECT_EQ(16, icon6.icon_side_length()); EXPECT_EQ("http://www.example.com/ds16.png", icon6.icon_url().spec()); + EXPECT_EQ("https://www.example.com/createForApp1", app1.create_url().spec()); + // Check Drive app 2 const AppResource& app2 = *applist->items()[1]; EXPECT_EQ("876543210000", app2.application_id()); EXPECT_EQ("Drive app 2", app2.name()); EXPECT_EQ("", app2.object_type()); EXPECT_FALSE(app2.supports_create()); - EXPECT_FALSE(app2.supports_import()); - EXPECT_TRUE(app2.is_installed()); - EXPECT_FALSE(app2.is_authorized()); - EXPECT_EQ("https://chrome.google.com/webstore/detail/" - "hgfedcbahgfedcbahgfedcbahgfedcba", - app2.product_url().spec()); + EXPECT_FALSE(app2.is_removable()); + EXPECT_EQ("hgfedcbahgfedcbahgfedcbahgfedcba", app2.product_id()); ASSERT_EQ(3U, app2.primary_mimetypes().size()); EXPECT_EQ("image/jpeg", *app2.primary_mimetypes()[0]); @@ -110,6 +104,8 @@ TEST(DriveAPIParserTest, AppListParser) { EXPECT_EQ(DriveAppIcon::DOCUMENT, icon2.category()); EXPECT_EQ(10, icon2.icon_side_length()); EXPECT_EQ("http://www.example.com/d10.png", icon2.icon_url().spec()); + + EXPECT_EQ("https://www.example.com/createForApp2", app2.create_url().spec()); } // Test file list parsing. @@ -123,12 +119,6 @@ TEST(DriveAPIParserTest, FileListParser) { scoped_ptr<FileList> filelist(new FileList); EXPECT_TRUE(filelist->Parse(*document)); - EXPECT_EQ("\"WtRjAPZWbDA7_fkFjc5ojsEvDEF/zyHTfoHpnRHovyi8bWpwK0DXABC\"", - filelist->etag()); - EXPECT_EQ("EAIaggELEgA6egpi96It9mH_____f_8AAP__AAD_okhU-cHLz83KzszMxsjMzs_Ry" - "NGJnridyrbHs7u9tv8AAP__AP7__n__AP8AokhU-cHLz83KzszMxsjMzs_RyNGJnr" - "idyrbHs7u9tv8A__4QZCEiXPTi_wtIgTkAAAAAngnSXUgCDEAAIgsJPgart10AAAA" - "ABC", filelist->next_page_token()); EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files?pageToken=EAIaggEL" "EgA6egpi96It9mH_____f_8AAP__AAD_okhU-cHLz83KzszMxsjMzs_RyNGJ" "nridyrbHs7u9tv8AAP__AP7__n__AP8AokhU-cHLz83KzszMxsjMzs_RyNGJ" @@ -144,11 +134,7 @@ TEST(DriveAPIParserTest, FileListParser) { EXPECT_EQ("My first file data", file1.title()); EXPECT_EQ("application/octet-stream", file1.mime_type()); - EXPECT_FALSE(file1.labels().is_starred()); - EXPECT_FALSE(file1.labels().is_hidden()); EXPECT_FALSE(file1.labels().is_trashed()); - EXPECT_FALSE(file1.labels().is_restricted()); - EXPECT_TRUE(file1.labels().is_viewed()); EXPECT_FALSE(file1.shared()); EXPECT_EQ(640, file1.image_media_metadata().width()); @@ -164,29 +150,19 @@ TEST(DriveAPIParserTest, FileListParser) { ASSERT_TRUE( util::GetTimeFromString("2012-07-27T05:43:20.269Z", &modified_time)); EXPECT_EQ(modified_time, file1.modified_date()); - EXPECT_EQ(modified_time, file1.modified_by_me_date()); ASSERT_EQ(1U, file1.parents().size()); - EXPECT_EQ("0B4v7G8yEYAWHYW1OcExsUVZLABC", file1.parents()[0]->file_id()); + EXPECT_EQ("0B4v7G8yEYAWHYW1OcExsUVZLABC", file1.parents()[0].file_id()); EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files/" "0B4v7G8yEYAWHYW1OcExsUVZLABC"), - file1.parents()[0]->parent_link()); - EXPECT_FALSE(file1.parents()[0]->is_root()); + file1.parents()[0].parent_link()); - EXPECT_EQ(GURL("https://www.example.com/download"), file1.download_url()); - EXPECT_EQ("ext", file1.file_extension()); EXPECT_EQ("d41d8cd98f00b204e9800998ecf8427e", file1.md5_checksum()); EXPECT_EQ(1000U, file1.file_size()); - EXPECT_EQ(GURL("https://www.googleapis.com/drive/v2/files/" - "0B4v7G8yEYAWHUmRrU2lMS2hLABC"), - file1.self_link()); EXPECT_EQ(GURL("https://docs.google.com/file/d/" "0B4v7G8yEYAWHUmRrU2lMS2hLABC/edit"), file1.alternate_link()); - EXPECT_EQ(GURL("https://docs.google.com/uc?" - "id=0B4v7G8yEYAWHUmRrU2lMS2hLABC&export=download"), - file1.web_content_link()); ASSERT_EQ(1U, file1.open_with_links().size()); EXPECT_EQ("1234567890", file1.open_with_links()[0].app_id); EXPECT_EQ(GURL("http://open_with_link/url"), @@ -197,11 +173,7 @@ TEST(DriveAPIParserTest, FileListParser) { EXPECT_EQ("Test Google Document", file2.title()); EXPECT_EQ("application/vnd.google-apps.document", file2.mime_type()); - EXPECT_TRUE(file2.labels().is_starred()); - EXPECT_TRUE(file2.labels().is_hidden()); EXPECT_TRUE(file2.labels().is_trashed()); - EXPECT_TRUE(file2.labels().is_restricted()); - EXPECT_TRUE(file2.labels().is_viewed()); EXPECT_TRUE(file2.shared()); EXPECT_EQ(-1, file2.image_media_metadata().width()); @@ -217,13 +189,6 @@ TEST(DriveAPIParserTest, FileListParser) { ASSERT_EQ(0U, file2.parents().size()); - EXPECT_EQ(GURL("https://docs.google.com/a/chromium.org/document/d/" - "1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC/preview"), - file2.embed_link()); - EXPECT_EQ(GURL("https://docs.google.com/feeds/vt?gd=true&" - "id=1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC&" - "v=3&s=AMedNnoAAAAAUBJyB0g8HbxZaLRnlztxefZPS24LiXYZ&sz=s220"), - file2.thumbnail_link()); EXPECT_EQ(0U, file2.open_with_links().size()); // Check file 3 (a folder) @@ -235,8 +200,7 @@ TEST(DriveAPIParserTest, FileListParser) { EXPECT_FALSE(file3.shared()); ASSERT_EQ(1U, file3.parents().size()); - EXPECT_EQ("0AIv7G8yEYAWHUk9ABC", file3.parents()[0]->file_id()); - EXPECT_TRUE(file3.parents()[0]->is_root()); + EXPECT_EQ("0AIv7G8yEYAWHUk9ABC", file3.parents()[0].file_id()); EXPECT_EQ(0U, file3.open_with_links().size()); } @@ -251,9 +215,6 @@ TEST(DriveAPIParserTest, ChangeListParser) { scoped_ptr<ChangeList> changelist(new ChangeList); EXPECT_TRUE(changelist->Parse(*document)); - EXPECT_EQ("\"Lp2bjAtLP341hvGmYHhxjYyBPJ8/BWbu_eylt5f_aGtCN6mGRv9hABC\"", - changelist->etag()); - EXPECT_EQ("8929", changelist->next_page_token()); EXPECT_EQ("https://www.googleapis.com/drive/v2/changes?pageToken=8929", changelist->next_link().spec()); EXPECT_EQ(13664, changelist->largest_change_id()); @@ -266,6 +227,7 @@ TEST(DriveAPIParserTest, ChangeListParser) { EXPECT_EQ("1Pc8jzfU1ErbN_eucMMqdqzY3eBm0v8sxXm_1CtLxABC", change1.file_id()); EXPECT_EQ(change1.file_id(), change1.file()->file_id()); EXPECT_FALSE(change1.file()->shared()); + EXPECT_EQ(change1.file()->modified_date(), change1.modification_date()); const ChangeResource& change2 = *changelist->items()[1]; EXPECT_EQ(8424, change2.change_id()); @@ -273,6 +235,7 @@ TEST(DriveAPIParserTest, ChangeListParser) { EXPECT_EQ("0B4v7G8yEYAWHUmRrU2lMS2hLABC", change2.file_id()); EXPECT_EQ(change2.file_id(), change2.file()->file_id()); EXPECT_TRUE(change2.file()->shared()); + EXPECT_EQ(change2.file()->modified_date(), change2.modification_date()); const ChangeResource& change3 = *changelist->items()[2]; EXPECT_EQ(8429, change3.change_id()); @@ -280,12 +243,17 @@ TEST(DriveAPIParserTest, ChangeListParser) { EXPECT_EQ("0B4v7G8yEYAWHYW1OcExsUVZLABC", change3.file_id()); EXPECT_EQ(change3.file_id(), change3.file()->file_id()); EXPECT_FALSE(change3.file()->shared()); + EXPECT_EQ(change3.file()->modified_date(), change3.modification_date()); // Deleted entry. const ChangeResource& change4 = *changelist->items()[3]; EXPECT_EQ(8430, change4.change_id()); EXPECT_EQ("ABCv7G8yEYAWHc3Y5X0hMSkJYXYZ", change4.file_id()); EXPECT_TRUE(change4.is_deleted()); + base::Time modification_time; + ASSERT_TRUE(util::GetTimeFromString("2012-07-27T12:34:56.789Z", + &modification_time)); + EXPECT_EQ(modification_time, change4.modification_date()); } TEST(DriveAPIParserTest, HasKind) { diff --git a/chromium/google_apis/drive/drive_api_requests.cc b/chromium/google_apis/drive/drive_api_requests.cc index f0960e6f237..a63757112b8 100644 --- a/chromium/google_apis/drive/drive_api_requests.cc +++ b/chromium/google_apis/drive/drive_api_requests.cc @@ -113,6 +113,15 @@ void ParseFileResourceWithUploadRangeAndRun( callback.Run(response, file_resource.Pass()); } +// Creates a Parents value which can be used as a part of request body. +scoped_ptr<base::DictionaryValue> CreateParentValue( + const std::string& file_id) { + scoped_ptr<base::DictionaryValue> parent(new base::DictionaryValue); + parent->SetString("kind", kParentLinkKind); + parent->SetString("id", file_id); + return parent.Pass(); +} + } // namespace namespace drive { @@ -153,6 +162,29 @@ GURL FilesGetRequest::GetURLInternal() const { return url_generator_.GetFilesGetUrl(file_id_); } +//============================ FilesAuthorizeRequest =========================== + +FilesAuthorizeRequest::FilesAuthorizeRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback) + : DriveApiDataRequest( + sender, + base::Bind(&ParseJsonAndRun<FileResource>, callback)), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +FilesAuthorizeRequest::~FilesAuthorizeRequest() {} + +net::URLFetcher::RequestType FilesAuthorizeRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +GURL FilesAuthorizeRequest::GetURLInternal() const { + return url_generator_.GetFilesAuthorizeUrl(file_id_, app_id_); +} + //============================ FilesInsertRequest ============================ FilesInsertRequest::FilesInsertRequest( @@ -178,9 +210,17 @@ bool FilesInsertRequest::GetContentData(std::string* upload_content_type, base::DictionaryValue root; + if (!last_viewed_by_me_date_.is_null()) { + root.SetString("lastViewedByMeDate", + util::FormatTimeAsString(last_viewed_by_me_date_)); + } + if (!mime_type_.empty()) root.SetString("mimeType", mime_type_); + if (!modified_date_.is_null()) + root.SetString("modifiedDate", util::FormatTimeAsString(modified_date_)); + if (!parents_.empty()) { base::ListValue* parents_value = new base::ListValue; for (size_t i = 0; i < parents_.size(); ++i) { @@ -491,18 +531,40 @@ GURL ChangesListNextPageRequest::GetURLInternal() const { AppsListRequest::AppsListRequest( RequestSender* sender, const DriveApiUrlGenerator& url_generator, + bool use_internal_endpoint, const AppListCallback& callback) : DriveApiDataRequest( sender, base::Bind(&ParseJsonAndRun<AppList>, callback)), - url_generator_(url_generator) { + url_generator_(url_generator), + use_internal_endpoint_(use_internal_endpoint) { DCHECK(!callback.is_null()); } AppsListRequest::~AppsListRequest() {} GURL AppsListRequest::GetURLInternal() const { - return url_generator_.GetAppsListUrl(); + return url_generator_.GetAppsListUrl(use_internal_endpoint_); +} + +//============================== AppsDeleteRequest =========================== + +AppsDeleteRequest::AppsDeleteRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback) + : EntryActionRequest(sender, callback), + url_generator_(url_generator) { + DCHECK(!callback.is_null()); +} + +AppsDeleteRequest::~AppsDeleteRequest() {} + +net::URLFetcher::RequestType AppsDeleteRequest::GetRequestType() const { + return net::URLFetcher::DELETE_REQUEST; +} + +GURL AppsDeleteRequest::GetURL() const { + return url_generator_.GetAppsDeleteUrl(app_id_); } //========================== ChildrenInsertRequest ============================ @@ -582,7 +644,7 @@ InitiateUploadNewFileRequest::InitiateUploadNewFileRequest( InitiateUploadNewFileRequest::~InitiateUploadNewFileRequest() {} GURL InitiateUploadNewFileRequest::GetURL() const { - return url_generator_.GetInitiateUploadNewFileUrl(); + return url_generator_.GetInitiateUploadNewFileUrl(!modified_date_.is_null()); } net::URLFetcher::RequestType @@ -599,15 +661,16 @@ bool InitiateUploadNewFileRequest::GetContentData( root.SetString("title", title_); // Fill parent link. - { - scoped_ptr<base::DictionaryValue> parent(new base::DictionaryValue); - parent->SetString("kind", kParentLinkKind); - parent->SetString("id", parent_resource_id_); + scoped_ptr<base::ListValue> parents(new base::ListValue); + parents->Append(CreateParentValue(parent_resource_id_).release()); + root.Set("parents", parents.release()); - scoped_ptr<base::ListValue> parents(new base::ListValue); - parents->Append(parent.release()); + if (!modified_date_.is_null()) + root.SetString("modifiedDate", util::FormatTimeAsString(modified_date_)); - root.Set("parents", parents.release()); + if (!last_viewed_by_me_date_.is_null()) { + root.SetString("lastViewedByMeDate", + util::FormatTimeAsString(last_viewed_by_me_date_)); } base::JSONWriter::Write(&root, upload_content); @@ -639,7 +702,8 @@ InitiateUploadExistingFileRequest::InitiateUploadExistingFileRequest( InitiateUploadExistingFileRequest::~InitiateUploadExistingFileRequest() {} GURL InitiateUploadExistingFileRequest::GetURL() const { - return url_generator_.GetInitiateUploadExistingFileUrl(resource_id_); + return url_generator_.GetInitiateUploadExistingFileUrl( + resource_id_, !modified_date_.is_null()); } net::URLFetcher::RequestType @@ -655,6 +719,37 @@ InitiateUploadExistingFileRequest::GetExtraRequestHeaders() const { return headers; } +bool InitiateUploadExistingFileRequest::GetContentData( + std::string* upload_content_type, + std::string* upload_content) { + base::DictionaryValue root; + if (!parent_resource_id_.empty()) { + scoped_ptr<base::ListValue> parents(new base::ListValue); + parents->Append(CreateParentValue(parent_resource_id_).release()); + root.Set("parents", parents.release()); + } + + if (!title_.empty()) + root.SetString("title", title_); + + if (!modified_date_.is_null()) + root.SetString("modifiedDate", util::FormatTimeAsString(modified_date_)); + + if (!last_viewed_by_me_date_.is_null()) { + root.SetString("lastViewedByMeDate", + util::FormatTimeAsString(last_viewed_by_me_date_)); + } + + if (root.empty()) + return false; + + *upload_content_type = kContentTypeApplicationJson; + base::JSONWriter::Write(&root, upload_content); + DVLOG(1) << "InitiateUploadExistingFile data: " << *upload_content_type + << ", [" << *upload_content << "]"; + return true; +} + //============================ ResumeUploadRequest =========================== ResumeUploadRequest::ResumeUploadRequest( @@ -739,5 +834,72 @@ DownloadFileRequest::DownloadFileRequest( DownloadFileRequest::~DownloadFileRequest() { } +//========================== PermissionsInsertRequest ========================== + +PermissionsInsertRequest::PermissionsInsertRequest( + RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback) + : EntryActionRequest(sender, callback), + url_generator_(url_generator), + type_(PERMISSION_TYPE_USER), + role_(PERMISSION_ROLE_READER) { +} + +PermissionsInsertRequest::~PermissionsInsertRequest() { +} + +GURL PermissionsInsertRequest::GetURL() const { + return url_generator_.GetPermissionsInsertUrl(id_); +} + +net::URLFetcher::RequestType +PermissionsInsertRequest::GetRequestType() const { + return net::URLFetcher::POST; +} + +bool PermissionsInsertRequest::GetContentData(std::string* upload_content_type, + std::string* upload_content) { + *upload_content_type = kContentTypeApplicationJson; + + base::DictionaryValue root; + switch (type_) { + case PERMISSION_TYPE_ANYONE: + root.SetString("type", "anyone"); + break; + case PERMISSION_TYPE_DOMAIN: + root.SetString("type", "domain"); + break; + case PERMISSION_TYPE_GROUP: + root.SetString("type", "group"); + break; + case PERMISSION_TYPE_USER: + root.SetString("type", "user"); + break; + } + switch (role_) { + case PERMISSION_ROLE_OWNER: + root.SetString("role", "owner"); + break; + case PERMISSION_ROLE_READER: + root.SetString("role", "reader"); + break; + case PERMISSION_ROLE_WRITER: + root.SetString("role", "writer"); + break; + case PERMISSION_ROLE_COMMENTER: + root.SetString("role", "reader"); + { + base::ListValue* list = new base::ListValue; + list->AppendString("commenter"); + root.Set("additionalRoles", list); + } + break; + } + root.SetString("value", value_); + base::JSONWriter::Write(&root, upload_content); + return true; +} + } // namespace drive } // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_requests.h b/chromium/google_apis/drive/drive_api_requests.h index 6f10aa66d95..59bffd67b87 100644 --- a/chromium/google_apis/drive/drive_api_requests.h +++ b/chromium/google_apis/drive/drive_api_requests.h @@ -93,6 +93,38 @@ class FilesGetRequest : public DriveApiDataRequest { DISALLOW_COPY_AND_ASSIGN(FilesGetRequest); }; +//============================ FilesAuthorizeRequest =========================== + +// This class performs request for authorizing an app to access a file. +// This request is mapped to /drive/v2internal/file/authorize internal endpoint. +class FilesAuthorizeRequest : public DriveApiDataRequest { + public: + FilesAuthorizeRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const FileResourceCallback& callback); + virtual ~FilesAuthorizeRequest(); + + // Required parameter. + const std::string& file_id() const { return file_id_; } + void set_file_id(const std::string& file_id) { file_id_ = file_id; } + const std::string& app_id() const { return app_id_; } + void set_app_id(const std::string& app_id) { app_id_ = app_id; } + + protected: + // Overridden from GetDataRequest. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + + // Overridden from DriveApiDataRequest. + virtual GURL GetURLInternal() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string file_id_; + std::string app_id_; + + DISALLOW_COPY_AND_ASSIGN(FilesAuthorizeRequest); +}; + //============================ FilesInsertRequest ============================= // This class performs the request for creating a resource. @@ -108,11 +140,23 @@ class FilesInsertRequest : public DriveApiDataRequest { virtual ~FilesInsertRequest(); // Optional request body. + const base::Time& last_viewed_by_me_date() const { + return last_viewed_by_me_date_; + } + void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) { + last_viewed_by_me_date_ = last_viewed_by_me_date; + } + const std::string& mime_type() const { return mime_type_; } void set_mime_type(const std::string& mime_type) { mime_type_ = mime_type; } + const base::Time& modified_date() const { return modified_date_; } + void set_modified_date(const base::Time& modified_date) { + modified_date_ = modified_date; + } + const std::vector<std::string>& parents() const { return parents_; } void add_parent(const std::string& parent) { parents_.push_back(parent); } @@ -131,7 +175,9 @@ class FilesInsertRequest : public DriveApiDataRequest { private: const DriveApiUrlGenerator url_generator_; + base::Time last_viewed_by_me_date_; std::string mime_type_; + base::Time modified_date_; std::vector<std::string> parents_; std::string title_; @@ -491,6 +537,7 @@ class AppsListRequest : public DriveApiDataRequest { public: AppsListRequest(RequestSender* sender, const DriveApiUrlGenerator& url_generator, + bool use_internal_endpoint, const AppListCallback& callback); virtual ~AppsListRequest(); @@ -500,10 +547,39 @@ class AppsListRequest : public DriveApiDataRequest { private: const DriveApiUrlGenerator url_generator_; + bool use_internal_endpoint_; DISALLOW_COPY_AND_ASSIGN(AppsListRequest); }; +//============================= AppsDeleteRequest ============================== + +// This class performs the request for deleting a Drive app. +// This request is mapped to +// https://developers.google.com/drive/v2/reference/files/trash +class AppsDeleteRequest : public EntryActionRequest { + public: + AppsDeleteRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback); + virtual ~AppsDeleteRequest(); + + // Required parameter. + const std::string& app_id() const { return app_id_; } + void set_app_id(const std::string& app_id) { app_id_ = app_id; } + + protected: + // Overridden from UrlFetchRequestBase. + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual GURL GetURL() const OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string app_id_; + + DISALLOW_COPY_AND_ASSIGN(AppsDeleteRequest); +}; + //========================== ChildrenInsertRequest ============================ // This class performs the request for inserting a resource to a directory. @@ -596,6 +672,18 @@ class InitiateUploadNewFileRequest : public InitiateUploadRequestBase { const InitiateUploadCallback& callback); virtual ~InitiateUploadNewFileRequest(); + // Optional parameters. + const base::Time& modified_date() const { return modified_date_; } + void set_modified_date(const base::Time& modified_date) { + modified_date_ = modified_date; + } + const base::Time& last_viewed_by_me_date() const { + return last_viewed_by_me_date_; + } + void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) { + last_viewed_by_me_date_ = last_viewed_by_me_date; + } + protected: // UrlFetchRequestBase overrides. virtual GURL GetURL() const OVERRIDE; @@ -608,6 +696,9 @@ class InitiateUploadNewFileRequest : public InitiateUploadRequestBase { const std::string parent_resource_id_; const std::string title_; + base::Time modified_date_; + base::Time last_viewed_by_me_date_; + DISALLOW_COPY_AND_ASSIGN(InitiateUploadNewFileRequest); }; @@ -631,17 +722,43 @@ class InitiateUploadExistingFileRequest : public InitiateUploadRequestBase { const InitiateUploadCallback& callback); virtual ~InitiateUploadExistingFileRequest(); + + // Optional parameters. + const std::string& parent_resource_id() const { return parent_resource_id_; } + void set_parent_resource_id(const std::string& parent_resource_id) { + parent_resource_id_ = parent_resource_id; + } + const std::string& title() const { return title_; } + void set_title(const std::string& title) { title_ = title; } + const base::Time& modified_date() const { return modified_date_; } + void set_modified_date(const base::Time& modified_date) { + modified_date_ = modified_date; + } + const base::Time& last_viewed_by_me_date() const { + return last_viewed_by_me_date_; + } + void set_last_viewed_by_me_date(const base::Time& last_viewed_by_me_date) { + last_viewed_by_me_date_ = last_viewed_by_me_date; + } + protected: // UrlFetchRequestBase overrides. virtual GURL GetURL() const OVERRIDE; virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; private: const DriveApiUrlGenerator url_generator_; const std::string resource_id_; const std::string etag_; + std::string parent_resource_id_; + std::string title_; + base::Time modified_date_; + base::Time last_viewed_by_me_date_; + DISALLOW_COPY_AND_ASSIGN(InitiateUploadExistingFileRequest); }; @@ -727,6 +844,54 @@ class DownloadFileRequest : public DownloadFileRequestBase { DISALLOW_COPY_AND_ASSIGN(DownloadFileRequest); }; +//========================== PermissionsInsertRequest ========================== + +// Enumeration type for specifying type of permissions. +enum PermissionType { + PERMISSION_TYPE_ANYONE, + PERMISSION_TYPE_DOMAIN, + PERMISSION_TYPE_GROUP, + PERMISSION_TYPE_USER, +}; + +// Enumeration type for specifying the role of permissions. +enum PermissionRole { + PERMISSION_ROLE_OWNER, + PERMISSION_ROLE_READER, + PERMISSION_ROLE_WRITER, + PERMISSION_ROLE_COMMENTER, +}; + +// This class performs the request for adding permission on a specified file. +class PermissionsInsertRequest : public EntryActionRequest { + public: + // See https://developers.google.com/drive/v2/reference/permissions/insert. + PermissionsInsertRequest(RequestSender* sender, + const DriveApiUrlGenerator& url_generator, + const EntryActionCallback& callback); + virtual ~PermissionsInsertRequest(); + + void set_id(const std::string& id) { id_ = id; } + void set_type(PermissionType type) { type_ = type; } + void set_role(PermissionRole role) { role_ = role; } + void set_value(const std::string& value) { value_ = value; } + + // UrlFetchRequestBase overrides. + virtual GURL GetURL() const OVERRIDE; + virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; + virtual bool GetContentData(std::string* upload_content_type, + std::string* upload_content) OVERRIDE; + + private: + const DriveApiUrlGenerator url_generator_; + std::string id_; + PermissionType type_; + PermissionRole role_; + std::string value_; + + DISALLOW_COPY_AND_ASSIGN(PermissionsInsertRequest); +}; + } // namespace drive } // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_requests_unittest.cc b/chromium/google_apis/drive/drive_api_requests_unittest.cc index 466240cd369..9281b866a88 100644 --- a/chromium/google_apis/drive/drive_api_requests_unittest.cc +++ b/chromium/google_apis/drive/drive_api_requests_unittest.cc @@ -6,6 +6,7 @@ #include "base/file_util.h" #include "base/files/file_path.h" #include "base/files/scoped_temp_dir.h" +#include "base/json/json_reader.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" #include "base/strings/string_number_conversions.h" @@ -37,6 +38,13 @@ const char kTestChildrenResponse[] = "\"childLink\": \"child_link\",\n" "}\n"; +const char kTestPermissionResponse[] = + "{\n" + "\"kind\": \"drive#permission\",\n" + "\"id\": \"resource_id\",\n" + "\"selfLink\": \"self_link\",\n" + "}\n"; + const char kTestUploadExistingFilePath[] = "/upload/existingfile/path"; const char kTestUploadNewFilePath[] = "/upload/newfile/path"; const char kTestDownloadPathPrefix[] = "/download/"; @@ -418,6 +426,10 @@ TEST_F(DriveApiRequestsTest, DriveApiDataRequest_Fields) { } TEST_F(DriveApiRequestsTest, FilesInsertRequest) { + const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123}; + const base::Time::Exploded kLastViewedByMeDate = + {2013, 7, 0, 19, 15, 59, 13, 123}; + // Set an expected data file containing the directory's entry data. expected_data_file_path_ = test_util::GetTestFilePath("drive/directory_entry.json"); @@ -434,7 +446,10 @@ TEST_F(DriveApiRequestsTest, FilesInsertRequest) { test_util::CreateQuitCallback( &run_loop, test_util::CreateCopyResultCallback(&error, &file_resource))); + request->set_last_viewed_by_me_date( + base::Time::FromUTCExploded(kLastViewedByMeDate)); request->set_mime_type("application/vnd.google-apps.folder"); + request->set_modified_date(base::Time::FromUTCExploded(kModifiedDate)); request->add_parent("root"); request->set_title("new directory"); request_sender_->StartRequestWithRetry(request); @@ -447,6 +462,12 @@ TEST_F(DriveApiRequestsTest, FilesInsertRequest) { EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"lastViewedByMeDate\":\"2013-07-19T15:59:13.123Z\"," + "\"mimeType\":\"application/vnd.google-apps.folder\"," + "\"modifiedDate\":\"2012-07-19T15:59:13.123Z\"," + "\"parents\":[{\"id\":\"root\"}]," + "\"title\":\"new directory\"}", + http_request_.content); scoped_ptr<FileResource> expected( FileResource::CreateFrom( @@ -585,6 +606,7 @@ TEST_F(DriveApiRequestsTest, AppsListRequest) { drive::AppsListRequest* request = new drive::AppsListRequest( request_sender_.get(), *url_generator_, + false, // use_internal_endpoint test_util::CreateQuitCallback( &run_loop, test_util::CreateCopyResultCallback(&error, &app_list))); @@ -1257,6 +1279,60 @@ TEST_F(DriveApiRequestsTest, UploadNewLargeFileRequest) { } } +TEST_F(DriveApiRequestsTest, UploadNewFileWithMetadataRequest) { + const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123}; + const base::Time::Exploded kLastViewedByMeDate = + {2013, 7, 0, 19, 15, 59, 13, 123}; + + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadNewFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadNewFileRequest* request = + new drive::InitiateUploadNewFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "parent_resource_id", // The resource id of the parent directory. + "new file title", // The title of the file being uploaded. + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request->set_modified_date(base::Time::FromUTCExploded(kModifiedDate)); + request->set_last_viewed_by_me_date( + base::Time::FromUTCExploded(kLastViewedByMeDate)); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadNewFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files?uploadType=resumable&setModifiedDate=true", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"lastViewedByMeDate\":\"2013-07-19T15:59:13.123Z\"," + "\"modifiedDate\":\"2012-07-19T15:59:13.123Z\"," + "\"parents\":[{\"id\":\"parent_resource_id\"," + "\"kind\":\"drive#fileLink\"}]," + "\"title\":\"new file title\"}", + http_request_.content); +} + TEST_F(DriveApiRequestsTest, UploadExistingFileRequest) { // Set an expected url for uploading. expected_upload_path_ = kTestUploadExistingFilePath; @@ -1573,6 +1649,65 @@ TEST_F(DriveApiRequestsTest, EXPECT_FALSE(new_entry.get()); } +TEST_F(DriveApiRequestsTest, UploadExistingFileWithMetadataRequest) { + const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123}; + const base::Time::Exploded kLastViewedByMeDate = + {2013, 7, 0, 19, 15, 59, 13, 123}; + + // Set an expected url for uploading. + expected_upload_path_ = kTestUploadExistingFilePath; + + const char kTestContentType[] = "text/plain"; + const std::string kTestContent(100, 'a'); + + GDataErrorCode error = GDATA_OTHER_ERROR; + GURL upload_url; + + // Initiate uploading a new file to the directory with "parent_resource_id". + { + base::RunLoop run_loop; + drive::InitiateUploadExistingFileRequest* request = + new drive::InitiateUploadExistingFileRequest( + request_sender_.get(), + *url_generator_, + kTestContentType, + kTestContent.size(), + "resource_id", // The resource id of the file to be overwritten. + kTestETag, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error, &upload_url))); + request->set_parent_resource_id("new_parent_resource_id"); + request->set_title("new file title"); + request->set_modified_date(base::Time::FromUTCExploded(kModifiedDate)); + request->set_last_viewed_by_me_date( + base::Time::FromUTCExploded(kLastViewedByMeDate)); + + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(kTestUploadExistingFilePath, upload_url.path()); + EXPECT_EQ(kTestContentType, http_request_.headers["X-Upload-Content-Type"]); + EXPECT_EQ(base::Int64ToString(kTestContent.size()), + http_request_.headers["X-Upload-Content-Length"]); + EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]); + + EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); + EXPECT_EQ("/upload/drive/v2/files/resource_id?" + "uploadType=resumable&setModifiedDate=true", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + EXPECT_TRUE(http_request_.has_content); + EXPECT_EQ("{\"lastViewedByMeDate\":\"2013-07-19T15:59:13.123Z\"," + "\"modifiedDate\":\"2012-07-19T15:59:13.123Z\"," + "\"parents\":[{\"id\":\"new_parent_resource_id\"," + "\"kind\":\"drive#fileLink\"}]," + "\"title\":\"new file title\"}", + http_request_.content); +} + TEST_F(DriveApiRequestsTest, DownloadFileRequest) { const base::FilePath kDownloadedFilePath = temp_dir_.path().AppendASCII("cache_file"); @@ -1644,4 +1779,77 @@ TEST_F(DriveApiRequestsTest, DownloadFileRequest_GetContentCallback) { EXPECT_EQ(expected_contents, contents); } +TEST_F(DriveApiRequestsTest, PermissionsInsertRequest) { + expected_content_type_ = "application/json"; + expected_content_ = kTestPermissionResponse; + + GDataErrorCode error = GDATA_OTHER_ERROR; + + // Add comment permission to the user "user@example.com". + { + base::RunLoop run_loop; + drive::PermissionsInsertRequest* request = + new drive::PermissionsInsertRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error))); + request->set_id("resource_id"); + request->set_role(drive::PERMISSION_ROLE_COMMENTER); + request->set_type(drive::PERMISSION_TYPE_USER); + request->set_value("user@example.com"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/drive/v2/files/resource_id/permissions", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + + scoped_ptr<base::Value> expected(base::JSONReader::Read( + "{\"additionalRoles\":[\"commenter\"], \"role\":\"reader\", " + "\"type\":\"user\",\"value\":\"user@example.com\"}")); + ASSERT_TRUE(expected); + + scoped_ptr<base::Value> result(base::JSONReader::Read(http_request_.content)); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(base::Value::Equals(expected.get(), result.get())); + + // Add "can edit" permission to users in "example.com". + error = GDATA_OTHER_ERROR; + { + base::RunLoop run_loop; + drive::PermissionsInsertRequest* request = + new drive::PermissionsInsertRequest( + request_sender_.get(), + *url_generator_, + test_util::CreateQuitCallback( + &run_loop, + test_util::CreateCopyResultCallback(&error))); + request->set_id("resource_id2"); + request->set_role(drive::PERMISSION_ROLE_WRITER); + request->set_type(drive::PERMISSION_TYPE_DOMAIN); + request->set_value("example.com"); + request_sender_->StartRequestWithRetry(request); + run_loop.Run(); + } + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); + EXPECT_EQ("/drive/v2/files/resource_id2/permissions", + http_request_.relative_url); + EXPECT_EQ("application/json", http_request_.headers["Content-Type"]); + + expected.reset(base::JSONReader::Read( + "{\"role\":\"writer\", \"type\":\"domain\",\"value\":\"example.com\"}")); + ASSERT_TRUE(expected); + + result.reset(base::JSONReader::Read(http_request_.content)); + EXPECT_TRUE(http_request_.has_content); + EXPECT_TRUE(base::Value::Equals(expected.get(), result.get())); +} + } // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_url_generator.cc b/chromium/google_apis/drive/drive_api_url_generator.cc index c12b947d3dc..b8374e27699 100644 --- a/chromium/google_apis/drive/drive_api_url_generator.cc +++ b/chromium/google_apis/drive/drive_api_url_generator.cc @@ -7,6 +7,7 @@ #include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" +#include "google_apis/google_api_keys.h" #include "net/base/escape.h" #include "net/base/url_util.h" @@ -29,6 +30,14 @@ const char kDriveV2FileTrashUrlFormat[] = "/drive/v2/files/%s/trash"; const char kDriveV2InitiateUploadNewFileUrl[] = "/upload/drive/v2/files"; const char kDriveV2InitiateUploadExistingFileUrlPrefix[] = "/upload/drive/v2/files/"; +const char kDriveV2PermissionsUrlFormat[] = "/drive/v2/files/%s/permissions"; + +// apps.delete and file.authorize API is exposed through a special endpoint +// v2internal that is accessible only by the official API key for Chrome. +const char kDriveV2InternalAppsUrl[] = "/drive/v2internal/apps"; +const char kDriveV2AppsDeleteUrlFormat[] = "/drive/v2internal/apps/%s"; +const char kDriveV2FilesAuthorizeUrlFormat[] = + "/drive/v2internal/files/%s/authorize?appId=%s"; GURL AddResumableUploadParam(const GURL& url) { return net::AppendOrReplaceQueryParameter(url, "uploadType", "resumable"); @@ -56,14 +65,28 @@ GURL DriveApiUrlGenerator::GetAboutGetUrl() const { return base_url_.Resolve(kDriveV2AboutUrl); } -GURL DriveApiUrlGenerator::GetAppsListUrl() const { - return base_url_.Resolve(kDriveV2AppsUrl); +GURL DriveApiUrlGenerator::GetAppsListUrl(bool use_internal_endpoint) const { + return base_url_.Resolve(use_internal_endpoint ? + kDriveV2InternalAppsUrl : kDriveV2AppsUrl); +} + +GURL DriveApiUrlGenerator::GetAppsDeleteUrl(const std::string& app_id) const { + return base_url_.Resolve(base::StringPrintf( + kDriveV2AppsDeleteUrlFormat, net::EscapePath(app_id).c_str())); } GURL DriveApiUrlGenerator::GetFilesGetUrl(const std::string& file_id) const { return base_url_.Resolve(kDriveV2FileUrlPrefix + net::EscapePath(file_id)); } +GURL DriveApiUrlGenerator::GetFilesAuthorizeUrl( + const std::string& file_id, + const std::string& app_id) const { + return base_url_.Resolve(base::StringPrintf(kDriveV2FilesAuthorizeUrlFormat, + net::EscapePath(file_id).c_str(), + net::EscapePath(app_id).c_str())); +} + GURL DriveApiUrlGenerator::GetFilesInsertUrl() const { return base_url_.Resolve(kDriveV2FilesUrl); } @@ -162,17 +185,31 @@ GURL DriveApiUrlGenerator::GetChildrenDeleteUrl( net::EscapePath(child_id).c_str())); } -GURL DriveApiUrlGenerator::GetInitiateUploadNewFileUrl() const { - return AddResumableUploadParam( +GURL DriveApiUrlGenerator::GetInitiateUploadNewFileUrl( + bool set_modified_date) const { + GURL url = AddResumableUploadParam( base_url_.Resolve(kDriveV2InitiateUploadNewFileUrl)); + + // setModifiedDate is "false" by default. + if (set_modified_date) + url = net::AppendOrReplaceQueryParameter(url, "setModifiedDate", "true"); + + return url; } GURL DriveApiUrlGenerator::GetInitiateUploadExistingFileUrl( - const std::string& resource_id) const { - const GURL& url = base_url_.Resolve( + const std::string& resource_id, + bool set_modified_date) const { + GURL url = base_url_.Resolve( kDriveV2InitiateUploadExistingFileUrlPrefix + net::EscapePath(resource_id)); - return AddResumableUploadParam(url); + url = AddResumableUploadParam(url); + + // setModifiedDate is "false" by default. + if (set_modified_date) + url = net::AppendOrReplaceQueryParameter(url, "setModifiedDate", "true"); + + return url; } GURL DriveApiUrlGenerator::GenerateDownloadFileUrl( @@ -180,4 +217,11 @@ GURL DriveApiUrlGenerator::GenerateDownloadFileUrl( return base_download_url_.Resolve(net::EscapePath(resource_id)); } +GURL DriveApiUrlGenerator::GetPermissionsInsertUrl( + const std::string& resource_id) const { + return base_url_.Resolve( + base::StringPrintf(kDriveV2PermissionsUrlFormat, + net::EscapePath(resource_id).c_str())); +} + } // namespace google_apis diff --git a/chromium/google_apis/drive/drive_api_url_generator.h b/chromium/google_apis/drive/drive_api_url_generator.h index cf93edd3255..8c4b160a79f 100644 --- a/chromium/google_apis/drive/drive_api_url_generator.h +++ b/chromium/google_apis/drive/drive_api_url_generator.h @@ -30,11 +30,20 @@ class DriveApiUrlGenerator { GURL GetAboutGetUrl() const; // Returns a URL to invoke "Apps: list" method. - GURL GetAppsListUrl() const; + // Set |use_internal_endpoint| to true if official Chrome's API key is used + // and retrieving more information (related to App uninstall) is necessary. + GURL GetAppsListUrl(bool use_internal_endpoint) const; + + // Returns a URL to uninstall an app with the give |app_id|. + GURL GetAppsDeleteUrl(const std::string& app_id) const; // Returns a URL to fetch a file metadata. GURL GetFilesGetUrl(const std::string& file_id) const; + // Returns a URL to authorize an app to access a file. + GURL GetFilesAuthorizeUrl(const std::string& file_id, + const std::string& app_id) const; + // Returns a URL to create a resource. GURL GetFilesInsertUrl() const; @@ -72,15 +81,19 @@ class DriveApiUrlGenerator { const std::string& folder_id) const; // Returns a URL to initiate uploading a new file. - GURL GetInitiateUploadNewFileUrl() const; + GURL GetInitiateUploadNewFileUrl(bool set_modified_date) const; // Returns a URL to initiate uploading an existing file specified by // |resource_id|. - GURL GetInitiateUploadExistingFileUrl(const std::string& resource_id) const; + GURL GetInitiateUploadExistingFileUrl(const std::string& resource_id, + bool set_modified_date) const; // Generates a URL for downloading a file. GURL GenerateDownloadFileUrl(const std::string& resource_id) const; + // Generates a URL for adding permissions. + GURL GetPermissionsInsertUrl(const std::string& resource_id) const; + private: const GURL base_url_; const GURL base_download_url_; diff --git a/chromium/google_apis/drive/drive_api_url_generator_unittest.cc b/chromium/google_apis/drive/drive_api_url_generator_unittest.cc index 343b2791c4e..557a0af3f82 100644 --- a/chromium/google_apis/drive/drive_api_url_generator_unittest.cc +++ b/chromium/google_apis/drive/drive_api_url_generator_unittest.cc @@ -35,10 +35,20 @@ TEST_F(DriveApiUrlGeneratorTest, GetAboutGetUrl) { } TEST_F(DriveApiUrlGeneratorTest, GetAppsListUrl) { + const bool use_internal_url = true; + EXPECT_EQ("https://www.googleapis.com/drive/v2internal/apps", + url_generator_.GetAppsListUrl(use_internal_url).spec()); EXPECT_EQ("https://www.googleapis.com/drive/v2/apps", - url_generator_.GetAppsListUrl().spec()); + url_generator_.GetAppsListUrl(!use_internal_url).spec()); EXPECT_EQ("http://127.0.0.1:12345/drive/v2/apps", - test_url_generator_.GetAppsListUrl().spec()); + test_url_generator_.GetAppsListUrl(!use_internal_url).spec()); +} + +TEST_F(DriveApiUrlGeneratorTest, GetAppsDeleteUrl) { + EXPECT_EQ("https://www.googleapis.com/drive/v2internal/apps/0ADK06pfg", + url_generator_.GetAppsDeleteUrl("0ADK06pfg").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2internal/apps/0ADK06pfg", + test_url_generator_.GetAppsDeleteUrl("0ADK06pfg").spec()); } TEST_F(DriveApiUrlGeneratorTest, GetFilesGetUrl) { @@ -58,6 +68,15 @@ TEST_F(DriveApiUrlGeneratorTest, GetFilesGetUrl) { test_url_generator_.GetFilesGetUrl("file:file_id").spec()); } +TEST_F(DriveApiUrlGeneratorTest, GetFilesAuthorizeUrl) { + EXPECT_EQ( + "https://www.googleapis.com/drive/v2internal/files/aa/authorize?appId=bb", + url_generator_.GetFilesAuthorizeUrl("aa", "bb").spec()); + EXPECT_EQ( + "http://127.0.0.1:12345/drive/v2internal/files/foo/authorize?appId=bar", + test_url_generator_.GetFilesAuthorizeUrl("foo", "bar").spec()); +} + TEST_F(DriveApiUrlGeneratorTest, GetFilesInsertUrl) { EXPECT_EQ("https://www.googleapis.com/drive/v2/files", url_generator_.GetFilesInsertUrl().spec()); @@ -338,45 +357,69 @@ TEST_F(DriveApiUrlGeneratorTest, GetChildrenDeleteUrl) { } TEST_F(DriveApiUrlGeneratorTest, GetInitiateUploadNewFileUrl) { + const bool kSetModifiedDate = true; + EXPECT_EQ( "https://www.googleapis.com/upload/drive/v2/files?uploadType=resumable", - url_generator_.GetInitiateUploadNewFileUrl().spec()); + url_generator_.GetInitiateUploadNewFileUrl(!kSetModifiedDate).spec()); EXPECT_EQ( "http://127.0.0.1:12345/upload/drive/v2/files?uploadType=resumable", - test_url_generator_.GetInitiateUploadNewFileUrl().spec()); + test_url_generator_.GetInitiateUploadNewFileUrl( + !kSetModifiedDate).spec()); + + EXPECT_EQ( + "http://127.0.0.1:12345/upload/drive/v2/files?uploadType=resumable&" + "setModifiedDate=true", + test_url_generator_.GetInitiateUploadNewFileUrl( + kSetModifiedDate).spec()); } TEST_F(DriveApiUrlGeneratorTest, GetInitiateUploadExistingFileUrl) { + const bool kSetModifiedDate = true; + // |resource_id| should be embedded into the url. EXPECT_EQ( "https://www.googleapis.com/upload/drive/v2/files/0ADK06pfg" "?uploadType=resumable", - url_generator_.GetInitiateUploadExistingFileUrl("0ADK06pfg").spec()); + url_generator_.GetInitiateUploadExistingFileUrl( + "0ADK06pfg", !kSetModifiedDate).spec()); EXPECT_EQ( "https://www.googleapis.com/upload/drive/v2/files/0Bz0bd074" "?uploadType=resumable", - url_generator_.GetInitiateUploadExistingFileUrl("0Bz0bd074").spec()); + url_generator_.GetInitiateUploadExistingFileUrl( + "0Bz0bd074", !kSetModifiedDate).spec()); EXPECT_EQ( "https://www.googleapis.com/upload/drive/v2/files/file%3Afile_id" "?uploadType=resumable", - url_generator_.GetInitiateUploadExistingFileUrl("file:file_id").spec()); + url_generator_.GetInitiateUploadExistingFileUrl( + "file:file_id", !kSetModifiedDate).spec()); + EXPECT_EQ( + "https://www.googleapis.com/upload/drive/v2/files/file%3Afile_id" + "?uploadType=resumable&setModifiedDate=true", + url_generator_.GetInitiateUploadExistingFileUrl( + "file:file_id", kSetModifiedDate).spec()); EXPECT_EQ( "http://127.0.0.1:12345/upload/drive/v2/files/0ADK06pfg" "?uploadType=resumable", test_url_generator_.GetInitiateUploadExistingFileUrl( - "0ADK06pfg").spec()); + "0ADK06pfg", !kSetModifiedDate).spec()); EXPECT_EQ( "http://127.0.0.1:12345/upload/drive/v2/files/0Bz0bd074" "?uploadType=resumable", test_url_generator_.GetInitiateUploadExistingFileUrl( - "0Bz0bd074").spec()); + "0Bz0bd074", !kSetModifiedDate).spec()); EXPECT_EQ( "http://127.0.0.1:12345/upload/drive/v2/files/file%3Afile_id" "?uploadType=resumable", test_url_generator_.GetInitiateUploadExistingFileUrl( - "file:file_id").spec()); + "file:file_id", !kSetModifiedDate).spec()); + EXPECT_EQ( + "http://127.0.0.1:12345/upload/drive/v2/files/file%3Afile_id" + "?uploadType=resumable&setModifiedDate=true", + test_url_generator_.GetInitiateUploadExistingFileUrl( + "file:file_id", kSetModifiedDate).spec()); } TEST_F(DriveApiUrlGeneratorTest, GenerateDownloadFileUrl) { @@ -391,4 +434,11 @@ TEST_F(DriveApiUrlGeneratorTest, GenerateDownloadFileUrl) { test_url_generator_.GenerateDownloadFileUrl("resourceId").spec()); } +TEST_F(DriveApiUrlGeneratorTest, GeneratePermissionsInsertUrl) { + EXPECT_EQ("https://www.googleapis.com/drive/v2/files/0ADK06pfg/permissions", + url_generator_.GetPermissionsInsertUrl("0ADK06pfg").spec()); + EXPECT_EQ("http://127.0.0.1:12345/drive/v2/files/file%3Aabc/permissions", + test_url_generator_.GetPermissionsInsertUrl("file:abc").spec()); +} + } // namespace google_apis diff --git a/chromium/google_apis/drive/drive_common_callbacks.h b/chromium/google_apis/drive/drive_common_callbacks.h index c31bea0358e..234c531f2b5 100644 --- a/chromium/google_apis/drive/drive_common_callbacks.h +++ b/chromium/google_apis/drive/drive_common_callbacks.h @@ -40,11 +40,6 @@ typedef base::Callback<void(GDataErrorCode error, typedef base::Callback<void(GDataErrorCode error, scoped_ptr<AppList> app_list)> AppListCallback; -// Callback used for handling UploadRangeResponse. -typedef base::Callback<void( - const UploadRangeResponse& response, - scoped_ptr<ResourceEntry> new_entry)> UploadRangeCallback; - // Callback used for authorizing an app. |open_url| is used to open the target // file with the authorized app. typedef base::Callback<void(GDataErrorCode error, diff --git a/chromium/google_apis/drive/gdata_contacts_requests.cc b/chromium/google_apis/drive/gdata_contacts_requests.cc deleted file mode 100644 index 11419af449b..00000000000 --- a/chromium/google_apis/drive/gdata_contacts_requests.cc +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2012 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/drive/gdata_contacts_requests.h" - -#include "google_apis/drive/time_util.h" -#include "net/base/url_util.h" -#include "url/gurl.h" - -namespace google_apis { - -namespace { - -// URL requesting all contact groups. -const char kGetContactGroupsURL[] = - "https://www.google.com/m8/feeds/groups/default/full?alt=json"; - -// URL requesting all contacts. -// TODO(derat): Per https://goo.gl/AufHP, "The feed may not contain all of the -// user's contacts, because there's a default limit on the number of results -// returned." Decide if 10000 is reasonable or not. -const char kGetContactsURL[] = - "https://www.google.com/m8/feeds/contacts/default/full" - "?alt=json&showdeleted=true&max-results=10000"; - -// Query parameter optionally appended to |kGetContactsURL| to return contacts -// from a specific group (as opposed to all contacts). -const char kGetContactsGroupParam[] = "group"; - -// Query parameter optionally appended to |kGetContactsURL| to return only -// recently-updated contacts. -const char kGetContactsUpdatedMinParam[] = "updated-min"; - -} // namespace - -//========================== GetContactGroupsRequest ========================= - -GetContactGroupsRequest::GetContactGroupsRequest( - RequestSender* runner, - const GetDataCallback& callback) - : GetDataRequest(runner, callback) { -} - -GetContactGroupsRequest::~GetContactGroupsRequest() {} - -GURL GetContactGroupsRequest::GetURL() const { - return !feed_url_for_testing_.is_empty() ? - feed_url_for_testing_ : - GURL(kGetContactGroupsURL); -} - -//============================ GetContactsRequest ============================ - -GetContactsRequest::GetContactsRequest( - RequestSender* runner, - const std::string& group_id, - const base::Time& min_update_time, - const GetDataCallback& callback) - : GetDataRequest(runner, callback), - group_id_(group_id), - min_update_time_(min_update_time) { -} - -GetContactsRequest::~GetContactsRequest() {} - -GURL GetContactsRequest::GetURL() const { - if (!feed_url_for_testing_.is_empty()) - return GURL(feed_url_for_testing_); - - GURL url(kGetContactsURL); - - if (!group_id_.empty()) { - url = net::AppendQueryParameter(url, kGetContactsGroupParam, group_id_); - } - if (!min_update_time_.is_null()) { - std::string time_rfc3339 = util::FormatTimeAsString(min_update_time_); - url = net::AppendQueryParameter( - url, kGetContactsUpdatedMinParam, time_rfc3339); - } - return url; -} - -//========================== GetContactPhotoRequest ========================== - -GetContactPhotoRequest::GetContactPhotoRequest( - RequestSender* runner, - const GURL& photo_url, - const GetContentCallback& callback) - : UrlFetchRequestBase(runner), - photo_url_(photo_url), - callback_(callback) { -} - -GetContactPhotoRequest::~GetContactPhotoRequest() {} - -GURL GetContactPhotoRequest::GetURL() const { - return photo_url_; -} - -void GetContactPhotoRequest::ProcessURLFetchResults( - const net::URLFetcher* source) { - GDataErrorCode code = GetErrorCode(); - scoped_ptr<std::string> data(new std::string(response_writer()->data())); - callback_.Run(code, data.Pass()); - OnProcessURLFetchResultsComplete(); -} - -void GetContactPhotoRequest::RunCallbackOnPrematureFailure( - GDataErrorCode code) { - scoped_ptr<std::string> data(new std::string); - callback_.Run(code, data.Pass()); -} - -} // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_contacts_requests.h b/chromium/google_apis/drive/gdata_contacts_requests.h deleted file mode 100644 index 05ce693242d..00000000000 --- a/chromium/google_apis/drive/gdata_contacts_requests.h +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2012 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_DRIVE_GDATA_CONTACTS_REQUESTS_H_ -#define GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_ - -#include <string> - -#include "base/time/time.h" -#include "google_apis/drive/base_requests.h" - -namespace google_apis { - -//========================== GetContactGroupsRequest ========================= - -// This class fetches a JSON feed containing a user's contact groups. -class GetContactGroupsRequest : public GetDataRequest { - public: - GetContactGroupsRequest(RequestSender* runner, - const GetDataCallback& callback); - virtual ~GetContactGroupsRequest(); - - void set_feed_url_for_testing(const GURL& url) { - feed_url_for_testing_ = url; - } - - protected: - // Overridden from GetDataRequest. - virtual GURL GetURL() const OVERRIDE; - - private: - // If non-empty, URL of the feed to fetch. - GURL feed_url_for_testing_; - - DISALLOW_COPY_AND_ASSIGN(GetContactGroupsRequest); -}; - -//============================ GetContactsRequest ============================ - -// This class fetches a JSON feed containing a user's contacts. -class GetContactsRequest : public GetDataRequest { - public: - GetContactsRequest(RequestSender* runner, - const std::string& group_id, - const base::Time& min_update_time, - const GetDataCallback& callback); - virtual ~GetContactsRequest(); - - void set_feed_url_for_testing(const GURL& url) { - feed_url_for_testing_ = url; - } - - protected: - // Overridden from GetDataRequest. - virtual GURL GetURL() const OVERRIDE; - - private: - // If non-empty, URL of the feed to fetch. - GURL feed_url_for_testing_; - - // If non-empty, contains the ID of the group whose contacts should be - // returned. Group IDs generally look like this: - // http://www.google.com/m8/feeds/groups/user%40gmail.com/base/6 - std::string group_id_; - - // If is_null() is false, contains a minimum last-updated time that will be - // used to filter contacts. - base::Time min_update_time_; - - DISALLOW_COPY_AND_ASSIGN(GetContactsRequest); -}; - -//========================== GetContactPhotoRequest ========================== - -// This class fetches a contact's photo. -class GetContactPhotoRequest : public UrlFetchRequestBase { - public: - GetContactPhotoRequest(RequestSender* runner, - const GURL& photo_url, - const GetContentCallback& callback); - virtual ~GetContactPhotoRequest(); - - protected: - // Overridden from UrlFetchRequestBase. - virtual GURL GetURL() const OVERRIDE; - virtual void ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE; - virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE; - - private: - // Location of the photo to fetch. - GURL photo_url_; - - // Callback to which the photo data is passed. - GetContentCallback callback_; - - DISALLOW_COPY_AND_ASSIGN(GetContactPhotoRequest); -}; - -} // namespace google_apis - -#endif // GOOGLE_APIS_DRIVE_GDATA_CONTACTS_REQUESTS_H_ diff --git a/chromium/google_apis/drive/gdata_wapi_parser.cc b/chromium/google_apis/drive/gdata_wapi_parser.cc index 82f9d43151f..9c50e5ab524 100644 --- a/chromium/google_apis/drive/gdata_wapi_parser.cc +++ b/chromium/google_apis/drive/gdata_wapi_parser.cc @@ -46,23 +46,6 @@ const char kFeedLinkField[] = "gd$feedLink"; const char kFileNameField[] = "docs$filename.$t"; const char kHrefField[] = "href"; const char kIDField[] = "id.$t"; -const char kInstalledAppField[] = "docs$installedApp"; -const char kInstalledAppNameField[] = "docs$installedAppName"; -const char kInstalledAppIdField[] = "docs$installedAppId"; -const char kInstalledAppIconField[] = "docs$installedAppIcon"; -const char kInstalledAppIconCategoryField[] = "docs$installedAppIconCategory"; -const char kInstalledAppIconSizeField[] = "docs$installedAppIconSize"; -const char kInstalledAppObjectTypeField[] = "docs$installedAppObjectType"; -const char kInstalledAppPrimaryFileExtensionField[] = - "docs$installedAppPrimaryFileExtension"; -const char kInstalledAppPrimaryMimeTypeField[] = - "docs$installedAppPrimaryMimeType"; -const char kInstalledAppSecondaryFileExtensionField[] = - "docs$installedAppSecondaryFileExtension"; -const char kInstalledAppSecondaryMimeTypeField[] = - "docs$installedAppSecondaryMimeType"; -const char kInstalledAppSupportsCreateField[] = - "docs$installedAppSupportsCreate"; const char kItemsPerPageField[] = "openSearch$itemsPerPage.$t"; const char kLabelField[] = "label"; const char kLargestChangestampField[] = "docs$largestChangestamp.value"; @@ -71,8 +54,6 @@ const char kLinkField[] = "link"; const char kMD5Field[] = "docs$md5Checksum.$t"; const char kNameField[] = "name.$t"; const char kPublishedField[] = "published.$t"; -const char kQuotaBytesTotalField[] = "gd$quotaBytesTotal.$t"; -const char kQuotaBytesUsedField[] = "gd$quotaBytesUsed.$t"; const char kRelField[] = "rel"; const char kRemovedField[] = "docs$removed"; const char kResourceIdField[] = "gd$resourceId.$t"; @@ -81,7 +62,6 @@ const char kSizeField[] = "docs$size.$t"; const char kSrcField[] = "src"; const char kStartIndexField[] = "openSearch$startIndex.$t"; const char kSuggestedFileNameField[] = "docs$suggestedFilename.$t"; -const char kTField[] = "$t"; const char kTermField[] = "term"; const char kTitleField[] = "title"; const char kTitleTField[] = "title.$t"; @@ -186,17 +166,6 @@ const CategoryTypeMap kCategoryTypeMap[] = { { Category::CATEGORY_LABEL, "http://schemas.google.com/g/2005/labels" }, }; -struct AppIconCategoryMap { - AppIcon::IconCategory category; - const char* category_name; -}; - -const AppIconCategoryMap kAppIconCategoryMap[] = { - { AppIcon::ICON_DOCUMENT, "document" }, - { AppIcon::ICON_APPLICATION, "application" }, - { AppIcon::ICON_SHARED_DOCUMENT, "documentShared" }, -}; - // Converts |url_string| to |result|. Always returns true to be used // for JSONValueConverter::RegisterCustomField method. // TODO(mukai): make it return false in case of invalid |url_string|. @@ -205,17 +174,6 @@ bool GetGURLFromString(const base::StringPiece& url_string, GURL* result) { return true; } -// Converts boolean string values like "true" into bool. -bool GetBoolFromString(const base::StringPiece& value, bool* result) { - *result = (value == "true"); - return true; -} - -bool SortBySize(const InstalledApp::IconList::value_type& a, - const InstalledApp::IconList::value_type& b) { - return a.first < b.first; -} - } // namespace //////////////////////////////////////////////////////////////////////////////// @@ -378,49 +336,6 @@ void Content::RegisterJSONConverter( } //////////////////////////////////////////////////////////////////////////////// -// AppIcon implementation - -AppIcon::AppIcon() : category_(AppIcon::ICON_UNKNOWN), icon_side_length_(0) { -} - -AppIcon::~AppIcon() { -} - -// static -void AppIcon::RegisterJSONConverter( - base::JSONValueConverter<AppIcon>* converter) { - converter->RegisterCustomField<AppIcon::IconCategory>( - kInstalledAppIconCategoryField, - &AppIcon::category_, - &AppIcon::GetIconCategory); - converter->RegisterCustomField<int>(kInstalledAppIconSizeField, - &AppIcon::icon_side_length_, - base::StringToInt); - converter->RegisterRepeatedMessage(kLinkField, &AppIcon::links_); -} - -GURL AppIcon::GetIconURL() const { - for (size_t i = 0; i < links_.size(); ++i) { - if (links_[i]->type() == Link::LINK_ICON) - return links_[i]->href(); - } - return GURL(); -} - -// static -bool AppIcon::GetIconCategory(const base::StringPiece& category, - AppIcon::IconCategory* result) { - for (size_t i = 0; i < arraysize(kAppIconCategoryMap); i++) { - if (category == kAppIconCategoryMap[i].category_name) { - *result = kAppIconCategoryMap[i].category; - return true; - } - } - DVLOG(1) << "Unknown icon category " << category; - return false; -} - -//////////////////////////////////////////////////////////////////////////////// // CommonMetadata implementation CommonMetadata::CommonMetadata() { @@ -523,9 +438,10 @@ void ResourceEntry::RegisterJSONConverter( // ImageMediaMetadata fields are not supported by WAPI. } -std::string ResourceEntry::GetHostedDocumentExtension() const { +// static +std::string ResourceEntry::GetHostedDocumentExtension(DriveEntryKind kind) { for (size_t i = 0; i < arraysize(kEntryKindMap); i++) { - if (kEntryKindMap[i].kind == kind_) { + if (kEntryKindMap[i].kind == kind) { if (kEntryKindMap[i].extension) return std::string(kEntryKindMap[i].extension); else @@ -536,19 +452,25 @@ std::string ResourceEntry::GetHostedDocumentExtension() const { } // static +DriveEntryKind ResourceEntry::GetEntryKindFromExtension( + const std::string& extension) { + for (size_t i = 0; i < arraysize(kEntryKindMap); ++i) { + const char* document_extension = kEntryKindMap[i].extension; + if (document_extension && extension == document_extension) + return kEntryKindMap[i].kind; + } + return ENTRY_KIND_UNKNOWN; +} + +// static int ResourceEntry::ClassifyEntryKindByFileExtension( const base::FilePath& file_path) { #if defined(OS_WIN) - std::string file_extension = WideToUTF8(file_path.Extension()); + std::string file_extension = base::WideToUTF8(file_path.Extension()); #else std::string file_extension = file_path.Extension(); #endif - for (size_t i = 0; i < arraysize(kEntryKindMap); ++i) { - const char* document_extension = kEntryKindMap[i].extension; - if (document_extension && file_extension == document_extension) - return ClassifyEntryKind(kEntryKindMap[i].kind); - } - return 0; + return ClassifyEntryKind(GetEntryKindFromExtension(file_extension)); } // static @@ -742,142 +664,4 @@ void ResourceList::ReleaseEntries(std::vector<ResourceEntry*>* entries) { entries_.release(entries); } -//////////////////////////////////////////////////////////////////////////////// -// InstalledApp implementation - -InstalledApp::InstalledApp() : supports_create_(false) { -} - -InstalledApp::~InstalledApp() { -} - -InstalledApp::IconList InstalledApp::GetIconsForCategory( - AppIcon::IconCategory category) const { - IconList result; - - for (ScopedVector<AppIcon>::const_iterator icon_iter = app_icons_.begin(); - icon_iter != app_icons_.end(); ++icon_iter) { - if ((*icon_iter)->category() != category) - continue; - GURL icon_url = (*icon_iter)->GetIconURL(); - if (icon_url.is_empty()) - continue; - result.push_back(std::make_pair((*icon_iter)->icon_side_length(), - icon_url)); - } - - // Return a sorted list, smallest to largest. - std::sort(result.begin(), result.end(), SortBySize); - return result; -} - -GURL InstalledApp::GetProductUrl() const { - for (ScopedVector<Link>::const_iterator it = links_.begin(); - it != links_.end(); ++it) { - const Link* link = *it; - if (link->type() == Link::LINK_PRODUCT) - return link->href(); - } - return GURL(); -} - -// static -bool InstalledApp::GetValueString(const base::Value* value, - std::string* result) { - const base::DictionaryValue* dict = NULL; - if (!value->GetAsDictionary(&dict)) - return false; - - if (!dict->GetString(kTField, result)) - return false; - - return true; -} - -// static -void InstalledApp::RegisterJSONConverter( - base::JSONValueConverter<InstalledApp>* converter) { - converter->RegisterRepeatedMessage(kInstalledAppIconField, - &InstalledApp::app_icons_); - converter->RegisterStringField(kInstalledAppIdField, - &InstalledApp::app_id_); - converter->RegisterStringField(kInstalledAppNameField, - &InstalledApp::app_name_); - converter->RegisterStringField(kInstalledAppObjectTypeField, - &InstalledApp::object_type_); - converter->RegisterCustomField<bool>(kInstalledAppSupportsCreateField, - &InstalledApp::supports_create_, - &GetBoolFromString); - converter->RegisterRepeatedCustomValue(kInstalledAppPrimaryMimeTypeField, - &InstalledApp::primary_mimetypes_, - &GetValueString); - converter->RegisterRepeatedCustomValue(kInstalledAppSecondaryMimeTypeField, - &InstalledApp::secondary_mimetypes_, - &GetValueString); - converter->RegisterRepeatedCustomValue(kInstalledAppPrimaryFileExtensionField, - &InstalledApp::primary_extensions_, - &GetValueString); - converter->RegisterRepeatedCustomValue( - kInstalledAppSecondaryFileExtensionField, - &InstalledApp::secondary_extensions_, - &GetValueString); - converter->RegisterRepeatedMessage(kLinkField, &InstalledApp::links_); -} - -//////////////////////////////////////////////////////////////////////////////// -// AccountMetadata implementation - -AccountMetadata::AccountMetadata() - : quota_bytes_total_(0), - quota_bytes_used_(0), - largest_changestamp_(0) { -} - -AccountMetadata::~AccountMetadata() { -} - -// static -void AccountMetadata::RegisterJSONConverter( - base::JSONValueConverter<AccountMetadata>* converter) { - converter->RegisterCustomField<int64>( - kQuotaBytesTotalField, - &AccountMetadata::quota_bytes_total_, - &base::StringToInt64); - converter->RegisterCustomField<int64>( - kQuotaBytesUsedField, - &AccountMetadata::quota_bytes_used_, - &base::StringToInt64); - converter->RegisterCustomField<int64>( - kLargestChangestampField, - &AccountMetadata::largest_changestamp_, - &base::StringToInt64); - converter->RegisterRepeatedMessage(kInstalledAppField, - &AccountMetadata::installed_apps_); -} - -// static -scoped_ptr<AccountMetadata> AccountMetadata::CreateFrom( - const base::Value& value) { - scoped_ptr<AccountMetadata> metadata(new AccountMetadata()); - const base::DictionaryValue* dictionary = NULL; - const base::Value* entry = NULL; - if (!value.GetAsDictionary(&dictionary) || - !dictionary->Get(kEntryField, &entry) || - !metadata->Parse(*entry)) { - LOG(ERROR) << "Unable to create: Invalid account metadata feed!"; - return scoped_ptr<AccountMetadata>(); - } - - return metadata.Pass(); -} - -bool AccountMetadata::Parse(const base::Value& value) { - base::JSONValueConverter<AccountMetadata> converter; - if (!converter.Convert(value, this)) { - LOG(ERROR) << "Unable to parse: Invalid account metadata feed!"; - return false; - } - return true; -} - } // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_parser.h b/chromium/google_apis/drive/gdata_wapi_parser.h index 0c89c11b09e..1081dbcf9cf 100644 --- a/chromium/google_apis/drive/gdata_wapi_parser.h +++ b/chromium/google_apis/drive/gdata_wapi_parser.h @@ -251,60 +251,6 @@ class Content { std::string mime_type_; }; -// This stores a representation of an application icon as registered with the -// installed applications section of the account metadata feed. There can be -// multiple icons registered for each application, differing in size, category -// and MIME type. -class AppIcon { - public: - enum IconCategory { - ICON_UNKNOWN, // Uninitialized state - ICON_DOCUMENT, // Document icon for various MIME types - ICON_APPLICATION, // Application icon for various MIME types - ICON_SHARED_DOCUMENT, // Icon for documents that are shared from other - // users. - }; - - AppIcon(); - ~AppIcon(); - - // Registers the mapping between JSON field names and the members in - // this class. - static void RegisterJSONConverter( - base::JSONValueConverter<AppIcon>* converter); - - // Category of the icon. - IconCategory category() const { return category_; } - - // Size in pixels of one side of the icon (icons are always square). - int icon_side_length() const { return icon_side_length_; } - - // Get a list of links available for this AppIcon. - const ScopedVector<Link>& links() const { return links_; } - - // Get the icon URL from the internal list of links. Returns the first - // icon URL found in the list. - GURL GetIconURL() const; - - void set_category(IconCategory category) { category_ = category; } - void set_icon_side_length(int icon_side_length) { - icon_side_length_ = icon_side_length; - } - void set_links(ScopedVector<Link> links) { links_ = links.Pass(); } - - private: - // Extracts the icon category from the given string. Returns false and does - // not change |result| when |scheme| has an unrecognizable value. - static bool GetIconCategory(const base::StringPiece& category, - IconCategory* result); - - IconCategory category_; - int icon_side_length_; - ScopedVector<Link> links_; - - DISALLOW_COPY_AND_ASSIGN(AppIcon); -}; - // Base class for feed entries. This class defines fields commonly used by // various feeds. class CommonMetadata { @@ -464,13 +410,17 @@ class ResourceEntry : public CommonMetadata { // If doesn't exist, then equals -1. int64 image_rotation() const { return image_rotation_; } + // The time of this modification. + // Note: This property is filled only when converted from ChangeResource. + const base::Time& modification_date() const { return modification_date_; } + // Text version of resource entry kind. Returns an empty string for // unknown entry kind. std::string GetEntryKindText() const; // Returns preferred file extension for hosted documents. If entry is not // a hosted document, this call returns an empty string. - std::string GetHostedDocumentExtension() const; + static std::string GetHostedDocumentExtension(DriveEntryKind kind); // True if resource entry is remotely hosted. bool is_hosted_document() const { @@ -509,6 +459,9 @@ class ResourceEntry : public CommonMetadata { KIND_OF_FILE = 1 << 4, }; + // Returns the kind enum corresponding to the extension in form ".xxx". + static DriveEntryKind GetEntryKindFromExtension(const std::string& extension); + // Classifies the EntryKind. The returned value is a bitmask of // EntryKindClass. For example, DOCUMENT is classified as // KIND_OF_HOSTED_DOCUMENT and KIND_OF_GOOGLE_DOCUMENT, hence the returned @@ -554,6 +507,9 @@ class ResourceEntry : public CommonMetadata { void set_image_rotation(int64 image_rotation) { image_rotation_ = image_rotation; } + void set_modification_date(const base::Time& modification_date) { + modification_date_ = modification_date; + } // Fills the remaining fields where JSONValueConverter cannot catch. // Currently, sets |kind_| and |labels_| based on the |categories_| in the @@ -592,6 +548,8 @@ class ResourceEntry : public CommonMetadata { int64 image_height_; int64 image_rotation_; + base::Time modification_date_; + DISALLOW_COPY_AND_ASSIGN(ResourceEntry); }; @@ -682,185 +640,6 @@ class ResourceList : public CommonMetadata { DISALLOW_COPY_AND_ASSIGN(ResourceList); }; -// Metadata representing installed Google Drive application. -class InstalledApp { - public: - typedef std::vector<std::pair<int, GURL> > IconList; - - InstalledApp(); - virtual ~InstalledApp(); - - // WebApp name. - const std::string& app_name() const { return app_name_; } - - // Drive app id - const std::string& app_id() const { return app_id_; } - - // Object (file) type name that is generated by this WebApp. - const std::string& object_type() const { return object_type_; } - - // True if WebApp supports creation of new file instances. - bool supports_create() const { return supports_create_; } - - // List of primary mime types supported by this WebApp. Primary status should - // trigger this WebApp becoming the default handler of file instances that - // have these mime types. - const ScopedVector<std::string>& primary_mimetypes() const { - return primary_mimetypes_; - } - - // List of secondary mime types supported by this WebApp. Secondary status - // should make this WebApp show up in "Open with..." pop-up menu of the - // default action menu for file with matching mime types. - const ScopedVector<std::string>& secondary_mimetypes() const { - return secondary_mimetypes_; - } - - // List of primary file extensions supported by this WebApp. Primary status - // should trigger this WebApp becoming the default handler of file instances - // that match these extensions. - const ScopedVector<std::string>& primary_extensions() const { - return primary_extensions_; - } - - // List of secondary file extensions supported by this WebApp. Secondary - // status should make this WebApp show up in "Open with..." pop-up menu of the - // default action menu for file with matching extensions. - const ScopedVector<std::string>& secondary_extensions() const { - return secondary_extensions_; - } - - // List of entry links. - const ScopedVector<Link>& links() const { return links_; } - - // Returns a list of icons associated with this installed application. - const ScopedVector<AppIcon>& app_icons() const { - return app_icons_; - } - - // Convenience function for getting the icon URLs for a particular |category| - // of icon. Icons are returned in a sorted list, from smallest to largest. - IconList GetIconsForCategory(AppIcon::IconCategory category) const; - - // Retrieves product URL from the link collection. - GURL GetProductUrl() const; - - // Registers the mapping between JSON field names and the members in - // this class. - static void RegisterJSONConverter( - base::JSONValueConverter<InstalledApp>* converter); - - void set_app_id(const std::string& app_id) { app_id_ = app_id; } - void set_app_name(const std::string& app_name) { app_name_ = app_name; } - void set_object_type(const std::string& object_type) { - object_type_ = object_type; - } - void set_supports_create(bool supports_create) { - supports_create_ = supports_create; - } - void set_primary_mimetypes( - ScopedVector<std::string> primary_mimetypes) { - primary_mimetypes_ = primary_mimetypes.Pass(); - } - void set_secondary_mimetypes( - ScopedVector<std::string> secondary_mimetypes) { - secondary_mimetypes_ = secondary_mimetypes.Pass(); - } - void set_primary_extensions( - ScopedVector<std::string> primary_extensions) { - primary_extensions_ = primary_extensions.Pass(); - } - void set_secondary_extensions( - ScopedVector<std::string> secondary_extensions) { - secondary_extensions_ = secondary_extensions.Pass(); - } - void set_links(ScopedVector<Link> links) { - links_ = links.Pass(); - } - void set_app_icons(ScopedVector<AppIcon> app_icons) { - app_icons_ = app_icons.Pass(); - } - - private: - // Extracts "$t" value from the dictionary |value| and returns it in |result|. - // If the string value can't be found, it returns false. - static bool GetValueString(const base::Value* value, - std::string* result); - - std::string app_id_; - std::string app_name_; - std::string object_type_; - bool supports_create_; - ScopedVector<std::string> primary_mimetypes_; - ScopedVector<std::string> secondary_mimetypes_; - ScopedVector<std::string> primary_extensions_; - ScopedVector<std::string> secondary_extensions_; - ScopedVector<Link> links_; - ScopedVector<AppIcon> app_icons_; -}; - -// Account metadata feed represents the metadata object attached to the user's -// account. -class AccountMetadata { - public: - AccountMetadata(); - virtual ~AccountMetadata(); - - // Creates feed from parsed JSON Value. You should call this - // instead of instantiating JSONValueConverter by yourself because - // this method does some post-process for some fields. See - // FillRemainingFields comment and implementation in ResourceEntry - // class for the details. - static scoped_ptr<AccountMetadata> CreateFrom(const base::Value& value); - - int64 quota_bytes_total() const { - return quota_bytes_total_; - } - - int64 quota_bytes_used() const { - return quota_bytes_used_; - } - - int64 largest_changestamp() const { - return largest_changestamp_; - } - - const ScopedVector<InstalledApp>& installed_apps() const { - return installed_apps_; - } - - void set_quota_bytes_total(int64 quota_bytes_total) { - quota_bytes_total_ = quota_bytes_total; - } - void set_quota_bytes_used(int64 quota_bytes_used) { - quota_bytes_used_ = quota_bytes_used; - } - void set_largest_changestamp(int64 largest_changestamp) { - largest_changestamp_ = largest_changestamp; - } - void set_installed_apps(ScopedVector<InstalledApp> installed_apps) { - installed_apps_ = installed_apps.Pass(); - } - - // Registers the mapping between JSON field names and the members in - // this class. - static void RegisterJSONConverter( - base::JSONValueConverter<AccountMetadata>* converter); - - private: - // Parses and initializes data members from content of |value|. - // Return false if parsing fails. - bool Parse(const base::Value& value); - - int64 quota_bytes_total_; - int64 quota_bytes_used_; - int64 largest_changestamp_; - ScopedVector<InstalledApp> installed_apps_; - - DISALLOW_COPY_AND_ASSIGN(AccountMetadata); -}; - - } // namespace google_apis #endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_PARSER_H_ diff --git a/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc b/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc index 58728d129ba..6655aed8cb0 100644 --- a/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc +++ b/chromium/google_apis/drive/gdata_wapi_parser_unittest.cc @@ -17,8 +17,6 @@ namespace google_apis { -// TODO(nhiroki): Move json files to out of 'chromeos' directory -// (http://crbug.com/149788). // Test document feed parsing. TEST(GDataWAPIParserTest, ResourceListJsonParser) { std::string error; @@ -237,73 +235,6 @@ TEST(GDataWAPIParserTest, ResourceEntryJsonParser) { EXPECT_EQ(-1, entry->image_rotation()); } -TEST(GDataWAPIParserTest, AccountMetadataParser) { - scoped_ptr<base::Value> document = - test_util::LoadJSONFile("gdata/account_metadata.json"); - ASSERT_TRUE(document.get()); - base::DictionaryValue* document_dict = NULL; - base::DictionaryValue* entry_value = NULL; - ASSERT_TRUE(document->GetAsDictionary(&document_dict)); - ASSERT_TRUE(document_dict->GetDictionary(std::string("entry"), &entry_value)); - ASSERT_TRUE(entry_value); - - scoped_ptr<AccountMetadata> metadata( - AccountMetadata::CreateFrom(*document)); - ASSERT_TRUE(metadata.get()); - EXPECT_EQ(GG_LONGLONG(6789012345), metadata->quota_bytes_used()); - EXPECT_EQ(GG_LONGLONG(9876543210), metadata->quota_bytes_total()); - EXPECT_EQ(654321, metadata->largest_changestamp()); - EXPECT_EQ(2U, metadata->installed_apps().size()); - const InstalledApp* first_app = metadata->installed_apps()[0]; - const InstalledApp* second_app = metadata->installed_apps()[1]; - - ASSERT_TRUE(first_app); - EXPECT_EQ("Drive App 1", first_app->app_name()); - EXPECT_EQ("Drive App Object 1", first_app->object_type()); - EXPECT_TRUE(first_app->supports_create()); - EXPECT_EQ("https://chrome.google.com/webstore/detail/abcdefabcdef", - first_app->GetProductUrl().spec()); - - ASSERT_EQ(2U, first_app->primary_mimetypes().size()); - EXPECT_EQ("application/test_type_1", - *first_app->primary_mimetypes()[0]); - EXPECT_EQ("application/vnd.google-apps.drive-sdk.11111111", - *first_app->primary_mimetypes()[1]); - - ASSERT_EQ(1U, first_app->secondary_mimetypes().size()); - EXPECT_EQ("image/jpeg", *first_app->secondary_mimetypes()[0]); - - ASSERT_EQ(2U, first_app->primary_extensions().size()); - EXPECT_EQ("ext_1", *first_app->primary_extensions()[0]); - EXPECT_EQ("ext_2", *first_app->primary_extensions()[1]); - - ASSERT_EQ(1U, first_app->secondary_extensions().size()); - EXPECT_EQ("ext_3", *first_app->secondary_extensions()[0]); - - ASSERT_EQ(1U, first_app->app_icons().size()); - EXPECT_EQ(AppIcon::ICON_DOCUMENT, first_app->app_icons()[0]->category()); - EXPECT_EQ(16, first_app->app_icons()[0]->icon_side_length()); - GURL icon_url = first_app->app_icons()[0]->GetIconURL(); - EXPECT_EQ("https://www.google.com/images/srpr/logo3w.png", icon_url.spec()); - InstalledApp::IconList icons = - first_app->GetIconsForCategory(AppIcon::ICON_DOCUMENT); - EXPECT_EQ("https://www.google.com/images/srpr/logo3w.png", - icons[0].second.spec()); - icons = first_app->GetIconsForCategory(AppIcon::ICON_SHARED_DOCUMENT); - EXPECT_TRUE(icons.empty()); - - ASSERT_TRUE(second_app); - EXPECT_EQ("Drive App 2", second_app->app_name()); - EXPECT_EQ("Drive App Object 2", second_app->object_type()); - EXPECT_EQ("https://chrome.google.com/webstore/detail/deadbeefdeadbeef", - second_app->GetProductUrl().spec()); - EXPECT_FALSE(second_app->supports_create()); - EXPECT_EQ(2U, second_app->primary_mimetypes().size()); - EXPECT_EQ(0U, second_app->secondary_mimetypes().size()); - EXPECT_EQ(1U, second_app->primary_extensions().size()); - EXPECT_EQ(0U, second_app->secondary_extensions().size()); -} - TEST(GDataWAPIParserTest, ClassifyEntryKindByFileExtension) { EXPECT_EQ( ResourceEntry::KIND_OF_GOOGLE_DOCUMENT | diff --git a/chromium/google_apis/drive/gdata_wapi_requests.cc b/chromium/google_apis/drive/gdata_wapi_requests.cc index 1ae8a95ef08..42592172075 100644 --- a/chromium/google_apis/drive/gdata_wapi_requests.cc +++ b/chromium/google_apis/drive/gdata_wapi_requests.cc @@ -4,202 +4,10 @@ #include "google_apis/drive/gdata_wapi_requests.h" -#include "base/location.h" -#include "base/sequenced_task_runner.h" -#include "base/task_runner_util.h" -#include "base/values.h" -#include "google_apis/drive/gdata_wapi_parser.h" #include "google_apis/drive/gdata_wapi_url_generator.h" -#include "google_apis/drive/request_sender.h" -#include "google_apis/drive/request_util.h" -#include "third_party/libxml/chromium/libxml_utils.h" - -using net::URLFetcher; namespace google_apis { -namespace { - -// Parses the JSON value to ResourceList. -scoped_ptr<ResourceList> ParseResourceListOnBlockingPool( - scoped_ptr<base::Value> value) { - DCHECK(value); - - return ResourceList::ExtractAndParse(*value); -} - -// Runs |callback| with |error| and |resource_list|, but replace the error code -// with GDATA_PARSE_ERROR, if there was a parsing error. -void DidParseResourceListOnBlockingPool( - const GetResourceListCallback& callback, - GDataErrorCode error, - scoped_ptr<ResourceList> resource_list) { - DCHECK(!callback.is_null()); - - // resource_list being NULL indicates there was a parsing error. - if (!resource_list) - error = GDATA_PARSE_ERROR; - - callback.Run(error, resource_list.Pass()); -} - -// Parses the JSON value to ResourceList on the blocking pool and runs -// |callback| on the UI thread once parsing is done. -void ParseResourceListAndRun( - scoped_refptr<base::TaskRunner> blocking_task_runner, - const GetResourceListCallback& callback, - GDataErrorCode error, - scoped_ptr<base::Value> value) { - DCHECK(!callback.is_null()); - - if (!value) { - callback.Run(error, scoped_ptr<ResourceList>()); - return; - } - - base::PostTaskAndReplyWithResult( - blocking_task_runner, - FROM_HERE, - base::Bind(&ParseResourceListOnBlockingPool, base::Passed(&value)), - base::Bind(&DidParseResourceListOnBlockingPool, callback, error)); -} - -// Parses the JSON value to AccountMetadata and runs |callback| on the UI -// thread once parsing is done. -void ParseAccounetMetadataAndRun(const GetAccountMetadataCallback& callback, - GDataErrorCode error, - scoped_ptr<base::Value> value) { - DCHECK(!callback.is_null()); - - if (!value) { - callback.Run(error, scoped_ptr<AccountMetadata>()); - return; - } - - // Parsing AccountMetadata is cheap enough to do on UI thread. - scoped_ptr<AccountMetadata> entry = - google_apis::AccountMetadata::CreateFrom(*value); - if (!entry) { - callback.Run(GDATA_PARSE_ERROR, scoped_ptr<AccountMetadata>()); - return; - } - - callback.Run(error, entry.Pass()); -} - -// Parses the |value| to ResourceEntry with error handling. -// This is designed to be used for ResumeUploadRequest and -// GetUploadStatusRequest. -scoped_ptr<ResourceEntry> ParseResourceEntry(scoped_ptr<base::Value> value) { - scoped_ptr<ResourceEntry> entry; - if (value.get()) { - entry = ResourceEntry::ExtractAndParse(*value); - - // Note: |value| may be NULL, in particular if the callback is for a - // failure. - if (!entry.get()) - LOG(WARNING) << "Invalid entry received on upload."; - } - - return entry.Pass(); -} - -// Extracts the open link url from the JSON Feed. Used by AuthorizeApp(). -void ParseOpenLinkAndRun(const std::string& app_id, - const AuthorizeAppCallback& callback, - GDataErrorCode error, - scoped_ptr<base::Value> value) { - DCHECK(!callback.is_null()); - - if (!value) { - callback.Run(error, GURL()); - return; - } - - // Parsing ResourceEntry is cheap enough to do on UI thread. - scoped_ptr<ResourceEntry> resource_entry = ParseResourceEntry(value.Pass()); - if (!resource_entry) { - callback.Run(GDATA_PARSE_ERROR, GURL()); - return; - } - - // Look for the link to open the file with the app with |app_id|. - const ScopedVector<Link>& resource_links = resource_entry->links(); - GURL open_link; - for (size_t i = 0; i < resource_links.size(); ++i) { - const Link& link = *resource_links[i]; - if (link.type() == Link::LINK_OPEN_WITH && link.app_id() == app_id) { - open_link = link.href(); - break; - } - } - - if (open_link.is_empty()) - error = GDATA_OTHER_ERROR; - - callback.Run(error, open_link); -} - -} // namespace - -//============================ GetResourceListRequest ======================== - -GetResourceListRequest::GetResourceListRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const GURL& override_url, - int64 start_changestamp, - const std::string& search_string, - const std::string& directory_resource_id, - const GetResourceListCallback& callback) - : GetDataRequest( - sender, - base::Bind(&ParseResourceListAndRun, - make_scoped_refptr(sender->blocking_task_runner()), - callback)), - url_generator_(url_generator), - override_url_(override_url), - start_changestamp_(start_changestamp), - search_string_(search_string), - directory_resource_id_(directory_resource_id) { - DCHECK(!callback.is_null()); -} - -GetResourceListRequest::~GetResourceListRequest() {} - -GURL GetResourceListRequest::GetURL() const { - return url_generator_.GenerateResourceListUrl(override_url_, - start_changestamp_, - search_string_, - directory_resource_id_); -} - -//============================ SearchByTitleRequest ========================== - -SearchByTitleRequest::SearchByTitleRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const std::string& title, - const std::string& directory_resource_id, - const GetResourceListCallback& callback) - : GetDataRequest( - sender, - base::Bind(&ParseResourceListAndRun, - make_scoped_refptr(sender->blocking_task_runner()), - callback)), - url_generator_(url_generator), - title_(title), - directory_resource_id_(directory_resource_id) { - DCHECK(!callback.is_null()); -} - -SearchByTitleRequest::~SearchByTitleRequest() {} - -GURL SearchByTitleRequest::GetURL() const { - return url_generator_.GenerateSearchByTitleUrl( - title_, directory_resource_id_); -} - //============================ GetResourceEntryRequest ======================= GetResourceEntryRequest::GetResourceEntryRequest( @@ -222,459 +30,4 @@ GURL GetResourceEntryRequest::GetURL() const { resource_id_, embed_origin_); } -//========================= GetAccountMetadataRequest ======================== - -GetAccountMetadataRequest::GetAccountMetadataRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const GetAccountMetadataCallback& callback, - bool include_installed_apps) - : GetDataRequest(sender, - base::Bind(&ParseAccounetMetadataAndRun, callback)), - url_generator_(url_generator), - include_installed_apps_(include_installed_apps) { - DCHECK(!callback.is_null()); -} - -GetAccountMetadataRequest::~GetAccountMetadataRequest() {} - -GURL GetAccountMetadataRequest::GetURL() const { - return url_generator_.GenerateAccountMetadataUrl(include_installed_apps_); -} - -//=========================== DeleteResourceRequest ========================== - -DeleteResourceRequest::DeleteResourceRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& resource_id, - const std::string& etag) - : EntryActionRequest(sender, callback), - url_generator_(url_generator), - resource_id_(resource_id), - etag_(etag) { - DCHECK(!callback.is_null()); -} - -DeleteResourceRequest::~DeleteResourceRequest() {} - -GURL DeleteResourceRequest::GetURL() const { - return url_generator_.GenerateEditUrl(resource_id_); -} - -URLFetcher::RequestType DeleteResourceRequest::GetRequestType() const { - return URLFetcher::DELETE_REQUEST; -} - -std::vector<std::string> -DeleteResourceRequest::GetExtraRequestHeaders() const { - std::vector<std::string> headers; - headers.push_back(util::GenerateIfMatchHeader(etag_)); - return headers; -} - -//========================== CreateDirectoryRequest ========================== - -CreateDirectoryRequest::CreateDirectoryRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const GetDataCallback& callback, - const std::string& parent_resource_id, - const std::string& directory_title) - : GetDataRequest(sender, callback), - url_generator_(url_generator), - parent_resource_id_(parent_resource_id), - directory_title_(directory_title) { - DCHECK(!callback.is_null()); -} - -CreateDirectoryRequest::~CreateDirectoryRequest() {} - -GURL CreateDirectoryRequest::GetURL() const { - return url_generator_.GenerateContentUrl(parent_resource_id_); -} - -URLFetcher::RequestType -CreateDirectoryRequest::GetRequestType() const { - return URLFetcher::POST; -} - -bool CreateDirectoryRequest::GetContentData(std::string* upload_content_type, - std::string* upload_content) { - upload_content_type->assign("application/atom+xml"); - XmlWriter xml_writer; - xml_writer.StartWriting(); - xml_writer.StartElement("entry"); - xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); - - xml_writer.StartElement("category"); - xml_writer.AddAttribute("scheme", - "http://schemas.google.com/g/2005#kind"); - xml_writer.AddAttribute("term", - "http://schemas.google.com/docs/2007#folder"); - xml_writer.EndElement(); // Ends "category" element. - - xml_writer.WriteElement("title", directory_title_); - - xml_writer.EndElement(); // Ends "entry" element. - xml_writer.StopWriting(); - upload_content->assign(xml_writer.GetWrittenString()); - DVLOG(1) << "CreateDirectory data: " << *upload_content_type << ", [" - << *upload_content << "]"; - return true; -} - -//=========================== RenameResourceRequest ========================== - -RenameResourceRequest::RenameResourceRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& resource_id, - const std::string& new_title) - : EntryActionRequest(sender, callback), - url_generator_(url_generator), - resource_id_(resource_id), - new_title_(new_title) { - DCHECK(!callback.is_null()); -} - -RenameResourceRequest::~RenameResourceRequest() {} - -URLFetcher::RequestType RenameResourceRequest::GetRequestType() const { - return URLFetcher::PUT; -} - -std::vector<std::string> -RenameResourceRequest::GetExtraRequestHeaders() const { - std::vector<std::string> headers; - headers.push_back(util::kIfMatchAllHeader); - return headers; -} - -GURL RenameResourceRequest::GetURL() const { - return url_generator_.GenerateEditUrl(resource_id_); -} - -bool RenameResourceRequest::GetContentData(std::string* upload_content_type, - std::string* upload_content) { - upload_content_type->assign("application/atom+xml"); - XmlWriter xml_writer; - xml_writer.StartWriting(); - xml_writer.StartElement("entry"); - xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); - - xml_writer.WriteElement("title", new_title_); - - xml_writer.EndElement(); // Ends "entry" element. - xml_writer.StopWriting(); - upload_content->assign(xml_writer.GetWrittenString()); - DVLOG(1) << "RenameResourceRequest data: " << *upload_content_type << ", [" - << *upload_content << "]"; - return true; -} - -//=========================== AuthorizeAppRequest ========================== - -AuthorizeAppRequest::AuthorizeAppRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const AuthorizeAppCallback& callback, - const std::string& resource_id, - const std::string& app_id) - : GetDataRequest(sender, - base::Bind(&ParseOpenLinkAndRun, app_id, callback)), - url_generator_(url_generator), - resource_id_(resource_id), - app_id_(app_id) { - DCHECK(!callback.is_null()); -} - -AuthorizeAppRequest::~AuthorizeAppRequest() {} - -URLFetcher::RequestType AuthorizeAppRequest::GetRequestType() const { - return URLFetcher::PUT; -} - -std::vector<std::string> -AuthorizeAppRequest::GetExtraRequestHeaders() const { - std::vector<std::string> headers; - headers.push_back(util::kIfMatchAllHeader); - return headers; -} - -bool AuthorizeAppRequest::GetContentData(std::string* upload_content_type, - std::string* upload_content) { - upload_content_type->assign("application/atom+xml"); - XmlWriter xml_writer; - xml_writer.StartWriting(); - xml_writer.StartElement("entry"); - xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); - xml_writer.AddAttribute("xmlns:docs", "http://schemas.google.com/docs/2007"); - xml_writer.WriteElement("docs:authorizedApp", app_id_); - - xml_writer.EndElement(); // Ends "entry" element. - xml_writer.StopWriting(); - upload_content->assign(xml_writer.GetWrittenString()); - DVLOG(1) << "AuthorizeAppRequest data: " << *upload_content_type << ", [" - << *upload_content << "]"; - return true; -} - -GURL AuthorizeAppRequest::GetURL() const { - return url_generator_.GenerateEditUrl(resource_id_); -} - -//======================= AddResourceToDirectoryRequest ====================== - -AddResourceToDirectoryRequest::AddResourceToDirectoryRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& parent_resource_id, - const std::string& resource_id) - : EntryActionRequest(sender, callback), - url_generator_(url_generator), - parent_resource_id_(parent_resource_id), - resource_id_(resource_id) { - DCHECK(!callback.is_null()); -} - -AddResourceToDirectoryRequest::~AddResourceToDirectoryRequest() {} - -GURL AddResourceToDirectoryRequest::GetURL() const { - return url_generator_.GenerateContentUrl(parent_resource_id_); -} - -URLFetcher::RequestType -AddResourceToDirectoryRequest::GetRequestType() const { - return URLFetcher::POST; -} - -bool AddResourceToDirectoryRequest::GetContentData( - std::string* upload_content_type, std::string* upload_content) { - upload_content_type->assign("application/atom+xml"); - XmlWriter xml_writer; - xml_writer.StartWriting(); - xml_writer.StartElement("entry"); - xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); - - xml_writer.WriteElement( - "id", url_generator_.GenerateEditUrlWithoutParams(resource_id_).spec()); - - xml_writer.EndElement(); // Ends "entry" element. - xml_writer.StopWriting(); - upload_content->assign(xml_writer.GetWrittenString()); - DVLOG(1) << "AddResourceToDirectoryRequest data: " << *upload_content_type - << ", [" << *upload_content << "]"; - return true; -} - -//==================== RemoveResourceFromDirectoryRequest ==================== - -RemoveResourceFromDirectoryRequest::RemoveResourceFromDirectoryRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& parent_resource_id, - const std::string& document_resource_id) - : EntryActionRequest(sender, callback), - url_generator_(url_generator), - resource_id_(document_resource_id), - parent_resource_id_(parent_resource_id) { - DCHECK(!callback.is_null()); -} - -RemoveResourceFromDirectoryRequest::~RemoveResourceFromDirectoryRequest() { -} - -GURL RemoveResourceFromDirectoryRequest::GetURL() const { - return url_generator_.GenerateResourceUrlForRemoval( - parent_resource_id_, resource_id_); -} - -URLFetcher::RequestType -RemoveResourceFromDirectoryRequest::GetRequestType() const { - return URLFetcher::DELETE_REQUEST; -} - -std::vector<std::string> -RemoveResourceFromDirectoryRequest::GetExtraRequestHeaders() const { - std::vector<std::string> headers; - headers.push_back(util::kIfMatchAllHeader); - return headers; -} - -//======================= InitiateUploadNewFileRequest ======================= - -InitiateUploadNewFileRequest::InitiateUploadNewFileRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const InitiateUploadCallback& callback, - const std::string& content_type, - int64 content_length, - const std::string& parent_resource_id, - const std::string& title) - : InitiateUploadRequestBase(sender, callback, content_type, content_length), - url_generator_(url_generator), - parent_resource_id_(parent_resource_id), - title_(title) { -} - -InitiateUploadNewFileRequest::~InitiateUploadNewFileRequest() {} - -GURL InitiateUploadNewFileRequest::GetURL() const { - return url_generator_.GenerateInitiateUploadNewFileUrl(parent_resource_id_); -} - -net::URLFetcher::RequestType -InitiateUploadNewFileRequest::GetRequestType() const { - return net::URLFetcher::POST; -} - -bool InitiateUploadNewFileRequest::GetContentData( - std::string* upload_content_type, - std::string* upload_content) { - upload_content_type->assign("application/atom+xml"); - XmlWriter xml_writer; - xml_writer.StartWriting(); - xml_writer.StartElement("entry"); - xml_writer.AddAttribute("xmlns", "http://www.w3.org/2005/Atom"); - xml_writer.AddAttribute("xmlns:docs", - "http://schemas.google.com/docs/2007"); - xml_writer.WriteElement("title", title_); - xml_writer.EndElement(); // Ends "entry" element. - xml_writer.StopWriting(); - upload_content->assign(xml_writer.GetWrittenString()); - DVLOG(1) << "InitiateUploadNewFile: " << *upload_content_type << ", [" - << *upload_content << "]"; - return true; -} - -//===================== InitiateUploadExistingFileRequest ==================== - -InitiateUploadExistingFileRequest::InitiateUploadExistingFileRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const InitiateUploadCallback& callback, - const std::string& content_type, - int64 content_length, - const std::string& resource_id, - const std::string& etag) - : InitiateUploadRequestBase(sender, callback, content_type, content_length), - url_generator_(url_generator), - resource_id_(resource_id), - etag_(etag) { -} - -InitiateUploadExistingFileRequest::~InitiateUploadExistingFileRequest() {} - -GURL InitiateUploadExistingFileRequest::GetURL() const { - return url_generator_.GenerateInitiateUploadExistingFileUrl(resource_id_); -} - -net::URLFetcher::RequestType -InitiateUploadExistingFileRequest::GetRequestType() const { - return net::URLFetcher::PUT; -} - -bool InitiateUploadExistingFileRequest::GetContentData( - std::string* upload_content_type, - std::string* upload_content) { - // According to the document there is no need to send the content-type. - // However, the server would return 500 server error without the - // content-type. - // As its workaround, send "text/plain" content-type here. - *upload_content_type = "text/plain"; - *upload_content = ""; - return true; -} - -std::vector<std::string> -InitiateUploadExistingFileRequest::GetExtraRequestHeaders() const { - std::vector<std::string> headers( - InitiateUploadRequestBase::GetExtraRequestHeaders()); - headers.push_back(util::GenerateIfMatchHeader(etag_)); - return headers; -} - -//============================ ResumeUploadRequest =========================== - -ResumeUploadRequest::ResumeUploadRequest( - RequestSender* sender, - const UploadRangeCallback& callback, - const ProgressCallback& progress_callback, - const GURL& upload_location, - int64 start_position, - int64 end_position, - int64 content_length, - const std::string& content_type, - const base::FilePath& local_file_path) - : ResumeUploadRequestBase(sender, - upload_location, - start_position, - end_position, - content_length, - content_type, - local_file_path), - callback_(callback), - progress_callback_(progress_callback) { - DCHECK(!callback_.is_null()); -} - -ResumeUploadRequest::~ResumeUploadRequest() {} - -void ResumeUploadRequest::OnRangeRequestComplete( - const UploadRangeResponse& response, scoped_ptr<base::Value> value) { - callback_.Run(response, ParseResourceEntry(value.Pass())); -} - -void ResumeUploadRequest::OnURLFetchUploadProgress( - const URLFetcher* source, int64 current, int64 total) { - if (!progress_callback_.is_null()) - progress_callback_.Run(current, total); -} - -//========================== GetUploadStatusRequest ========================== - -GetUploadStatusRequest::GetUploadStatusRequest( - RequestSender* sender, - const UploadRangeCallback& callback, - const GURL& upload_url, - int64 content_length) - : GetUploadStatusRequestBase(sender, upload_url, content_length), - callback_(callback) { - DCHECK(!callback.is_null()); -} - -GetUploadStatusRequest::~GetUploadStatusRequest() {} - -void GetUploadStatusRequest::OnRangeRequestComplete( - const UploadRangeResponse& response, scoped_ptr<base::Value> value) { - callback_.Run(response, ParseResourceEntry(value.Pass())); -} - -//========================== DownloadFileRequest ========================== - -DownloadFileRequest::DownloadFileRequest( - RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const DownloadActionCallback& download_action_callback, - const GetContentCallback& get_content_callback, - const ProgressCallback& progress_callback, - const std::string& resource_id, - const base::FilePath& output_file_path) - : DownloadFileRequestBase( - sender, - download_action_callback, - get_content_callback, - progress_callback, - url_generator.GenerateDownloadFileUrl(resource_id), - output_file_path) { -} - -DownloadFileRequest::~DownloadFileRequest() { -} - } // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_requests.h b/chromium/google_apis/drive/gdata_wapi_requests.h index a4587d7628b..7e64906ca48 100644 --- a/chromium/google_apis/drive/gdata_wapi_requests.h +++ b/chromium/google_apis/drive/gdata_wapi_requests.h @@ -6,96 +6,12 @@ #define GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_ #include <string> -#include <vector> #include "google_apis/drive/base_requests.h" -#include "google_apis/drive/drive_common_callbacks.h" #include "google_apis/drive/gdata_wapi_url_generator.h" namespace google_apis { -class AccountMetadata; -class GDataWapiUrlGenerator; -class ResourceEntry; - -//============================ GetResourceListRequest ======================== - -// This class performs the request for fetching a resource list. -class GetResourceListRequest : public GetDataRequest { - public: - // override_url: - // If empty, a hard-coded base URL of the WAPI server is used to fetch - // the first page of the feed. This parameter is used for fetching 2nd - // page and onward. - // - // start_changestamp: - // This parameter specifies the starting point of a delta feed or 0 if a - // full feed is necessary. - // - // search_string: - // If non-empty, fetches a list of resources that match the search - // string. - // - // directory_resource_id: - // If non-empty, fetches a list of resources in a particular directory. - // - // callback: - // Called once the feed is fetched. Must not be null. - GetResourceListRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const GURL& override_url, - int64 start_changestamp, - const std::string& search_string, - const std::string& directory_resource_id, - const GetResourceListCallback& callback); - virtual ~GetResourceListRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const GURL override_url_; - const int64 start_changestamp_; - const std::string search_string_; - const std::string directory_resource_id_; - - DISALLOW_COPY_AND_ASSIGN(GetResourceListRequest); -}; - -//============================ SearchByTitleRequest ========================== - -// This class performs the request for searching resources by title. -class SearchByTitleRequest : public GetDataRequest { - public: - // title: the search query. - // - // directory_resource_id: If given (non-empty), the search target is - // directly under the directory with the |directory_resource_id|. - // If empty, the search target is all the existing resources. - // - // callback: - // Called once the feed is fetched. Must not be null. - SearchByTitleRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const std::string& title, - const std::string& directory_resource_id, - const GetResourceListCallback& callback); - virtual ~SearchByTitleRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string title_; - const std::string directory_resource_id_; - - DISALLOW_COPY_AND_ASSIGN(SearchByTitleRequest); -}; - //========================= GetResourceEntryRequest ========================== // This class performs the request for fetching a single resource entry. @@ -123,364 +39,6 @@ class GetResourceEntryRequest : public GetDataRequest { DISALLOW_COPY_AND_ASSIGN(GetResourceEntryRequest); }; -//========================= GetAccountMetadataRequest ======================== - -// Callback used for GetAccountMetadata(). -typedef base::Callback<void(GDataErrorCode error, - scoped_ptr<AccountMetadata> account_metadata)> - GetAccountMetadataCallback; - -// This class performs the request for fetching account metadata. -class GetAccountMetadataRequest : public GetDataRequest { - public: - // If |include_installed_apps| is set to true, the result should include - // the list of installed third party applications. - // |callback| must not be null. - GetAccountMetadataRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const GetAccountMetadataCallback& callback, - bool include_installed_apps); - virtual ~GetAccountMetadataRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const bool include_installed_apps_; - - DISALLOW_COPY_AND_ASSIGN(GetAccountMetadataRequest); -}; - -//=========================== DeleteResourceRequest ========================== - -// This class performs the request for deleting a resource. -// -// In WAPI, "gd:deleted" means that the resource was put in the trash, and -// "docs:removed" means its permanently gone. Since what the class does is to -// put the resource into trash, we have chosen "Delete" in the name, even though -// we are preferring the term "Remove" in drive/google_api code. -class DeleteResourceRequest : public EntryActionRequest { - public: - // |callback| must not be null. - DeleteResourceRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& resource_id, - const std::string& etag); - virtual ~DeleteResourceRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string resource_id_; - const std::string etag_; - - DISALLOW_COPY_AND_ASSIGN(DeleteResourceRequest); -}; - -//========================== CreateDirectoryRequest ========================== - -// This class performs the request for creating a directory. -class CreateDirectoryRequest : public GetDataRequest { - public: - // A new directory will be created under a directory specified by - // |parent_resource_id|. If this parameter is empty, a new directory will - // be created in the root directory. - // |callback| must not be null. - CreateDirectoryRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const GetDataCallback& callback, - const std::string& parent_resource_id, - const std::string& directory_title); - virtual ~CreateDirectoryRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual bool GetContentData(std::string* upload_content_type, - std::string* upload_content) OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string parent_resource_id_; - const std::string directory_title_; - - DISALLOW_COPY_AND_ASSIGN(CreateDirectoryRequest); -}; - -//=========================== RenameResourceRequest ========================== - -// This class performs the request for renaming a document/file/directory. -class RenameResourceRequest : public EntryActionRequest { - public: - // |callback| must not be null. - RenameResourceRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& resource_id, - const std::string& new_title); - virtual ~RenameResourceRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; - virtual GURL GetURL() const OVERRIDE; - virtual bool GetContentData(std::string* upload_content_type, - std::string* upload_content) OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string resource_id_; - const std::string new_title_; - - DISALLOW_COPY_AND_ASSIGN(RenameResourceRequest); -}; - -//=========================== AuthorizeAppRequest ========================== - -// This class performs the request for authorizing an application specified -// by |app_id| to access a document specified by |resource_id|. -class AuthorizeAppRequest : public GetDataRequest { - public: - // |callback| must not be null. - AuthorizeAppRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const AuthorizeAppCallback& callback, - const std::string& resource_id, - const std::string& app_id); - virtual ~AuthorizeAppRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual bool GetContentData(std::string* upload_content_type, - std::string* upload_content) OVERRIDE; - virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; - virtual GURL GetURL() const OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string resource_id_; - const std::string app_id_; - - DISALLOW_COPY_AND_ASSIGN(AuthorizeAppRequest); -}; - -//======================= AddResourceToDirectoryRequest ====================== - -// This class performs the request for adding a document/file/directory -// to a directory. -class AddResourceToDirectoryRequest : public EntryActionRequest { - public: - // |callback| must not be null. - AddResourceToDirectoryRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& parent_resource_id, - const std::string& resource_id); - virtual ~AddResourceToDirectoryRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual bool GetContentData(std::string* upload_content_type, - std::string* upload_content) OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string parent_resource_id_; - const std::string resource_id_; - - DISALLOW_COPY_AND_ASSIGN(AddResourceToDirectoryRequest); -}; - -//==================== RemoveResourceFromDirectoryRequest ==================== - -// This class performs the request for removing a document/file/directory -// from a directory. -class RemoveResourceFromDirectoryRequest : public EntryActionRequest { - public: - // |callback| must not be null. - RemoveResourceFromDirectoryRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const EntryActionCallback& callback, - const std::string& parent_resource_id, - const std::string& resource_id); - virtual ~RemoveResourceFromDirectoryRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string resource_id_; - const std::string parent_resource_id_; - - DISALLOW_COPY_AND_ASSIGN(RemoveResourceFromDirectoryRequest); -}; - -//======================= InitiateUploadNewFileRequest ======================= - -// This class performs the request for initiating the upload of a new file. -class InitiateUploadNewFileRequest : public InitiateUploadRequestBase { - public: - // |title| should be set. - // |parent_upload_url| should be the upload_url() of the parent directory. - // (resumable-create-media URL) - // See also the comments of InitiateUploadRequestBase for more details - // about the other parameters. - InitiateUploadNewFileRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const InitiateUploadCallback& callback, - const std::string& content_type, - int64 content_length, - const std::string& parent_resource_id, - const std::string& title); - virtual ~InitiateUploadNewFileRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual bool GetContentData(std::string* upload_content_type, - std::string* upload_content) OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string parent_resource_id_; - const std::string title_; - - DISALLOW_COPY_AND_ASSIGN(InitiateUploadNewFileRequest); -}; - -//==================== InitiateUploadExistingFileRequest ===================== - -// This class performs the request for initiating the upload of an existing -// file. -class InitiateUploadExistingFileRequest - : public InitiateUploadRequestBase { - public: - // |upload_url| should be the upload_url() of the file - // (resumable-create-media URL) - // |etag| should be set if it is available to detect the upload confliction. - // See also the comments of InitiateUploadRequestBase for more details - // about the other parameters. - InitiateUploadExistingFileRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const InitiateUploadCallback& callback, - const std::string& content_type, - int64 content_length, - const std::string& resource_id, - const std::string& etag); - virtual ~InitiateUploadExistingFileRequest(); - - protected: - // UrlFetchRequestBase overrides. - virtual GURL GetURL() const OVERRIDE; - virtual net::URLFetcher::RequestType GetRequestType() const OVERRIDE; - virtual std::vector<std::string> GetExtraRequestHeaders() const OVERRIDE; - virtual bool GetContentData(std::string* upload_content_type, - std::string* upload_content) OVERRIDE; - - private: - const GDataWapiUrlGenerator url_generator_; - const std::string resource_id_; - const std::string etag_; - - DISALLOW_COPY_AND_ASSIGN(InitiateUploadExistingFileRequest); -}; - -//============================ ResumeUploadRequest =========================== - -// Performs the request for resuming the upload of a file. -class ResumeUploadRequest : public ResumeUploadRequestBase { - public: - // See also ResumeUploadRequestBase's comment for parameters meaning. - // |callback| must not be null. - ResumeUploadRequest(RequestSender* sender, - const UploadRangeCallback& callback, - const ProgressCallback& progress_callback, - const GURL& upload_location, - int64 start_position, - int64 end_position, - int64 content_length, - const std::string& content_type, - const base::FilePath& local_file_path); - virtual ~ResumeUploadRequest(); - - protected: - // UploadRangeRequestBase overrides. - virtual void OnRangeRequestComplete( - const UploadRangeResponse& response, - scoped_ptr<base::Value> value) OVERRIDE; - // content::UrlFetcherDelegate overrides. - virtual void OnURLFetchUploadProgress(const net::URLFetcher* source, - int64 current, int64 total) OVERRIDE; - - private: - const UploadRangeCallback callback_; - const ProgressCallback progress_callback_; - - DISALLOW_COPY_AND_ASSIGN(ResumeUploadRequest); -}; - -//========================== GetUploadStatusRequest ========================== - -// Performs the request to request the current upload status of a file. -class GetUploadStatusRequest : public GetUploadStatusRequestBase { - public: - // See also GetUploadStatusRequestBase's comment for parameters meaning. - // |callback| must not be null. - GetUploadStatusRequest(RequestSender* sender, - const UploadRangeCallback& callback, - const GURL& upload_url, - int64 content_length); - virtual ~GetUploadStatusRequest(); - - protected: - // UploadRangeRequestBase overrides. - virtual void OnRangeRequestComplete( - const UploadRangeResponse& response, - scoped_ptr<base::Value> value) OVERRIDE; - - private: - const UploadRangeCallback callback_; - - DISALLOW_COPY_AND_ASSIGN(GetUploadStatusRequest); -}; - - -//========================== DownloadFileRequest ========================== - -// This class performs the request for downloading of a specified file. -class DownloadFileRequest : public DownloadFileRequestBase { - public: - // See also DownloadFileRequestBase's comment for parameters meaning. - DownloadFileRequest(RequestSender* sender, - const GDataWapiUrlGenerator& url_generator, - const DownloadActionCallback& download_action_callback, - const GetContentCallback& get_content_callback, - const ProgressCallback& progress_callback, - const std::string& resource_id, - const base::FilePath& output_file_path); - virtual ~DownloadFileRequest(); - - DISALLOW_COPY_AND_ASSIGN(DownloadFileRequest); -}; - } // namespace google_apis #endif // GOOGLE_APIS_DRIVE_GDATA_WAPI_REQUESTS_H_ diff --git a/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc b/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc index e302f23f1e8..2c39745184a 100644 --- a/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc +++ b/chromium/google_apis/drive/gdata_wapi_requests_unittest.cc @@ -2,19 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include <algorithm> -#include <map> - #include "base/bind.h" -#include "base/file_util.h" -#include "base/files/file_path.h" -#include "base/files/scoped_temp_dir.h" -#include "base/json/json_reader.h" -#include "base/json/json_writer.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" -#include "base/strings/string_number_conversions.h" -#include "base/strings/stringprintf.h" #include "base/values.h" #include "google_apis/drive/dummy_auth_service.h" #include "google_apis/drive/gdata_wapi_parser.h" @@ -34,14 +24,9 @@ namespace google_apis { namespace { const char kTestUserAgent[] = "test-user-agent"; -const char kTestETag[] = "test_etag"; -const char kTestDownloadPathPrefix[] = "/download/"; class GDataWapiRequestsTest : public testing::Test { public: - GDataWapiRequestsTest() { - } - virtual void SetUp() OVERRIDE { request_context_getter_ = new net::TestURLRequestContextGetter( message_loop_.message_loop_proxy()); @@ -51,35 +36,13 @@ class GDataWapiRequestsTest : public testing::Test { message_loop_.message_loop_proxy(), kTestUserAgent)); - ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); - ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady()); test_server_.RegisterRequestHandler( - base::Bind(&test_util::HandleDownloadFileRequest, - test_server_.base_url(), - base::Unretained(&http_request_))); - test_server_.RegisterRequestHandler( base::Bind(&GDataWapiRequestsTest::HandleResourceFeedRequest, base::Unretained(this))); - test_server_.RegisterRequestHandler( - base::Bind(&GDataWapiRequestsTest::HandleMetadataRequest, - base::Unretained(this))); - test_server_.RegisterRequestHandler( - base::Bind(&GDataWapiRequestsTest::HandleCreateSessionRequest, - base::Unretained(this))); - test_server_.RegisterRequestHandler( - base::Bind(&GDataWapiRequestsTest::HandleUploadRequest, - base::Unretained(this))); - test_server_.RegisterRequestHandler( - base::Bind(&GDataWapiRequestsTest::HandleDownloadRequest, - base::Unretained(this))); GURL test_base_url = test_util::GetBaseUrlForTesting(test_server_.port()); - url_generator_.reset(new GDataWapiUrlGenerator( - test_base_url, test_base_url.Resolve(kTestDownloadPathPrefix))); - - received_bytes_ = 0; - content_length_ = 0; + url_generator_.reset(new GDataWapiUrlGenerator(test_base_url)); } protected: @@ -90,16 +53,6 @@ class GDataWapiRequestsTest : public testing::Test { const GURL absolute_url = test_server_.GetURL(request.relative_url); std::string remaining_path; - if (absolute_url.path() == "/feeds/default/private/full" && - request.method == net::test_server::METHOD_POST) { - // This is a request for copying a document. - // TODO(satorux): we should generate valid JSON data for the newly - // copied document but for now, just return "file_entry.json" - scoped_ptr<net::test_server::BasicHttpResponse> result( - test_util::CreateHttpResponseFromFile( - test_util::GetTestFilePath("gdata/file_entry.json"))); - return result.PassAs<net::test_server::HttpResponse>(); - } if (!test_util::RemovePrefix(absolute_url.path(), "/feeds/default/private/full", @@ -107,236 +60,34 @@ class GDataWapiRequestsTest : public testing::Test { return scoped_ptr<net::test_server::HttpResponse>(); } - if (remaining_path.empty()) { - // Process the default feed. + // Process a feed for a single resource ID. + const std::string resource_id = net::UnescapeURLComponent( + remaining_path.substr(1), net::UnescapeRule::URL_SPECIAL_CHARS); + if (resource_id == "file:2_file_resource_id") { scoped_ptr<net::test_server::BasicHttpResponse> result( test_util::CreateHttpResponseFromFile( - test_util::GetTestFilePath("gdata/root_feed.json"))); + test_util::GetTestFilePath("gdata/file_entry.json"))); return result.PassAs<net::test_server::HttpResponse>(); - } else { - // Process a feed for a single resource ID. - const std::string resource_id = net::UnescapeURLComponent( - remaining_path.substr(1), net::UnescapeRule::URL_SPECIAL_CHARS); - if (resource_id == "file:2_file_resource_id") { - scoped_ptr<net::test_server::BasicHttpResponse> result( - test_util::CreateHttpResponseFromFile( - test_util::GetTestFilePath("gdata/file_entry.json"))); - return result.PassAs<net::test_server::HttpResponse>(); - } else if (resource_id == "folder:root/contents" && - request.method == net::test_server::METHOD_POST) { - // This is a request for creating a directory in the root directory. - // TODO(satorux): we should generate valid JSON data for the newly - // created directory but for now, just return "directory_entry.json" - scoped_ptr<net::test_server::BasicHttpResponse> result( - test_util::CreateHttpResponseFromFile( - test_util::GetTestFilePath( - "gdata/directory_entry.json"))); - return result.PassAs<net::test_server::HttpResponse>(); - } else if (resource_id == - "folder:root/contents/file:2_file_resource_id" && - request.method == net::test_server::METHOD_DELETE) { - // This is a request for deleting a file from the root directory. - // TODO(satorux): Investigate what's returned from the server, and - // copy it. For now, just return a random file, as the contents don't - // matter. + } else if (resource_id == "invalid_resource_id") { + // Check if this is an authorization request for an app. + // This emulates to return invalid formatted result from the server. + if (request.method == net::test_server::METHOD_PUT && + request.content.find("<docs:authorizedApp>") != std::string::npos) { scoped_ptr<net::test_server::BasicHttpResponse> result( test_util::CreateHttpResponseFromFile( test_util::GetTestFilePath("gdata/testfile.txt"))); return result.PassAs<net::test_server::HttpResponse>(); - } else if (resource_id == "invalid_resource_id") { - // Check if this is an authorization request for an app. - // This emulates to return invalid formatted result from the server. - if (request.method == net::test_server::METHOD_PUT && - request.content.find("<docs:authorizedApp>") != std::string::npos) { - scoped_ptr<net::test_server::BasicHttpResponse> result( - test_util::CreateHttpResponseFromFile( - test_util::GetTestFilePath("gdata/testfile.txt"))); - return result.PassAs<net::test_server::HttpResponse>(); - } - } - } - - return scoped_ptr<net::test_server::HttpResponse>(); - } - - // Handles a request for fetching a metadata feed. - scoped_ptr<net::test_server::HttpResponse> HandleMetadataRequest( - const net::test_server::HttpRequest& request) { - http_request_ = request; - - const GURL absolute_url = test_server_.GetURL(request.relative_url); - if (absolute_url.path() != "/feeds/metadata/default") - return scoped_ptr<net::test_server::HttpResponse>(); - - scoped_ptr<net::test_server::BasicHttpResponse> result( - test_util::CreateHttpResponseFromFile( - test_util::GetTestFilePath( - "gdata/account_metadata.json"))); - if (absolute_url.query().find("include-installed-apps=true") == - string::npos) { - // Exclude the list of installed apps. - scoped_ptr<base::Value> parsed_content( - base::JSONReader::Read(result->content(), base::JSON_PARSE_RFC)); - CHECK(parsed_content); - - // Remove the install apps node. - base::DictionaryValue* dictionary_value; - CHECK(parsed_content->GetAsDictionary(&dictionary_value)); - dictionary_value->Remove("entry.docs$installedApp", NULL); - - // Write back it as the content of the result. - std::string content; - base::JSONWriter::Write(parsed_content.get(), &content); - result->set_content(content); - } - - return result.PassAs<net::test_server::HttpResponse>(); - } - - // Handles a request for creating a session for uploading. - scoped_ptr<net::test_server::HttpResponse> HandleCreateSessionRequest( - const net::test_server::HttpRequest& request) { - http_request_ = request; - - const GURL absolute_url = test_server_.GetURL(request.relative_url); - if (StartsWithASCII(absolute_url.path(), - "/feeds/upload/create-session/default/private/full", - true)) { // case sensitive - // This is an initiating upload URL. - scoped_ptr<net::test_server::BasicHttpResponse> http_response( - new net::test_server::BasicHttpResponse); - - // Check an ETag. - std::map<std::string, std::string>::const_iterator found = - request.headers.find("If-Match"); - if (found != request.headers.end() && - found->second != "*" && - found->second != kTestETag) { - http_response->set_code(net::HTTP_PRECONDITION_FAILED); - return http_response.PassAs<net::test_server::HttpResponse>(); - } - - // Check if the X-Upload-Content-Length is present. If yes, store the - // length of the file. - found = request.headers.find("X-Upload-Content-Length"); - if (found == request.headers.end() || - !base::StringToInt64(found->second, &content_length_)) { - return scoped_ptr<net::test_server::HttpResponse>(); - } - received_bytes_ = 0; - - http_response->set_code(net::HTTP_OK); - GURL upload_url; - // POST is used for a new file, and PUT is used for an existing file. - if (request.method == net::test_server::METHOD_POST) { - upload_url = test_server_.GetURL("/upload_new_file"); - } else if (request.method == net::test_server::METHOD_PUT) { - upload_url = test_server_.GetURL("/upload_existing_file"); - } else { - return scoped_ptr<net::test_server::HttpResponse>(); } - http_response->AddCustomHeader("Location", upload_url.spec()); - return http_response.PassAs<net::test_server::HttpResponse>(); } return scoped_ptr<net::test_server::HttpResponse>(); } - // Handles a request for uploading content. - scoped_ptr<net::test_server::HttpResponse> HandleUploadRequest( - const net::test_server::HttpRequest& request) { - http_request_ = request; - - const GURL absolute_url = test_server_.GetURL(request.relative_url); - if (absolute_url.path() != "/upload_new_file" && - absolute_url.path() != "/upload_existing_file") { - return scoped_ptr<net::test_server::HttpResponse>(); - } - - // TODO(satorux): We should create a correct JSON data for the uploaded - // file, but for now, just return file_entry.json. - scoped_ptr<net::test_server::BasicHttpResponse> response = - test_util::CreateHttpResponseFromFile( - test_util::GetTestFilePath("gdata/file_entry.json")); - // response.code() is set to SUCCESS. Change it to CREATED if it's a new - // file. - if (absolute_url.path() == "/upload_new_file") - response->set_code(net::HTTP_CREATED); - - // Check if the Content-Range header is present. This must be present if - // the request body is not empty. - if (!request.content.empty()) { - std::map<std::string, std::string>::const_iterator iter = - request.headers.find("Content-Range"); - if (iter == request.headers.end()) - return scoped_ptr<net::test_server::HttpResponse>(); - int64 length = 0; - int64 start_position = 0; - int64 end_position = 0; - if (!test_util::ParseContentRangeHeader(iter->second, - &start_position, - &end_position, - &length)) { - return scoped_ptr<net::test_server::HttpResponse>(); - } - EXPECT_EQ(start_position, received_bytes_); - EXPECT_EQ(length, content_length_); - // end_position is inclusive, but so +1 to change the range to byte size. - received_bytes_ = end_position + 1; - } - - // Add Range header to the response, based on the values of - // Content-Range header in the request. - // The header is annotated only when at least one byte is received. - if (received_bytes_ > 0) { - response->AddCustomHeader( - "Range", - "bytes=0-" + base::Int64ToString(received_bytes_ - 1)); - } - - // Change the code to RESUME_INCOMPLETE if upload is not complete. - if (received_bytes_ < content_length_) - response->set_code(static_cast<net::HttpStatusCode>(308)); - - return response.PassAs<net::test_server::HttpResponse>(); - } - - // Handles a request for downloading a file. - scoped_ptr<net::test_server::HttpResponse> HandleDownloadRequest( - const net::test_server::HttpRequest& request) { - http_request_ = request; - - const GURL absolute_url = test_server_.GetURL(request.relative_url); - std::string id; - if (!test_util::RemovePrefix(absolute_url.path(), - kTestDownloadPathPrefix, - &id)) { - return scoped_ptr<net::test_server::HttpResponse>(); - } - - // For testing, returns a text with |id| repeated 3 times. - scoped_ptr<net::test_server::BasicHttpResponse> response( - new net::test_server::BasicHttpResponse); - response->set_code(net::HTTP_OK); - response->set_content(id + id + id); - response->set_content_type("text/plain"); - return response.PassAs<net::test_server::HttpResponse>(); - } - base::MessageLoopForIO message_loop_; // Test server needs IO thread. net::test_server::EmbeddedTestServer test_server_; scoped_ptr<RequestSender> request_sender_; scoped_ptr<GDataWapiUrlGenerator> url_generator_; scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; - base::ScopedTempDir temp_dir_; - - // These fields are used to keep the current upload state during a - // test case. These values are updated by the request from - // ResumeUploadRequest, and used to construct the response for - // both ResumeUploadRequest and GetUploadStatusRequest, to emulate - // the WAPI server. - int64 received_bytes_; - int64 content_length_; // The incoming HTTP request is saved so tests can verify the request // parameters like HTTP method (ex. some requests should use DELETE @@ -346,130 +97,6 @@ class GDataWapiRequestsTest : public testing::Test { } // namespace -TEST_F(GDataWapiRequestsTest, GetResourceListRequest_DefaultFeed) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - scoped_ptr<ResourceList> result_data; - - { - base::RunLoop run_loop; - GetResourceListRequest* request = new GetResourceListRequest( - request_sender_.get(), - *url_generator_, - GURL(), // Pass an empty URL to use the default feed - 0, // start changestamp - std::string(), // search string - std::string(), // directory resource ID - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data))); - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); - EXPECT_EQ("/feeds/default/private/full?v=3&alt=json&showroot=true&" - "showfolders=true&include-shared=true&max-results=500", - http_request_.relative_url); - - // Sanity check of the result. - scoped_ptr<ResourceList> expected( - ResourceList::ExtractAndParse( - *test_util::LoadJSONFile("gdata/root_feed.json"))); - ASSERT_TRUE(result_data); - EXPECT_EQ(expected->title(), result_data->title()); -} - -TEST_F(GDataWapiRequestsTest, GetResourceListRequest_ValidFeed) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - scoped_ptr<ResourceList> result_data; - - { - base::RunLoop run_loop; - GetResourceListRequest* request = new GetResourceListRequest( - request_sender_.get(), - *url_generator_, - test_server_.GetURL("/files/gdata/root_feed.json"), - 0, // start changestamp - std::string(), // search string - std::string(), // directory resource ID - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data))); - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); - EXPECT_EQ("/files/gdata/root_feed.json?v=3&alt=json&showroot=true&" - "showfolders=true&include-shared=true&max-results=500", - http_request_.relative_url); - - scoped_ptr<ResourceList> expected( - ResourceList::ExtractAndParse( - *test_util::LoadJSONFile("gdata/root_feed.json"))); - ASSERT_TRUE(result_data); - EXPECT_EQ(expected->title(), result_data->title()); -} - -TEST_F(GDataWapiRequestsTest, GetResourceListRequest_InvalidFeed) { - // testfile.txt exists but the response is not JSON, so it should - // emit a parse error instead. - GDataErrorCode result_code = GDATA_OTHER_ERROR; - scoped_ptr<ResourceList> result_data; - - { - base::RunLoop run_loop; - GetResourceListRequest* request = new GetResourceListRequest( - request_sender_.get(), - *url_generator_, - test_server_.GetURL("/files/gdata/testfile.txt"), - 0, // start changestamp - std::string(), // search string - std::string(), // directory resource ID - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data))); - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(GDATA_PARSE_ERROR, result_code); - EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); - EXPECT_EQ("/files/gdata/testfile.txt?v=3&alt=json&showroot=true&" - "showfolders=true&include-shared=true&max-results=500", - http_request_.relative_url); - EXPECT_FALSE(result_data); -} - -TEST_F(GDataWapiRequestsTest, SearchByTitleRequest) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - scoped_ptr<ResourceList> result_data; - - { - base::RunLoop run_loop; - SearchByTitleRequest* request = new SearchByTitleRequest( - request_sender_.get(), - *url_generator_, - "search-title", - std::string(), // directory resource id - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data))); - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); - EXPECT_EQ("/feeds/default/private/full?v=3&alt=json&showroot=true&" - "showfolders=true&include-shared=true&max-results=500" - "&title=search-title&title-exact=true", - http_request_.relative_url); - EXPECT_TRUE(result_data); -} - TEST_F(GDataWapiRequestsTest, GetResourceEntryRequest_ValidResourceId) { GDataErrorCode result_code = GDATA_OTHER_ERROR; scoped_ptr<base::Value> result_data; @@ -526,1036 +153,4 @@ TEST_F(GDataWapiRequestsTest, GetResourceEntryRequest_InvalidResourceId) { ASSERT_FALSE(result_data); } -TEST_F(GDataWapiRequestsTest, GetAccountMetadataRequest) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - scoped_ptr<AccountMetadata> result_data; - - { - base::RunLoop run_loop; - GetAccountMetadataRequest* request = new GetAccountMetadataRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data)), - true); // Include installed apps. - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); - EXPECT_EQ("/feeds/metadata/default?v=3&alt=json&showroot=true" - "&include-installed-apps=true", - http_request_.relative_url); - - scoped_ptr<AccountMetadata> expected( - AccountMetadata::CreateFrom( - *test_util::LoadJSONFile("gdata/account_metadata.json"))); - - ASSERT_TRUE(result_data.get()); - EXPECT_EQ(expected->largest_changestamp(), - result_data->largest_changestamp()); - EXPECT_EQ(expected->quota_bytes_total(), - result_data->quota_bytes_total()); - EXPECT_EQ(expected->quota_bytes_used(), - result_data->quota_bytes_used()); - - // Sanity check for installed apps. - EXPECT_EQ(expected->installed_apps().size(), - result_data->installed_apps().size()); -} - -TEST_F(GDataWapiRequestsTest, - GetAccountMetadataRequestWithoutInstalledApps) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - scoped_ptr<AccountMetadata> result_data; - - { - base::RunLoop run_loop; - GetAccountMetadataRequest* request = new GetAccountMetadataRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data)), - false); // Exclude installed apps. - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); - EXPECT_EQ("/feeds/metadata/default?v=3&alt=json&showroot=true", - http_request_.relative_url); - - scoped_ptr<AccountMetadata> expected( - AccountMetadata::CreateFrom( - *test_util::LoadJSONFile("gdata/account_metadata.json"))); - - ASSERT_TRUE(result_data.get()); - EXPECT_EQ(expected->largest_changestamp(), - result_data->largest_changestamp()); - EXPECT_EQ(expected->quota_bytes_total(), - result_data->quota_bytes_total()); - EXPECT_EQ(expected->quota_bytes_used(), - result_data->quota_bytes_used()); - - // Installed apps shouldn't be included. - EXPECT_EQ(0U, result_data->installed_apps().size()); -} - -TEST_F(GDataWapiRequestsTest, DeleteResourceRequest) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - - { - base::RunLoop run_loop; - DeleteResourceRequest* request = new DeleteResourceRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code)), - "file:2_file_resource_id", - std::string()); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); - EXPECT_EQ( - "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json" - "&showroot=true", - http_request_.relative_url); - EXPECT_EQ("*", http_request_.headers["If-Match"]); -} - -TEST_F(GDataWapiRequestsTest, DeleteResourceRequestWithETag) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - - { - base::RunLoop run_loop; - DeleteResourceRequest* request = new DeleteResourceRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code)), - "file:2_file_resource_id", - "etag"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); - EXPECT_EQ( - "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json" - "&showroot=true", - http_request_.relative_url); - EXPECT_EQ("etag", http_request_.headers["If-Match"]); -} - -TEST_F(GDataWapiRequestsTest, CreateDirectoryRequest) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - scoped_ptr<base::Value> result_data; - - // Create "new directory" in the root directory. - { - base::RunLoop run_loop; - CreateDirectoryRequest* request = new CreateDirectoryRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data)), - "folder:root", - "new directory"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); - EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents?v=3&alt=json" - "&showroot=true", - http_request_.relative_url); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n" - " <category scheme=\"http://schemas.google.com/g/2005#kind\" " - "term=\"http://schemas.google.com/docs/2007#folder\"/>\n" - " <title>new directory</title>\n" - "</entry>\n", - http_request_.content); -} - -TEST_F(GDataWapiRequestsTest, RenameResourceRequest) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - - // Rename a file with a new name "New File". - { - base::RunLoop run_loop; - RenameResourceRequest* request = new RenameResourceRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code)), - "file:2_file_resource_id", - "New File"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - EXPECT_EQ( - "/feeds/default/private/full/file%3A2_file_resource_id?v=3&alt=json" - "&showroot=true", - http_request_.relative_url); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - EXPECT_EQ("*", http_request_.headers["If-Match"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n" - " <title>New File</title>\n" - "</entry>\n", - http_request_.content); -} - -TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_ValidFeed) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL result_data; - - // Authorize an app with APP_ID to access to a document. - { - base::RunLoop run_loop; - AuthorizeAppRequest* request = new AuthorizeAppRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data)), - "file:2_file_resource_id", - "the_app_id"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(GURL("https://entry1_open_with_link/"), result_data); - - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id" - "?v=3&alt=json&showroot=true", - http_request_.relative_url); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - EXPECT_EQ("*", http_request_.headers["If-Match"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\" " - "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" - " <docs:authorizedApp>the_app_id</docs:authorizedApp>\n" - "</entry>\n", - http_request_.content); -} - -TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_NotFound) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL result_data; - - // Authorize an app with APP_ID to access to a document. - { - base::RunLoop run_loop; - AuthorizeAppRequest* request = new AuthorizeAppRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data)), - "file:2_file_resource_id", - "unauthorized_app_id"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(GDATA_OTHER_ERROR, result_code); - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - EXPECT_EQ("/feeds/default/private/full/file%3A2_file_resource_id" - "?v=3&alt=json&showroot=true", - http_request_.relative_url); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - EXPECT_EQ("*", http_request_.headers["If-Match"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\" " - "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" - " <docs:authorizedApp>unauthorized_app_id</docs:authorizedApp>\n" - "</entry>\n", - http_request_.content); -} - -TEST_F(GDataWapiRequestsTest, AuthorizeAppRequest_InvalidFeed) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL result_data; - - // Authorize an app with APP_ID to access to a document but an invalid feed. - { - base::RunLoop run_loop; - AuthorizeAppRequest* request = new AuthorizeAppRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &result_data)), - "invalid_resource_id", - "APP_ID"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(GDATA_PARSE_ERROR, result_code); - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - EXPECT_EQ("/feeds/default/private/full/invalid_resource_id" - "?v=3&alt=json&showroot=true", - http_request_.relative_url); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - EXPECT_EQ("*", http_request_.headers["If-Match"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\" " - "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" - " <docs:authorizedApp>APP_ID</docs:authorizedApp>\n" - "</entry>\n", - http_request_.content); -} - -TEST_F(GDataWapiRequestsTest, AddResourceToDirectoryRequest) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - - // Add a file to the root directory. - { - base::RunLoop run_loop; - AddResourceToDirectoryRequest* request = - new AddResourceToDirectoryRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code)), - "folder:root", - "file:2_file_resource_id"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); - EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents?v=3&alt=json" - "&showroot=true", - http_request_.relative_url); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ(base::StringPrintf("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n" - " <id>%sfeeds/default/private/full/" - "file%%3A2_file_resource_id</id>\n" - "</entry>\n", - test_server_.base_url().spec().c_str()), - http_request_.content); -} - -TEST_F(GDataWapiRequestsTest, RemoveResourceFromDirectoryRequest) { - GDataErrorCode result_code = GDATA_OTHER_ERROR; - - // Remove a file from the root directory. - { - base::RunLoop run_loop; - RemoveResourceFromDirectoryRequest* request = - new RemoveResourceFromDirectoryRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code)), - "folder:root", - "file:2_file_resource_id"); - - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - // DELETE method should be used, without the body content. - EXPECT_EQ(net::test_server::METHOD_DELETE, http_request_.method); - EXPECT_EQ("/feeds/default/private/full/folder%3Aroot/contents/" - "file%3A2_file_resource_id?v=3&alt=json&showroot=true", - http_request_.relative_url); - EXPECT_EQ("*", http_request_.headers["If-Match"]); - EXPECT_FALSE(http_request_.has_content); -} - -// This test exercises InitiateUploadNewFileRequest and -// ResumeUploadRequest for a scenario of uploading a new file. -TEST_F(GDataWapiRequestsTest, UploadNewFile) { - const std::string kUploadContent = "hello"; - const base::FilePath kTestFilePath = - temp_dir_.path().AppendASCII("upload_file.txt"); - ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); - - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL upload_url; - - // 1) Get the upload URL for uploading a new file. - { - base::RunLoop run_loop; - InitiateUploadNewFileRequest* initiate_request = - new InitiateUploadNewFileRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &upload_url)), - "text/plain", - kUploadContent.size(), - "folder:id", - "New file"); - request_sender_->StartRequestWithRetry(initiate_request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url); - EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); - // convert=false should be passed as files should be uploaded as-is. - EXPECT_EQ( - "/feeds/upload/create-session/default/private/full/folder%3Aid/contents" - "?convert=false&v=3&alt=json&showroot=true", - http_request_.relative_url); - EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - EXPECT_EQ(base::Int64ToString(kUploadContent.size()), - http_request_.headers["X-Upload-Content-Length"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\" " - "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" - " <title>New file</title>\n" - "</entry>\n", - http_request_.content); - - // 2) Upload the content to the upload URL. - UploadRangeResponse response; - scoped_ptr<ResourceEntry> new_entry; - - { - base::RunLoop run_loop; - ResumeUploadRequest* resume_request = new ResumeUploadRequest( - request_sender_.get(), - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&response, &new_entry)), - ProgressCallback(), - upload_url, - 0, // start_position - kUploadContent.size(), // end_position (exclusive) - kUploadContent.size(), // content_length, - "text/plain", // content_type - kTestFilePath); - - request_sender_->StartRequestWithRetry(resume_request); - run_loop.Run(); - } - - // METHOD_PUT should be used to upload data. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // Request should go to the upload URL. - EXPECT_EQ(upload_url.path(), http_request_.relative_url); - // Content-Range header should be added. - EXPECT_EQ("bytes 0-" + - base::Int64ToString(kUploadContent.size() -1) + "/" + - base::Int64ToString(kUploadContent.size()), - http_request_.headers["Content-Range"]); - // The upload content should be set in the HTTP request. - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ(kUploadContent, http_request_.content); - - // Check the response. - EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file - // The start and end positions should be set to -1, if an upload is complete. - EXPECT_EQ(-1, response.start_position_received); - EXPECT_EQ(-1, response.end_position_received); -} - -// This test exercises InitiateUploadNewFileRequest and ResumeUploadRequest -// for a scenario of uploading a new *large* file, which requires multiple -// requests of ResumeUploadRequest. GetUploadRequest is also tested in this -// test case. -TEST_F(GDataWapiRequestsTest, UploadNewLargeFile) { - const size_t kMaxNumBytes = 10; - // This is big enough to cause multiple requests of ResumeUploadRequest - // as we are going to send at most kMaxNumBytes at a time. - // So, sending "kMaxNumBytes * 2 + 1" bytes ensures three - // ResumeUploadRequests, which are start, middle and last requests. - const std::string kUploadContent(kMaxNumBytes * 2 + 1, 'a'); - const base::FilePath kTestFilePath = - temp_dir_.path().AppendASCII("upload_file.txt"); - ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); - - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL upload_url; - - // 1) Get the upload URL for uploading a new file. - { - base::RunLoop run_loop; - InitiateUploadNewFileRequest* initiate_request = - new InitiateUploadNewFileRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &upload_url)), - "text/plain", - kUploadContent.size(), - "folder:id", - "New file"); - request_sender_->StartRequestWithRetry(initiate_request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url); - EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); - // convert=false should be passed as files should be uploaded as-is. - EXPECT_EQ( - "/feeds/upload/create-session/default/private/full/folder%3Aid/contents" - "?convert=false&v=3&alt=json&showroot=true", - http_request_.relative_url); - EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - EXPECT_EQ(base::Int64ToString(kUploadContent.size()), - http_request_.headers["X-Upload-Content-Length"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\" " - "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" - " <title>New file</title>\n" - "</entry>\n", - http_request_.content); - - // 2) Before sending any data, check the current status. - // This is an edge case test for GetUploadStatusRequest - // (UploadRangeRequestBase). - { - UploadRangeResponse response; - scoped_ptr<ResourceEntry> new_entry; - - // Check the response by GetUploadStatusRequest. - { - base::RunLoop run_loop; - GetUploadStatusRequest* get_upload_status_request = - new GetUploadStatusRequest( - request_sender_.get(), - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&response, &new_entry)), - upload_url, - kUploadContent.size()); - request_sender_->StartRequestWithRetry(get_upload_status_request); - run_loop.Run(); - } - - // METHOD_PUT should be used to upload data. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // Request should go to the upload URL. - EXPECT_EQ(upload_url.path(), http_request_.relative_url); - // Content-Range header should be added. - EXPECT_EQ("bytes */" + base::Int64ToString(kUploadContent.size()), - http_request_.headers["Content-Range"]); - EXPECT_TRUE(http_request_.has_content); - EXPECT_TRUE(http_request_.content.empty()); - - // Check the response. - EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); - EXPECT_EQ(0, response.start_position_received); - EXPECT_EQ(0, response.end_position_received); - } - - // 3) Upload the content to the upload URL with multiple requests. - size_t num_bytes_consumed = 0; - for (size_t start_position = 0; start_position < kUploadContent.size(); - start_position += kMaxNumBytes) { - SCOPED_TRACE(testing::Message("start_position: ") << start_position); - - // The payload is at most kMaxNumBytes. - const size_t remaining_size = kUploadContent.size() - start_position; - const std::string payload = kUploadContent.substr( - start_position, std::min(kMaxNumBytes, remaining_size)); - num_bytes_consumed += payload.size(); - // The end position is exclusive. - const size_t end_position = start_position + payload.size(); - - UploadRangeResponse response; - scoped_ptr<ResourceEntry> new_entry; - - { - base::RunLoop run_loop; - ResumeUploadRequest* resume_request = new ResumeUploadRequest( - request_sender_.get(), - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&response, &new_entry)), - ProgressCallback(), - upload_url, - start_position, - end_position, - kUploadContent.size(), // content_length, - "text/plain", // content_type - kTestFilePath); - request_sender_->StartRequestWithRetry(resume_request); - run_loop.Run(); - } - - // METHOD_PUT should be used to upload data. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // Request should go to the upload URL. - EXPECT_EQ(upload_url.path(), http_request_.relative_url); - // Content-Range header should be added. - EXPECT_EQ("bytes " + - base::Int64ToString(start_position) + "-" + - base::Int64ToString(end_position - 1) + "/" + - base::Int64ToString(kUploadContent.size()), - http_request_.headers["Content-Range"]); - // The upload content should be set in the HTTP request. - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ(payload, http_request_.content); - - // Check the response. - if (payload.size() == remaining_size) { - EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file. - // The start and end positions should be set to -1, if an upload is - // complete. - EXPECT_EQ(-1, response.start_position_received); - EXPECT_EQ(-1, response.end_position_received); - // The upload process is completed, so exit from the loop. - break; - } - - EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); - EXPECT_EQ(0, response.start_position_received); - EXPECT_EQ(static_cast<int64>(end_position), - response.end_position_received); - - // Check the response by GetUploadStatusRequest. - { - base::RunLoop run_loop; - GetUploadStatusRequest* get_upload_status_request = - new GetUploadStatusRequest( - request_sender_.get(), - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&response, &new_entry)), - upload_url, - kUploadContent.size()); - request_sender_->StartRequestWithRetry(get_upload_status_request); - run_loop.Run(); - } - - // METHOD_PUT should be used to upload data. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // Request should go to the upload URL. - EXPECT_EQ(upload_url.path(), http_request_.relative_url); - // Content-Range header should be added. - EXPECT_EQ("bytes */" + base::Int64ToString(kUploadContent.size()), - http_request_.headers["Content-Range"]); - EXPECT_TRUE(http_request_.has_content); - EXPECT_TRUE(http_request_.content.empty()); - - // Check the response. - EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); - EXPECT_EQ(0, response.start_position_received); - EXPECT_EQ(static_cast<int64>(end_position), - response.end_position_received); - } - - EXPECT_EQ(kUploadContent.size(), num_bytes_consumed); -} - -// This test exercises InitiateUploadNewFileRequest and ResumeUploadRequest -// for a scenario of uploading a new *empty* file. -// -// The test is almost identical to UploadNewFile. The only difference is the -// expectation for the Content-Range header. -TEST_F(GDataWapiRequestsTest, UploadNewEmptyFile) { - const std::string kUploadContent; - const base::FilePath kTestFilePath = - temp_dir_.path().AppendASCII("empty_file.txt"); - ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); - - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL upload_url; - - // 1) Get the upload URL for uploading a new file. - { - base::RunLoop run_loop; - InitiateUploadNewFileRequest* initiate_request = - new InitiateUploadNewFileRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &upload_url)), - "text/plain", - kUploadContent.size(), - "folder:id", - "New file"); - request_sender_->StartRequestWithRetry(initiate_request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(test_server_.GetURL("/upload_new_file"), upload_url); - EXPECT_EQ(net::test_server::METHOD_POST, http_request_.method); - // convert=false should be passed as files should be uploaded as-is. - EXPECT_EQ( - "/feeds/upload/create-session/default/private/full/folder%3Aid/contents" - "?convert=false&v=3&alt=json&showroot=true", - http_request_.relative_url); - EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ("application/atom+xml", http_request_.headers["Content-Type"]); - EXPECT_EQ(base::Int64ToString(kUploadContent.size()), - http_request_.headers["X-Upload-Content-Length"]); - - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("<?xml version=\"1.0\"?>\n" - "<entry xmlns=\"http://www.w3.org/2005/Atom\" " - "xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" - " <title>New file</title>\n" - "</entry>\n", - http_request_.content); - - // 2) Upload the content to the upload URL. - UploadRangeResponse response; - scoped_ptr<ResourceEntry> new_entry; - - { - base::RunLoop run_loop; - ResumeUploadRequest* resume_request = new ResumeUploadRequest( - request_sender_.get(), - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&response, &new_entry)), - ProgressCallback(), - upload_url, - 0, // start_position - kUploadContent.size(), // end_position (exclusive) - kUploadContent.size(), // content_length, - "text/plain", // content_type - kTestFilePath); - request_sender_->StartRequestWithRetry(resume_request); - run_loop.Run(); - } - - // METHOD_PUT should be used to upload data. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // Request should go to the upload URL. - EXPECT_EQ(upload_url.path(), http_request_.relative_url); - // Content-Range header should not exit if the content is empty. - // We should not generate the header with an invalid value "bytes 0--1/0". - EXPECT_EQ(0U, http_request_.headers.count("Content-Range")); - // The upload content should be set in the HTTP request. - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ(kUploadContent, http_request_.content); - - // Check the response. - EXPECT_EQ(HTTP_CREATED, response.code); // Because it's a new file. - // The start and end positions should be set to -1, if an upload is complete. - EXPECT_EQ(-1, response.start_position_received); - EXPECT_EQ(-1, response.end_position_received); -} - -// This test exercises InitiateUploadExistingFileRequest and -// ResumeUploadRequest for a scenario of updating an existing file. -TEST_F(GDataWapiRequestsTest, UploadExistingFile) { - const std::string kUploadContent = "hello"; - const base::FilePath kTestFilePath = - temp_dir_.path().AppendASCII("upload_file.txt"); - ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); - - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL upload_url; - - // 1) Get the upload URL for uploading an existing file. - { - base::RunLoop run_loop; - InitiateUploadExistingFileRequest* initiate_request = - new InitiateUploadExistingFileRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &upload_url)), - "text/plain", - kUploadContent.size(), - "file:foo", - std::string() /* etag */); - request_sender_->StartRequestWithRetry(initiate_request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(test_server_.GetURL("/upload_existing_file"), upload_url); - // For updating an existing file, METHOD_PUT should be used. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // convert=false should be passed as files should be uploaded as-is. - EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo" - "?convert=false&v=3&alt=json&showroot=true", - http_request_.relative_url); - // Even though the body is empty, the content type should be set to - // "text/plain". - EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]); - EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ(base::Int64ToString(kUploadContent.size()), - http_request_.headers["X-Upload-Content-Length"]); - // For updating an existing file, an empty body should be attached (PUT - // requires a body) - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("", http_request_.content); - EXPECT_EQ("*", http_request_.headers["If-Match"]); - - // 2) Upload the content to the upload URL. - UploadRangeResponse response; - scoped_ptr<ResourceEntry> new_entry; - - { - base::RunLoop run_loop; - ResumeUploadRequest* resume_request = new ResumeUploadRequest( - request_sender_.get(), - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&response, &new_entry)), - ProgressCallback(), - upload_url, - 0, // start_position - kUploadContent.size(), // end_position (exclusive) - kUploadContent.size(), // content_length, - "text/plain", // content_type - kTestFilePath); - - request_sender_->StartRequestWithRetry(resume_request); - run_loop.Run(); - } - - // METHOD_PUT should be used to upload data. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // Request should go to the upload URL. - EXPECT_EQ(upload_url.path(), http_request_.relative_url); - // Content-Range header should be added. - EXPECT_EQ("bytes 0-" + - base::Int64ToString(kUploadContent.size() -1) + "/" + - base::Int64ToString(kUploadContent.size()), - http_request_.headers["Content-Range"]); - // The upload content should be set in the HTTP request. - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ(kUploadContent, http_request_.content); - - // Check the response. - EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file. - // The start and end positions should be set to -1, if an upload is complete. - EXPECT_EQ(-1, response.start_position_received); - EXPECT_EQ(-1, response.end_position_received); -} - -// This test exercises InitiateUploadExistingFileRequest and -// ResumeUploadRequest for a scenario of updating an existing file. -TEST_F(GDataWapiRequestsTest, UploadExistingFileWithETag) { - const std::string kUploadContent = "hello"; - const base::FilePath kTestFilePath = - temp_dir_.path().AppendASCII("upload_file.txt"); - ASSERT_TRUE(test_util::WriteStringToFile(kTestFilePath, kUploadContent)); - - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL upload_url; - - // 1) Get the upload URL for uploading an existing file. - { - base::RunLoop run_loop; - InitiateUploadExistingFileRequest* initiate_request = - new InitiateUploadExistingFileRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &upload_url)), - "text/plain", - kUploadContent.size(), - "file:foo", - kTestETag); - request_sender_->StartRequestWithRetry(initiate_request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(test_server_.GetURL("/upload_existing_file"), upload_url); - // For updating an existing file, METHOD_PUT should be used. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // convert=false should be passed as files should be uploaded as-is. - EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo" - "?convert=false&v=3&alt=json&showroot=true", - http_request_.relative_url); - // Even though the body is empty, the content type should be set to - // "text/plain". - EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]); - EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ(base::Int64ToString(kUploadContent.size()), - http_request_.headers["X-Upload-Content-Length"]); - // For updating an existing file, an empty body should be attached (PUT - // requires a body) - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("", http_request_.content); - EXPECT_EQ(kTestETag, http_request_.headers["If-Match"]); - - // 2) Upload the content to the upload URL. - UploadRangeResponse response; - scoped_ptr<ResourceEntry> new_entry; - - { - base::RunLoop run_loop; - ResumeUploadRequest* resume_request = new ResumeUploadRequest( - request_sender_.get(), - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&response, &new_entry)), - ProgressCallback(), - upload_url, - 0, // start_position - kUploadContent.size(), // end_position (exclusive) - kUploadContent.size(), // content_length, - "text/plain", // content_type - kTestFilePath); - request_sender_->StartRequestWithRetry(resume_request); - run_loop.Run(); - } - - // METHOD_PUT should be used to upload data. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // Request should go to the upload URL. - EXPECT_EQ(upload_url.path(), http_request_.relative_url); - // Content-Range header should be added. - EXPECT_EQ("bytes 0-" + - base::Int64ToString(kUploadContent.size() -1) + "/" + - base::Int64ToString(kUploadContent.size()), - http_request_.headers["Content-Range"]); - // The upload content should be set in the HTTP request. - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ(kUploadContent, http_request_.content); - - // Check the response. - EXPECT_EQ(HTTP_SUCCESS, response.code); // Because it's an existing file. - // The start and end positions should be set to -1, if an upload is complete. - EXPECT_EQ(-1, response.start_position_received); - EXPECT_EQ(-1, response.end_position_received); -} - -// This test exercises InitiateUploadExistingFileRequest for a scenario of -// confliction on updating an existing file. -TEST_F(GDataWapiRequestsTest, UploadExistingFileWithETagConflict) { - const std::string kUploadContent = "hello"; - const std::string kWrongETag = "wrong_etag"; - GDataErrorCode result_code = GDATA_OTHER_ERROR; - GURL upload_url; - - { - base::RunLoop run_loop; - InitiateUploadExistingFileRequest* initiate_request = - new InitiateUploadExistingFileRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &upload_url)), - "text/plain", - kUploadContent.size(), - "file:foo", - kWrongETag); - request_sender_->StartRequestWithRetry(initiate_request); - run_loop.Run(); - } - - EXPECT_EQ(HTTP_PRECONDITION, result_code); - // For updating an existing file, METHOD_PUT should be used. - EXPECT_EQ(net::test_server::METHOD_PUT, http_request_.method); - // convert=false should be passed as files should be uploaded as-is. - EXPECT_EQ("/feeds/upload/create-session/default/private/full/file%3Afoo" - "?convert=false&v=3&alt=json&showroot=true", - http_request_.relative_url); - // Even though the body is empty, the content type should be set to - // "text/plain". - EXPECT_EQ("text/plain", http_request_.headers["Content-Type"]); - EXPECT_EQ("text/plain", http_request_.headers["X-Upload-Content-Type"]); - EXPECT_EQ(base::Int64ToString(kUploadContent.size()), - http_request_.headers["X-Upload-Content-Length"]); - // For updating an existing file, an empty body should be attached (PUT - // requires a body) - EXPECT_TRUE(http_request_.has_content); - EXPECT_EQ("", http_request_.content); - EXPECT_EQ(kWrongETag, http_request_.headers["If-Match"]); -} - -TEST_F(GDataWapiRequestsTest, DownloadFileRequest) { - const base::FilePath kDownloadedFilePath = - temp_dir_.path().AppendASCII("cache_file"); - const std::string kTestIdWithTypeLabel("file:dummyId"); - const std::string kTestId("dummyId"); - - GDataErrorCode result_code = GDATA_OTHER_ERROR; - base::FilePath temp_file; - { - base::RunLoop run_loop; - DownloadFileRequest* request = new DownloadFileRequest( - request_sender_.get(), - *url_generator_, - test_util::CreateQuitCallback( - &run_loop, - test_util::CreateCopyResultCallback(&result_code, &temp_file)), - GetContentCallback(), - ProgressCallback(), - kTestIdWithTypeLabel, - kDownloadedFilePath); - request_sender_->StartRequestWithRetry(request); - run_loop.Run(); - } - - std::string contents; - base::ReadFileToString(temp_file, &contents); - base::DeleteFile(temp_file, false); - - EXPECT_EQ(HTTP_SUCCESS, result_code); - EXPECT_EQ(net::test_server::METHOD_GET, http_request_.method); - EXPECT_EQ(kTestDownloadPathPrefix + kTestId, http_request_.relative_url); - EXPECT_EQ(kDownloadedFilePath, temp_file); - - const std::string expected_contents = kTestId + kTestId + kTestId; - EXPECT_EQ(expected_contents, contents); -} - } // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator.cc b/chromium/google_apis/drive/gdata_wapi_url_generator.cc index c1263b71f34..ffdb0f3d943 100644 --- a/chromium/google_apis/drive/gdata_wapi_url_generator.cc +++ b/chromium/google_apis/drive/gdata_wapi_url_generator.cc @@ -14,52 +14,15 @@ namespace google_apis { namespace { -// Content URL for modification or resource list retrieval in a particular -// directory specified by "%s" which will be replaced with its resource id. -const char kContentURLFormat[] = "/feeds/default/private/full/%s/contents"; - -// Content URL for removing a resource specified by the latter "%s" from the -// directory specified by the former "%s". -const char kResourceURLForRemovalFormat[] = - "/feeds/default/private/full/%s/contents/%s"; - // URL requesting single resource entry whose resource id is followed by this // prefix. const char kGetEditURLPrefix[] = "/feeds/default/private/full/"; -// Root resource list url. -const char kResourceListRootURL[] = "/feeds/default/private/full"; - -// Metadata feed with things like user quota. -const char kAccountMetadataURL[] = "/feeds/metadata/default"; - -// URL to upload a new file under a particular directory specified by "%s". -const char kInitiateUploadNewFileURLFormat[] = - "/feeds/upload/create-session/default/private/full/%s/contents"; - -// URL to upload a file content to overwrite a file whose resource id is -// followed by this prefix. -const char kInitiateUploadExistingFileURLPrefix[] = - "/feeds/upload/create-session/default/private/full/"; - -// Maximum number of resource entries to include in a feed. -// Be careful not to use something too small because it might overload the -// server. Be careful not to use something too large because it makes the -// "fetched N items" UI less responsive. -const int kMaxDocumentsPerFeed = 500; -const int kMaxDocumentsPerSearchFeed = 50; - -// URL requesting documents list of changes to documents collections. -const char kGetChangesListURL[] = "/feeds/default/private/changes"; - } // namespace const char GDataWapiUrlGenerator::kBaseUrlForProduction[] = "https://docs.google.com/"; -const char GDataWapiUrlGenerator::kBaseDownloadUrlForProduction[] = - "https://www.googledrive.com/host/"; - // static GURL GDataWapiUrlGenerator::AddStandardUrlParams(const GURL& url) { GURL result = net::AppendOrReplaceQueryParameter(url, "v", "3"); @@ -68,90 +31,13 @@ GURL GDataWapiUrlGenerator::AddStandardUrlParams(const GURL& url) { return result; } -// static -GURL GDataWapiUrlGenerator::AddInitiateUploadUrlParams(const GURL& url) { - GURL result = net::AppendOrReplaceQueryParameter(url, "convert", "false"); - return AddStandardUrlParams(result); -} - -// static -GURL GDataWapiUrlGenerator::AddFeedUrlParams( - const GURL& url, - int num_items_to_fetch) { - GURL result = AddStandardUrlParams(url); - result = net::AppendOrReplaceQueryParameter(result, "showfolders", "true"); - result = net::AppendOrReplaceQueryParameter(result, "include-shared", "true"); - result = net::AppendOrReplaceQueryParameter( - result, "max-results", base::IntToString(num_items_to_fetch)); - return result; -} - -GDataWapiUrlGenerator::GDataWapiUrlGenerator(const GURL& base_url, - const GURL& base_download_url) - : base_url_(base_url), - base_download_url_(base_download_url) { +GDataWapiUrlGenerator::GDataWapiUrlGenerator(const GURL& base_url) + : base_url_(base_url) { } GDataWapiUrlGenerator::~GDataWapiUrlGenerator() { } -GURL GDataWapiUrlGenerator::GenerateResourceListUrl( - const GURL& override_url, - int64 start_changestamp, - const std::string& search_string, - const std::string& directory_resource_id) const { - DCHECK_LE(0, start_changestamp); - - int max_docs = search_string.empty() ? kMaxDocumentsPerFeed : - kMaxDocumentsPerSearchFeed; - GURL url; - if (!override_url.is_empty()) { - // |override_url| specifies the URL of the continuation feed when the feed - // is broken up to multiple chunks. In this case we must not add the - // |start_changestamp| that provides the original start point. - start_changestamp = 0; - url = override_url; - } else if (start_changestamp > 0) { - // The start changestamp shouldn't be used for a search. - DCHECK(search_string.empty()); - url = base_url_.Resolve(kGetChangesListURL); - } else if (!directory_resource_id.empty()) { - url = base_url_.Resolve( - base::StringPrintf(kContentURLFormat, - net::EscapePath( - directory_resource_id).c_str())); - } else { - url = base_url_.Resolve(kResourceListRootURL); - } - - url = AddFeedUrlParams(url, max_docs); - - if (start_changestamp) { - url = net::AppendOrReplaceQueryParameter( - url, "start-index", base::Int64ToString(start_changestamp)); - } - if (!search_string.empty()) { - url = net::AppendOrReplaceQueryParameter(url, "q", search_string); - } - - return url; -} - -GURL GDataWapiUrlGenerator::GenerateSearchByTitleUrl( - const std::string& title, - const std::string& directory_resource_id) const { - DCHECK(!title.empty()); - - GURL url = directory_resource_id.empty() ? - base_url_.Resolve(kResourceListRootURL) : - base_url_.Resolve(base::StringPrintf( - kContentURLFormat, net::EscapePath(directory_resource_id).c_str())); - url = AddFeedUrlParams(url, kMaxDocumentsPerFeed); - url = net::AppendOrReplaceQueryParameter(url, "title", title); - url = net::AppendOrReplaceQueryParameter(url, "title-exact", "true"); - return url; -} - GURL GDataWapiUrlGenerator::GenerateEditUrl( const std::string& resource_id) const { return AddStandardUrlParams(GenerateEditUrlWithoutParams(resource_id)); @@ -180,71 +66,4 @@ GURL GDataWapiUrlGenerator::GenerateEditUrlWithEmbedOrigin( return url; } -GURL GDataWapiUrlGenerator::GenerateContentUrl( - const std::string& resource_id) const { - if (resource_id.empty()) { - // |resource_id| must not be empty. Return an empty GURL as an error. - return GURL(); - } - - GURL result = base_url_.Resolve( - base::StringPrintf(kContentURLFormat, - net::EscapePath(resource_id).c_str())); - return AddStandardUrlParams(result); -} - -GURL GDataWapiUrlGenerator::GenerateResourceUrlForRemoval( - const std::string& parent_resource_id, - const std::string& resource_id) const { - if (resource_id.empty() || parent_resource_id.empty()) { - // Both |resource_id| and |parent_resource_id| must be non-empty. - // Return an empty GURL as an error. - return GURL(); - } - - GURL result = base_url_.Resolve( - base::StringPrintf(kResourceURLForRemovalFormat, - net::EscapePath(parent_resource_id).c_str(), - net::EscapePath(resource_id).c_str())); - return AddStandardUrlParams(result); -} - -GURL GDataWapiUrlGenerator::GenerateInitiateUploadNewFileUrl( - const std::string& parent_resource_id) const { - GURL result = base_url_.Resolve( - base::StringPrintf(kInitiateUploadNewFileURLFormat, - net::EscapePath(parent_resource_id).c_str())); - return AddInitiateUploadUrlParams(result); -} - -GURL GDataWapiUrlGenerator::GenerateInitiateUploadExistingFileUrl( - const std::string& resource_id) const { - GURL result = base_url_.Resolve( - kInitiateUploadExistingFileURLPrefix + net::EscapePath(resource_id)); - return AddInitiateUploadUrlParams(result); -} - -GURL GDataWapiUrlGenerator::GenerateResourceListRootUrl() const { - return AddStandardUrlParams(base_url_.Resolve(kResourceListRootURL)); -} - -GURL GDataWapiUrlGenerator::GenerateAccountMetadataUrl( - bool include_installed_apps) const { - GURL result = AddStandardUrlParams(base_url_.Resolve(kAccountMetadataURL)); - if (include_installed_apps) { - result = net::AppendOrReplaceQueryParameter( - result, "include-installed-apps", "true"); - } - return result; -} - -GURL GDataWapiUrlGenerator::GenerateDownloadFileUrl( - const std::string& resource_id) const { - // Strip the file type prefix before the colon character. - size_t colon = resource_id.find(':'); - return base_download_url_.Resolve(net::EscapePath( - colon == std::string::npos ? resource_id - : resource_id.substr(colon + 1))); -} - } // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator.h b/chromium/google_apis/drive/gdata_wapi_url_generator.h index 05b565d404b..c0486cfff14 100644 --- a/chromium/google_apis/drive/gdata_wapi_url_generator.h +++ b/chromium/google_apis/drive/gdata_wapi_url_generator.h @@ -17,8 +17,7 @@ namespace google_apis { // for production, and the local server for testing. class GDataWapiUrlGenerator { public: - // The - GDataWapiUrlGenerator(const GURL& base_url, const GURL& base_download_url); + GDataWapiUrlGenerator(const GURL& base_url); ~GDataWapiUrlGenerator(); // The base URL for communicating with the WAPI server for production. @@ -31,53 +30,6 @@ class GDataWapiUrlGenerator { // show folders in the feed are added to document feed URLs. static GURL AddStandardUrlParams(const GURL& url); - // Adds additional parameters for initiate uploading as well as standard - // url params (as AddStandardUrlParams above does). - static GURL AddInitiateUploadUrlParams(const GURL& url); - - // Adds additional parameters for API version, output content type and to - // show folders in the feed are added to document feed URLs. - static GURL AddFeedUrlParams(const GURL& url, - int num_items_to_fetch); - - // Generates a URL for getting the resource list feed. - // - // The parameters other than |search_string| are mutually exclusive. - // If |override_url| is non-empty, other parameters are ignored. Or if - // |override_url| is empty, others are not used. Besides, |search_string| - // cannot be set together with |start_changestamp|. - // - // override_url: - // By default, a hard-coded base URL of the WAPI server is used. - // The base URL can be overridden by |override_url|. - // This is used for handling continuation of feeds (2nd page and onward). - // - // start_changestamp - // If |start_changestamp| is 0, URL for a full feed is generated. - // If |start_changestamp| is non-zero, URL for a delta feed is generated. - // - // search_string - // If |search_string| is non-empty, q=... parameter is added, and - // max-results=... parameter is adjusted for a search. - // - // directory_resource_id: - // If |directory_resource_id| is non-empty, a URL for fetching documents in - // a particular directory is generated. - // - GURL GenerateResourceListUrl( - const GURL& override_url, - int64 start_changestamp, - const std::string& search_string, - const std::string& directory_resource_id) const; - - // Generates a URL for searching resources by title (exact-match). - // |directory_resource_id| is optional parameter. When it is empty - // all the existing resources are target of the search. Otherwise, - // the search target is just under the directory with it. - GURL GenerateSearchByTitleUrl( - const std::string& title, - const std::string& directory_resource_id) const; - // Generates a URL for getting or editing the resource entry of // the given resource ID. GURL GenerateEditUrl(const std::string& resource_id) const; @@ -98,41 +50,8 @@ class GDataWapiUrlGenerator { GURL GenerateEditUrlWithEmbedOrigin(const std::string& resource_id, const GURL& embed_origin) const; - // Generates a URL for editing the contents in the directory specified - // by the given resource ID. - GURL GenerateContentUrl(const std::string& resource_id) const; - - // Generates a URL to remove an entry specified by |resource_id| from - // the directory specified by the given |parent_resource_id|. - GURL GenerateResourceUrlForRemoval(const std::string& parent_resource_id, - const std::string& resource_id) const; - - // Generates a URL to initiate uploading a new file to a directory - // specified by |parent_resource_id|. - GURL GenerateInitiateUploadNewFileUrl( - const std::string& parent_resource_id) const; - - // Generates a URL to initiate uploading file content to overwrite a - // file specified by |resource_id|. - GURL GenerateInitiateUploadExistingFileUrl( - const std::string& resource_id) const; - - // Generates a URL for getting the root resource list feed. - // Used to make changes in the root directory (ex. create a directory in the - // root directory) - GURL GenerateResourceListRootUrl() const; - - // Generates a URL for getting the account metadata feed. - // If |include_installed_apps| is set to true, the response will include the - // list of installed third party applications. - GURL GenerateAccountMetadataUrl(bool include_installed_apps) const; - - // Generates a URL for downloading a file. - GURL GenerateDownloadFileUrl(const std::string& resource_id) const; - private: const GURL base_url_; - const GURL base_download_url_; }; } // namespace google_apis diff --git a/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc b/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc index 63db63d6e3e..0ac8508d774 100644 --- a/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc +++ b/chromium/google_apis/drive/gdata_wapi_url_generator_unittest.cc @@ -6,6 +6,7 @@ #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" +#include "url/url_util.h" namespace google_apis { @@ -13,8 +14,7 @@ class GDataWapiUrlGeneratorTest : public testing::Test { public: GDataWapiUrlGeneratorTest() : url_generator_( - GURL(GDataWapiUrlGenerator::kBaseUrlForProduction), - GURL(GDataWapiUrlGenerator::kBaseDownloadUrlForProduction)) { + GURL(GDataWapiUrlGenerator::kBaseUrlForProduction)) { } protected: @@ -27,113 +27,6 @@ TEST_F(GDataWapiUrlGeneratorTest, AddStandardUrlParams) { GURL("http://www.example.com")).spec()); } -TEST_F(GDataWapiUrlGeneratorTest, AddInitiateUploadUrlParams) { - EXPECT_EQ("http://www.example.com/?convert=false&v=3&alt=json&showroot=true", - GDataWapiUrlGenerator::AddInitiateUploadUrlParams( - GURL("http://www.example.com")).spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, AddFeedUrlParams) { - EXPECT_EQ( - "http://www.example.com/?v=3&alt=json&showroot=true&" - "showfolders=true" - "&include-shared=true" - "&max-results=100", - GDataWapiUrlGenerator::AddFeedUrlParams(GURL("http://www.example.com"), - 100 // num_items_to_fetch - ).spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceListUrl) { - // This is the very basic URL for the GetResourceList request. - EXPECT_EQ("https://docs.google.com/feeds/default/private/full" - "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" - "&max-results=500", - url_generator_.GenerateResourceListUrl( - GURL(), // override_url, - 0, // start_changestamp, - std::string(), // search_string, - std::string() // directory resource ID - ).spec()); - - // With an override URL provided, the base URL is changed, but the default - // parameters remain as-is. - EXPECT_EQ("http://localhost/" - "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" - "&max-results=500", - url_generator_.GenerateResourceListUrl( - GURL("http://localhost/"), // override_url, - 0, // start_changestamp, - std::string(), // search_string, - std::string() // directory resource ID - ).spec()); - - // With a non-zero start_changestamp provided, the base URL is changed from - // "full" to "changes", and "start-index" parameter is added. - EXPECT_EQ("https://docs.google.com/feeds/default/private/changes" - "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" - "&max-results=500&start-index=100", - url_generator_.GenerateResourceListUrl( - GURL(), // override_url, - 100, // start_changestamp, - std::string(), // search_string, - std::string() // directory resource ID - ).spec()); - - // With a non-empty search string provided, "max-results" value is changed, - // and "q" parameter is added. - EXPECT_EQ("https://docs.google.com/feeds/default/private/full" - "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" - "&max-results=50&q=foo", - url_generator_.GenerateResourceListUrl( - GURL(), // override_url, - 0, // start_changestamp, - "foo", // search_string, - std::string() // directory resource ID - ).spec()); - - // With a non-empty directory resource ID provided, the base URL is - // changed, but the default parameters remain. - EXPECT_EQ( - "https://docs.google.com/feeds/default/private/full/XXX/contents" - "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" - "&max-results=500", - url_generator_.GenerateResourceListUrl(GURL(), // override_url, - 0, // start_changestamp, - std::string(), // search_string, - "XXX" // directory resource ID - ).spec()); - - // With a non-empty override_url provided, the base URL is changed, but - // the default parameters remain. Note that start-index should not be - // overridden. - EXPECT_EQ("http://example.com/" - "?start-index=123&v=3&alt=json&showroot=true&showfolders=true" - "&include-shared=true&max-results=500", - url_generator_.GenerateResourceListUrl( - GURL("http://example.com/?start-index=123"), // override_url, - 100, // start_changestamp, - std::string(), // search_string, - "XXX" // directory resource ID - ).spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateSearchByTitleUrl) { - EXPECT_EQ( - "https://docs.google.com/feeds/default/private/full" - "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" - "&max-results=500&title=search-title&title-exact=true", - url_generator_.GenerateSearchByTitleUrl( - "search-title", std::string()).spec()); - - EXPECT_EQ( - "https://docs.google.com/feeds/default/private/full/XXX/contents" - "?v=3&alt=json&showroot=true&showfolders=true&include-shared=true" - "&max-results=500&title=search-title&title-exact=true", - url_generator_.GenerateSearchByTitleUrl( - "search-title", "XXX").spec()); -} - TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrl) { EXPECT_EQ( "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json" @@ -148,6 +41,8 @@ TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrlWithoutParams) { } TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrlWithEmbedOrigin) { + url::AddStandardScheme("chrome-extension"); + EXPECT_EQ( "https://docs.google.com/feeds/default/private/full/XXX?v=3&alt=json" "&showroot=true&embedOrigin=chrome-extension%3A%2F%2Ftest", @@ -162,61 +57,4 @@ TEST_F(GDataWapiUrlGeneratorTest, GenerateEditUrlWithEmbedOrigin) { GURL()).spec()); } -TEST_F(GDataWapiUrlGeneratorTest, GenerateContentUrl) { - EXPECT_EQ( - "https://docs.google.com/feeds/default/private/full/" - "folder%3Aroot/contents?v=3&alt=json&showroot=true", - url_generator_.GenerateContentUrl("folder:root").spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceUrlForRemoval) { - EXPECT_EQ( - "https://docs.google.com/feeds/default/private/full/" - "folder%3Aroot/contents/file%3AABCDE?v=3&alt=json&showroot=true", - url_generator_.GenerateResourceUrlForRemoval( - "folder:root", "file:ABCDE").spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateInitiateUploadNewFileUrl) { - EXPECT_EQ( - "https://docs.google.com/feeds/upload/create-session/default/private/" - "full/folder%3Aabcde/contents?convert=false&v=3&alt=json&showroot=true", - url_generator_.GenerateInitiateUploadNewFileUrl("folder:abcde").spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateInitiateUploadExistingFileUrl) { - EXPECT_EQ( - "https://docs.google.com/feeds/upload/create-session/default/private/" - "full/file%3Aresource_id?convert=false&v=3&alt=json&showroot=true", - url_generator_.GenerateInitiateUploadExistingFileUrl( - "file:resource_id").spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateResourceListRootUrl) { - EXPECT_EQ( - "https://docs.google.com/feeds/default/private/full?v=3&alt=json" - "&showroot=true", - url_generator_.GenerateResourceListRootUrl().spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateAccountMetadataUrl) { - // Include installed apps. - EXPECT_EQ( - "https://docs.google.com/feeds/metadata/default" - "?v=3&alt=json&showroot=true&include-installed-apps=true", - url_generator_.GenerateAccountMetadataUrl(true).spec()); - - // Exclude installed apps. - EXPECT_EQ( - "https://docs.google.com/feeds/metadata/default?v=3&alt=json" - "&showroot=true", - url_generator_.GenerateAccountMetadataUrl(false).spec()); -} - -TEST_F(GDataWapiUrlGeneratorTest, GenerateDownloadFileUrl) { - EXPECT_EQ( - "https://www.googledrive.com/host/resourceId", - url_generator_.GenerateDownloadFileUrl("file:resourceId").spec()); -} - } // namespace google_apis diff --git a/chromium/google_apis/drive/task_util.cc b/chromium/google_apis/drive/task_util.cc index 3f149e434ef..420afacb844 100644 --- a/chromium/google_apis/drive/task_util.cc +++ b/chromium/google_apis/drive/task_util.cc @@ -8,9 +8,9 @@ namespace google_apis { -void RunTaskOnThread(scoped_refptr<base::SingleThreadTaskRunner> task_runner, +void RunTaskOnThread(scoped_refptr<base::SequencedTaskRunner> task_runner, const base::Closure& task) { - if (task_runner->BelongsToCurrentThread()) { + if (task_runner->RunsTasksOnCurrentThread()) { task.Run(); } else { const bool posted = task_runner->PostTask(FROM_HERE, task); diff --git a/chromium/google_apis/drive/task_util.h b/chromium/google_apis/drive/task_util.h index 443f719694e..3cba9bd2cf4 100644 --- a/chromium/google_apis/drive/task_util.h +++ b/chromium/google_apis/drive/task_util.h @@ -10,8 +10,8 @@ namespace google_apis { -// Runs task on the thread to which |task_runner| belongs. -void RunTaskOnThread(scoped_refptr<base::SingleThreadTaskRunner> task_runner, +// Runs task on a thread on which |task_runner| may run tasks. +void RunTaskOnThread(scoped_refptr<base::SequencedTaskRunner> task_runner, const base::Closure& task); namespace internal { @@ -95,17 +95,6 @@ struct ComposedCallback<void(T1, T2, T3, T4)> { } }; -// ComposedCallback with four arguments, and the second one is scoped_ptr. -template<typename T1, typename T2, typename D2, typename T3, typename T4> -struct ComposedCallback<void(T1, scoped_ptr<T2, D2>, T3, T4)> { - static void Run( - const base::Callback<void(const base::Closure&)>& runner, - const base::Callback<void(T1, scoped_ptr<T2, D2>, T3, T4)>& callback, - T1 arg1, scoped_ptr<T2, D2> arg2, T3 arg3, T4 arg4) { - runner.Run(base::Bind(callback, arg1, base::Passed(&arg2), arg3, arg4)); - } -}; - } // namespace internal // Returns callback that takes arguments (arg1, arg2, ...), create a closure diff --git a/chromium/google_apis/drive/test_util.cc b/chromium/google_apis/drive/test_util.cc index 20d8ad65a6f..e77d1cf69e8 100644 --- a/chromium/google_apis/drive/test_util.cc +++ b/chromium/google_apis/drive/test_util.cc @@ -57,7 +57,8 @@ void RunAndQuit(base::RunLoop* run_loop, const base::Closure& closure) { bool WriteStringToFile(const base::FilePath& file_path, const std::string& content) { - int result = file_util::WriteFile(file_path, content.data(), content.size()); + int result = base::WriteFile( + file_path, content.data(), static_cast<int>(content.size())); return content.size() == static_cast<size_t>(result); } diff --git a/chromium/google_apis/gaia/DEPS b/chromium/google_apis/gaia/DEPS deleted file mode 100644 index f445dee28bf..00000000000 --- a/chromium/google_apis/gaia/DEPS +++ /dev/null @@ -1,5 +0,0 @@ -specific_include_rules = { - ".*_[a-z]*test\.cc": [ - "+content/public/test/test_browser_thread_bundle.h", - ] -} diff --git a/chromium/google_apis/gaia/account_tracker.cc b/chromium/google_apis/gaia/account_tracker.cc new file mode 100644 index 00000000000..65d28369be9 --- /dev/null +++ b/chromium/google_apis/gaia/account_tracker.cc @@ -0,0 +1,293 @@ +// 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/gaia/account_tracker.h" + +#include "base/logging.h" +#include "base/stl_util.h" +#include "net/url_request/url_request_context_getter.h" + +namespace gaia { + +AccountTracker::AccountTracker( + IdentityProvider* identity_provider, + net::URLRequestContextGetter* request_context_getter) + : identity_provider_(identity_provider), + request_context_getter_(request_context_getter), + shutdown_called_(false) { + identity_provider_->AddObserver(this); + identity_provider_->GetTokenService()->AddObserver(this); +} + +AccountTracker::~AccountTracker() { + DCHECK(shutdown_called_); +} + +void AccountTracker::Shutdown() { + shutdown_called_ = true; + STLDeleteValues(&user_info_requests_); + identity_provider_->GetTokenService()->RemoveObserver(this); + identity_provider_->RemoveObserver(this); +} + +void AccountTracker::AddObserver(Observer* observer) { + observer_list_.AddObserver(observer); +} + +void AccountTracker::RemoveObserver(Observer* observer) { + observer_list_.RemoveObserver(observer); +} + +std::vector<AccountIds> AccountTracker::GetAccounts() const { + const std::string active_account_id = + identity_provider_->GetActiveAccountId(); + std::vector<AccountIds> accounts; + + for (std::map<std::string, AccountState>::const_iterator it = + accounts_.begin(); + it != accounts_.end(); + ++it) { + const AccountState& state = it->second; + bool is_visible = state.is_signed_in && !state.ids.gaia.empty(); + + if (it->first == active_account_id) { + if (is_visible) + accounts.insert(accounts.begin(), state.ids); + else + return std::vector<AccountIds>(); + + } else if (is_visible) { + accounts.push_back(state.ids); + } + } + return accounts; +} + +AccountIds AccountTracker::FindAccountIdsByGaiaId(const std::string& gaia_id) { + for (std::map<std::string, AccountState>::const_iterator it = + accounts_.begin(); + it != accounts_.end(); + ++it) { + const AccountState& state = it->second; + if (state.ids.gaia == gaia_id) { + return state.ids; + } + } + + return AccountIds(); +} + +void AccountTracker::OnRefreshTokenAvailable(const std::string& account_id) { + // Ignore refresh tokens if there is no active account ID at all. + if (identity_provider_->GetActiveAccountId().empty()) + return; + + DVLOG(1) << "AVAILABLE " << account_id; + UpdateSignInState(account_id, true); +} + +void AccountTracker::OnRefreshTokenRevoked(const std::string& account_id) { + DVLOG(1) << "REVOKED " << account_id; + UpdateSignInState(account_id, false); +} + +void AccountTracker::OnActiveAccountLogin() { + std::vector<std::string> accounts = + identity_provider_->GetTokenService()->GetAccounts(); + + DVLOG(1) << "LOGIN " << accounts.size() << " accounts available."; + + for (std::vector<std::string>::const_iterator it = accounts.begin(); + it != accounts.end(); + ++it) { + OnRefreshTokenAvailable(*it); + } +} + +void AccountTracker::OnActiveAccountLogout() { + DVLOG(1) << "LOGOUT"; + StopTrackingAllAccounts(); +} + +void AccountTracker::SetAccountStateForTest(AccountIds ids, bool is_signed_in) { + accounts_[ids.account_key].ids = ids; + accounts_[ids.account_key].is_signed_in = is_signed_in; + + DVLOG(1) << "SetAccountStateForTest " << ids.account_key << ":" + << is_signed_in; + + if (VLOG_IS_ON(1)) { + for (std::map<std::string, AccountState>::const_iterator it = + accounts_.begin(); + it != accounts_.end(); + ++it) { + DVLOG(1) << it->first << ":" << it->second.is_signed_in; + } + } +} + +void AccountTracker::NotifyAccountAdded(const AccountState& account) { + DCHECK(!account.ids.gaia.empty()); + FOR_EACH_OBSERVER( + Observer, observer_list_, OnAccountAdded(account.ids)); +} + +void AccountTracker::NotifyAccountRemoved(const AccountState& account) { + DCHECK(!account.ids.gaia.empty()); + FOR_EACH_OBSERVER( + Observer, observer_list_, OnAccountRemoved(account.ids)); +} + +void AccountTracker::NotifySignInChanged(const AccountState& account) { + DCHECK(!account.ids.gaia.empty()); + FOR_EACH_OBSERVER(Observer, + observer_list_, + OnAccountSignInChanged(account.ids, account.is_signed_in)); +} + +void AccountTracker::UpdateSignInState(const std::string account_key, + bool is_signed_in) { + StartTrackingAccount(account_key); + AccountState& account = accounts_[account_key]; + bool needs_gaia_id = account.ids.gaia.empty(); + bool was_signed_in = account.is_signed_in; + account.is_signed_in = is_signed_in; + + if (needs_gaia_id && is_signed_in) + StartFetchingUserInfo(account_key); + + if (!needs_gaia_id && (was_signed_in != is_signed_in)) + NotifySignInChanged(account); +} + +void AccountTracker::StartTrackingAccount(const std::string account_key) { + if (!ContainsKey(accounts_, account_key)) { + DVLOG(1) << "StartTracking " << account_key; + AccountState account_state; + account_state.ids.account_key = account_key; + account_state.ids.email = account_key; + account_state.is_signed_in = false; + accounts_.insert(make_pair(account_key, account_state)); + } +} + +void AccountTracker::StopTrackingAccount(const std::string account_key) { + DVLOG(1) << "StopTracking " << account_key; + if (ContainsKey(accounts_, account_key)) { + AccountState& account = accounts_[account_key]; + if (!account.ids.gaia.empty()) { + UpdateSignInState(account_key, false); + NotifyAccountRemoved(account); + } + accounts_.erase(account_key); + } + + if (ContainsKey(user_info_requests_, account_key)) + DeleteFetcher(user_info_requests_[account_key]); +} + +void AccountTracker::StopTrackingAllAccounts() { + while (!accounts_.empty()) + StopTrackingAccount(accounts_.begin()->first); +} + +void AccountTracker::StartFetchingUserInfo(const std::string account_key) { + if (ContainsKey(user_info_requests_, account_key)) + DeleteFetcher(user_info_requests_[account_key]); + + DVLOG(1) << "StartFetching " << account_key; + AccountIdFetcher* fetcher = + new AccountIdFetcher(identity_provider_->GetTokenService(), + request_context_getter_.get(), + this, + account_key); + user_info_requests_[account_key] = fetcher; + fetcher->Start(); +} + +void AccountTracker::OnUserInfoFetchSuccess(AccountIdFetcher* fetcher, + const std::string& gaia_id) { + const std::string& account_key = fetcher->account_key(); + DCHECK(ContainsKey(accounts_, account_key)); + AccountState& account = accounts_[account_key]; + + account.ids.gaia = gaia_id; + NotifyAccountAdded(account); + + if (account.is_signed_in) + NotifySignInChanged(account); + + DeleteFetcher(fetcher); +} + +void AccountTracker::OnUserInfoFetchFailure(AccountIdFetcher* fetcher) { + LOG(WARNING) << "Failed to get UserInfo for " << fetcher->account_key(); + std::string key = fetcher->account_key(); + DeleteFetcher(fetcher); + StopTrackingAccount(key); +} + +void AccountTracker::DeleteFetcher(AccountIdFetcher* fetcher) { + DVLOG(1) << "DeleteFetcher " << fetcher->account_key(); + const std::string& account_key = fetcher->account_key(); + DCHECK(ContainsKey(user_info_requests_, account_key)); + DCHECK_EQ(fetcher, user_info_requests_[account_key]); + user_info_requests_.erase(account_key); + delete fetcher; +} + +AccountIdFetcher::AccountIdFetcher( + OAuth2TokenService* token_service, + net::URLRequestContextGetter* request_context_getter, + AccountTracker* tracker, + const std::string& account_key) + : OAuth2TokenService::Consumer("gaia_account_tracker"), + token_service_(token_service), + request_context_getter_(request_context_getter), + tracker_(tracker), + account_key_(account_key) { +} + +AccountIdFetcher::~AccountIdFetcher() {} + +void AccountIdFetcher::Start() { + login_token_request_ = token_service_->StartRequest( + account_key_, OAuth2TokenService::ScopeSet(), this); +} + +void AccountIdFetcher::OnGetTokenSuccess( + const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) { + DCHECK_EQ(request, login_token_request_.get()); + + gaia_oauth_client_.reset(new gaia::GaiaOAuthClient(request_context_getter_)); + + const int kMaxGetUserIdRetries = 3; + gaia_oauth_client_->GetUserId(access_token, kMaxGetUserIdRetries, this); +} + +void AccountIdFetcher::OnGetTokenFailure( + const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) { + LOG(ERROR) << "OnGetTokenFailure: " << error.ToString(); + DCHECK_EQ(request, login_token_request_.get()); + tracker_->OnUserInfoFetchFailure(this); +} + +void AccountIdFetcher::OnGetUserIdResponse(const std::string& gaia_id) { + tracker_->OnUserInfoFetchSuccess(this, gaia_id); +} + +void AccountIdFetcher::OnOAuthError() { + LOG(ERROR) << "OnOAuthError"; + tracker_->OnUserInfoFetchFailure(this); +} + +void AccountIdFetcher::OnNetworkError(int response_code) { + LOG(ERROR) << "OnNetworkError " << response_code; + tracker_->OnUserInfoFetchFailure(this); +} + +} // namespace gaia diff --git a/chromium/google_apis/gaia/account_tracker.h b/chromium/google_apis/gaia/account_tracker.h new file mode 100644 index 00000000000..5a29ea62491 --- /dev/null +++ b/chromium/google_apis/gaia/account_tracker.h @@ -0,0 +1,149 @@ +// 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_GAIA_ACCOUNT_TRACKER_H_ +#define GOOGLE_APIS_GAIA_ACCOUNT_TRACKER_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "base/observer_list.h" +#include "google_apis/gaia/gaia_oauth_client.h" +#include "google_apis/gaia/identity_provider.h" +#include "google_apis/gaia/oauth2_token_service.h" + +class GoogleServiceAuthError; + +namespace net { +class URLRequestContextGetter; +} + +namespace gaia { + +struct AccountIds { + std::string account_key; // The account ID used by OAuth2TokenService. + std::string gaia; + std::string email; +}; + +class AccountIdFetcher; + +// The AccountTracker keeps track of what accounts exist on the +// profile and the state of their credentials. The tracker fetches the +// gaia ID of each account it knows about. +// +// The AccountTracker maintains these invariants: +// 1. Events are only fired after the gaia ID has been fetched. +// 2. Add/Remove and SignIn/SignOut pairs are always generated in order. +// 3. SignIn follows Add, and there will be a SignOut between SignIn & Remove. +// 4. If there is no primary account, there are no other accounts. +class AccountTracker : public OAuth2TokenService::Observer, + public IdentityProvider::Observer { + public: + AccountTracker(IdentityProvider* identity_provider, + net::URLRequestContextGetter* request_context_getter); + virtual ~AccountTracker(); + + class Observer { + public: + virtual void OnAccountAdded(const AccountIds& ids) = 0; + virtual void OnAccountRemoved(const AccountIds& ids) = 0; + virtual void OnAccountSignInChanged(const AccountIds& ids, + bool is_signed_in) = 0; + }; + + void Shutdown(); + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Returns the list of accounts that are signed in, and for which gaia IDs + // have been fetched. The primary account for the profile will be first + // in the vector. Additional accounts will be in order of their gaia IDs. + std::vector<AccountIds> GetAccounts() const; + AccountIds FindAccountIdsByGaiaId(const std::string& gaia_id); + + // OAuth2TokenService::Observer implementation. + virtual void OnRefreshTokenAvailable(const std::string& account_key) OVERRIDE; + virtual void OnRefreshTokenRevoked(const std::string& account_key) OVERRIDE; + + void OnUserInfoFetchSuccess(AccountIdFetcher* fetcher, + const std::string& gaia_id); + void OnUserInfoFetchFailure(AccountIdFetcher* fetcher); + + // IdentityProvider::Observer implementation. + virtual void OnActiveAccountLogin() OVERRIDE; + virtual void OnActiveAccountLogout() OVERRIDE; + + // Sets the state of an account. Does not fire notifications. + void SetAccountStateForTest(AccountIds ids, bool is_signed_in); + + IdentityProvider* identity_provider() { return identity_provider_; } + + private: + struct AccountState { + AccountIds ids; + bool is_signed_in; + }; + + void NotifyAccountAdded(const AccountState& account); + void NotifyAccountRemoved(const AccountState& account); + void NotifySignInChanged(const AccountState& account); + + void UpdateSignInState(const std::string account_key, bool is_signed_in); + + void StartTrackingAccount(const std::string account_key); + void StopTrackingAccount(const std::string account_key); + void StopTrackingAllAccounts(); + void StartFetchingUserInfo(const std::string account_key); + void DeleteFetcher(AccountIdFetcher* fetcher); + + IdentityProvider* identity_provider_; // Not owned. + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + std::map<std::string, AccountIdFetcher*> user_info_requests_; + std::map<std::string, AccountState> accounts_; + ObserverList<Observer> observer_list_; + bool shutdown_called_; +}; + +class AccountIdFetcher : public OAuth2TokenService::Consumer, + public gaia::GaiaOAuthClient::Delegate { + public: + AccountIdFetcher(OAuth2TokenService* token_service, + net::URLRequestContextGetter* request_context_getter, + AccountTracker* tracker, + const std::string& account_key); + virtual ~AccountIdFetcher(); + + const std::string& account_key() { return account_key_; } + + void Start(); + + // OAuth2TokenService::Consumer implementation. + virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) OVERRIDE; + + // gaia::GaiaOAuthClient::Delegate implementation. + virtual void OnGetUserIdResponse(const std::string& gaia_id) OVERRIDE; + virtual void OnOAuthError() OVERRIDE; + virtual void OnNetworkError(int response_code) OVERRIDE; + + private: + OAuth2TokenService* token_service_; + net::URLRequestContextGetter* request_context_getter_; + AccountTracker* tracker_; + const std::string account_key_; + + scoped_ptr<OAuth2TokenService::Request> login_token_request_; + scoped_ptr<gaia::GaiaOAuthClient> gaia_oauth_client_; +}; + +} // namespace extensions + +#endif // GOOGLE_APIS_GAIA_ACCOUNT_TRACKER_H_ diff --git a/chromium/google_apis/gaia/account_tracker_unittest.cc b/chromium/google_apis/gaia/account_tracker_unittest.cc new file mode 100644 index 00000000000..fe8346eeb25 --- /dev/null +++ b/chromium/google_apis/gaia/account_tracker_unittest.cc @@ -0,0 +1,816 @@ +// 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/gaia/account_tracker.h" + +#include <algorithm> +#include <vector> + +#include "base/message_loop/message_loop.h" +#include "base/strings/stringprintf.h" +#include "google_apis/gaia/fake_identity_provider.h" +#include "google_apis/gaia/fake_oauth2_token_service.h" +#include "google_apis/gaia/gaia_oauth_client.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kPrimaryAccountKey[] = "primary_account@example.com"; + +enum TrackingEventType { + ADDED, + REMOVED, + SIGN_IN, + SIGN_OUT +}; + +std::string AccountKeyToObfuscatedId(const std::string email) { + return "obfid-" + email; +} + +class TrackingEvent { + public: + TrackingEvent(TrackingEventType type, + const std::string& account_key, + const std::string& gaia_id) + : type_(type), + account_key_(account_key), + gaia_id_(gaia_id) {} + + TrackingEvent(TrackingEventType type, + const std::string& account_key) + : type_(type), + account_key_(account_key), + gaia_id_(AccountKeyToObfuscatedId(account_key)) {} + + bool operator==(const TrackingEvent& event) const { + return type_ == event.type_ && account_key_ == event.account_key_ && + gaia_id_ == event.gaia_id_; + } + + std::string ToString() const { + const char * typestr = "INVALID"; + switch (type_) { + case ADDED: + typestr = "ADD"; + break; + case REMOVED: + typestr = "REM"; + break; + case SIGN_IN: + typestr = " IN"; + break; + case SIGN_OUT: + typestr = "OUT"; + break; + } + return base::StringPrintf("{ type: %s, email: %s, gaia: %s }", + typestr, + account_key_.c_str(), + gaia_id_.c_str()); + } + + private: + friend bool CompareByUser(TrackingEvent a, TrackingEvent b); + + TrackingEventType type_; + std::string account_key_; + std::string gaia_id_; +}; + +bool CompareByUser(TrackingEvent a, TrackingEvent b) { + return a.account_key_ < b.account_key_; +} + +std::string Str(const std::vector<TrackingEvent>& events) { + std::string str = "["; + bool needs_comma = false; + for (std::vector<TrackingEvent>::const_iterator it = + events.begin(); it != events.end(); ++it) { + if (needs_comma) + str += ",\n "; + needs_comma = true; + str += it->ToString(); + } + str += "]"; + return str; +} + +} // namespace + +namespace gaia { + +class AccountTrackerObserver : public AccountTracker::Observer { + public: + AccountTrackerObserver() {} + virtual ~AccountTrackerObserver() {} + + testing::AssertionResult CheckEvents(); + testing::AssertionResult CheckEvents(const TrackingEvent& e1); + testing::AssertionResult CheckEvents(const TrackingEvent& e1, + const TrackingEvent& e2); + testing::AssertionResult CheckEvents(const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3); + testing::AssertionResult CheckEvents(const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3, + const TrackingEvent& e4); + testing::AssertionResult CheckEvents(const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3, + const TrackingEvent& e4, + const TrackingEvent& e5); + testing::AssertionResult CheckEvents(const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3, + const TrackingEvent& e4, + const TrackingEvent& e5, + const TrackingEvent& e6); + void Clear(); + void SortEventsByUser(); + + // AccountTracker::Observer implementation + virtual void OnAccountAdded(const AccountIds& ids) OVERRIDE; + virtual void OnAccountRemoved(const AccountIds& ids) OVERRIDE; + virtual void OnAccountSignInChanged(const AccountIds& ids, bool is_signed_in) + OVERRIDE; + + private: + testing::AssertionResult CheckEvents( + const std::vector<TrackingEvent>& events); + + std::vector<TrackingEvent> events_; +}; + +void AccountTrackerObserver::OnAccountAdded(const AccountIds& ids) { + events_.push_back(TrackingEvent(ADDED, ids.email, ids.gaia)); +} + +void AccountTrackerObserver::OnAccountRemoved(const AccountIds& ids) { + events_.push_back(TrackingEvent(REMOVED, ids.email, ids.gaia)); +} + +void AccountTrackerObserver::OnAccountSignInChanged(const AccountIds& ids, + bool is_signed_in) { + events_.push_back( + TrackingEvent(is_signed_in ? SIGN_IN : SIGN_OUT, ids.email, ids.gaia)); +} + +void AccountTrackerObserver::Clear() { + events_.clear(); +} + +void AccountTrackerObserver::SortEventsByUser() { + std::stable_sort(events_.begin(), events_.end(), CompareByUser); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents() { + std::vector<TrackingEvent> events; + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1) { + std::vector<TrackingEvent> events; + events.push_back(e1); + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1, + const TrackingEvent& e2) { + std::vector<TrackingEvent> events; + events.push_back(e1); + events.push_back(e2); + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3) { + std::vector<TrackingEvent> events; + events.push_back(e1); + events.push_back(e2); + events.push_back(e3); + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3, + const TrackingEvent& e4) { + std::vector<TrackingEvent> events; + events.push_back(e1); + events.push_back(e2); + events.push_back(e3); + events.push_back(e4); + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3, + const TrackingEvent& e4, + const TrackingEvent& e5) { + std::vector<TrackingEvent> events; + events.push_back(e1); + events.push_back(e2); + events.push_back(e3); + events.push_back(e4); + events.push_back(e5); + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const TrackingEvent& e1, + const TrackingEvent& e2, + const TrackingEvent& e3, + const TrackingEvent& e4, + const TrackingEvent& e5, + const TrackingEvent& e6) { + std::vector<TrackingEvent> events; + events.push_back(e1); + events.push_back(e2); + events.push_back(e3); + events.push_back(e4); + events.push_back(e5); + events.push_back(e6); + return CheckEvents(events); +} + +testing::AssertionResult AccountTrackerObserver::CheckEvents( + const std::vector<TrackingEvent>& events) { + std::string maybe_newline = (events.size() + events_.size()) > 2 ? "\n" : ""; + testing::AssertionResult result( + (events_ == events) + ? testing::AssertionSuccess() + : (testing::AssertionFailure() + << "Expected " << maybe_newline << Str(events) << ", " + << maybe_newline << "Got " << maybe_newline << Str(events_))); + events_.clear(); + return result; +} + +class IdentityAccountTrackerTest : public testing::Test { + public: + IdentityAccountTrackerTest() {} + + virtual ~IdentityAccountTrackerTest() {} + + virtual void SetUp() OVERRIDE { + + fake_oauth2_token_service_.reset(new FakeOAuth2TokenService()); + + fake_identity_provider_.reset( + new FakeIdentityProvider(fake_oauth2_token_service_.get())); + + account_tracker_.reset( + new AccountTracker(fake_identity_provider_.get(), + new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()))); + account_tracker_->AddObserver(&observer_); + } + + virtual void TearDown() OVERRIDE { + account_tracker_->RemoveObserver(&observer_); + account_tracker_->Shutdown(); + } + + AccountTrackerObserver* observer() { + return &observer_; + } + + AccountTracker* account_tracker() { + return account_tracker_.get(); + } + + // Helpers to pass fake events to the tracker. + + void NotifyLogin(const std::string account_key) { + identity_provider()->LogIn(account_key); + } + + void NotifyLogout() { identity_provider()->LogOut(); } + + void NotifyTokenAvailable(const std::string& username) { + fake_oauth2_token_service_->AddAccount(username); + } + + void NotifyTokenRevoked(const std::string& username) { + fake_oauth2_token_service_->RemoveAccount(username); + } + + // Helpers to fake access token and user info fetching + void IssueAccessToken(const std::string& username) { + fake_oauth2_token_service_->IssueAllTokensForAccount( + username, "access_token-" + username, base::Time::Max()); + } + + std::string GetValidTokenInfoResponse(const std::string account_key) { + return std::string("{ \"id\": \"") + AccountKeyToObfuscatedId(account_key) + + "\" }"; + } + + void ReturnOAuthUrlFetchResults(int fetcher_id, + net::HttpStatusCode response_code, + const std::string& response_string); + + void ReturnOAuthUrlFetchSuccess(const std::string& account_key); + void ReturnOAuthUrlFetchFailure(const std::string& account_key); + + void SetupPrimaryLogin() { + // Initial setup for tests that start with a signed in profile. + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + observer()->Clear(); + } + + std::string active_account_id() { + return identity_provider()->GetActiveAccountId(); + } + + private: + FakeIdentityProvider* identity_provider() { + return static_cast<FakeIdentityProvider*>( + account_tracker_->identity_provider()); + } + + base::MessageLoopForIO message_loop_; // net:: stuff needs IO message loop. + net::TestURLFetcherFactory test_fetcher_factory_; + scoped_ptr<FakeOAuth2TokenService> fake_oauth2_token_service_; + scoped_ptr<FakeIdentityProvider> fake_identity_provider_; + + scoped_ptr<AccountTracker> account_tracker_; + AccountTrackerObserver observer_; +}; + +void IdentityAccountTrackerTest::ReturnOAuthUrlFetchResults( + int fetcher_id, + net::HttpStatusCode response_code, + const std::string& response_string) { + + net::TestURLFetcher* fetcher = + test_fetcher_factory_.GetFetcherByID(fetcher_id); + ASSERT_TRUE(fetcher); + fetcher->set_response_code(response_code); + fetcher->SetResponseString(response_string); + fetcher->delegate()->OnURLFetchComplete(fetcher); +} + +void IdentityAccountTrackerTest::ReturnOAuthUrlFetchSuccess( + const std::string& account_key) { + IssueAccessToken(account_key); + ReturnOAuthUrlFetchResults(gaia::GaiaOAuthClient::kUrlFetcherId, + net::HTTP_OK, + GetValidTokenInfoResponse(account_key)); +} + +void IdentityAccountTrackerTest::ReturnOAuthUrlFetchFailure( + const std::string& account_key) { + IssueAccessToken(account_key); + ReturnOAuthUrlFetchResults( + gaia::GaiaOAuthClient::kUrlFetcherId, net::HTTP_BAD_REQUEST, ""); +} + +// Primary tests just involve the Active account + +TEST_F(IdentityAccountTrackerTest, PrimaryNoEventsBeforeLogin) { + NotifyTokenAvailable(kPrimaryAccountKey); + NotifyTokenRevoked(kPrimaryAccountKey); + NotifyLogout(); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryLoginThenTokenAvailable) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents()); + + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(ADDED, kPrimaryAccountKey), + TrackingEvent(SIGN_IN, kPrimaryAccountKey))); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryTokenAvailableThenLogin) { + NotifyTokenAvailable(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents()); + + NotifyLogin(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(ADDED, kPrimaryAccountKey), + TrackingEvent(SIGN_IN, kPrimaryAccountKey))); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryTokenAvailableAndRevokedThenLogin) { + NotifyTokenAvailable(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents()); + + NotifyLogin(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(ADDED, kPrimaryAccountKey), + TrackingEvent(SIGN_IN, kPrimaryAccountKey))); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryRevokeThenLogout) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + observer()->Clear(); + + NotifyTokenRevoked(kPrimaryAccountKey); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, kPrimaryAccountKey))); + + NotifyLogout(); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(REMOVED, kPrimaryAccountKey))); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryRevokeThenLogin) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + NotifyTokenRevoked(kPrimaryAccountKey); + observer()->Clear(); + + NotifyLogin(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryRevokeThenTokenAvailable) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + NotifyTokenRevoked(kPrimaryAccountKey); + observer()->Clear(); + + NotifyTokenAvailable(kPrimaryAccountKey); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_IN, kPrimaryAccountKey))); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryLogoutThenRevoke) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + observer()->Clear(); + + NotifyLogout(); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, kPrimaryAccountKey), + TrackingEvent(REMOVED, kPrimaryAccountKey))); + + NotifyTokenRevoked(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(IdentityAccountTrackerTest, PrimaryLogoutFetchCancelAvailable) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + // TokenAvailable kicks off a fetch. Logout without satisfying it. + NotifyLogout(); + EXPECT_TRUE(observer()->CheckEvents()); + + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, kPrimaryAccountKey), + TrackingEvent(SIGN_IN, kPrimaryAccountKey))); +} + +// Non-primary accounts + +TEST_F(IdentityAccountTrackerTest, Available) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents()); + + ReturnOAuthUrlFetchSuccess("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "user@example.com"), + TrackingEvent(SIGN_IN, "user@example.com"))); +} + +TEST_F(IdentityAccountTrackerTest, Revoke) { + SetupPrimaryLogin(); + + account_tracker()->OnRefreshTokenRevoked("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(IdentityAccountTrackerTest, AvailableRevokeAvailable) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchSuccess("user@example.com"); + NotifyTokenRevoked("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "user@example.com"), + TrackingEvent(SIGN_IN, "user@example.com"), + TrackingEvent(SIGN_OUT, "user@example.com"))); + + NotifyTokenAvailable("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(SIGN_IN, "user@example.com"))); +} + +TEST_F(IdentityAccountTrackerTest, AvailableRevokeAvailableWithPendingFetch) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("user@example.com"); + NotifyTokenRevoked("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents()); + + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchSuccess("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "user@example.com"), + TrackingEvent(SIGN_IN, "user@example.com"))); +} + +TEST_F(IdentityAccountTrackerTest, AvailableRevokeRevoke) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchSuccess("user@example.com"); + NotifyTokenRevoked("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "user@example.com"), + TrackingEvent(SIGN_IN, "user@example.com"), + TrackingEvent(SIGN_OUT, "user@example.com"))); + + NotifyTokenRevoked("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(IdentityAccountTrackerTest, AvailableAvailable) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchSuccess("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "user@example.com"), + TrackingEvent(SIGN_IN, "user@example.com"))); + + NotifyTokenAvailable("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(IdentityAccountTrackerTest, TwoAccounts) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("alpha@example.com"); + ReturnOAuthUrlFetchSuccess("alpha@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "alpha@example.com"), + TrackingEvent(SIGN_IN, "alpha@example.com"))); + + NotifyTokenAvailable("beta@example.com"); + ReturnOAuthUrlFetchSuccess("beta@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "beta@example.com"), + TrackingEvent(SIGN_IN, "beta@example.com"))); + + NotifyTokenRevoked("alpha@example.com"); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, "alpha@example.com"))); + + NotifyTokenRevoked("beta@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(SIGN_OUT, "beta@example.com"))); +} + +TEST_F(IdentityAccountTrackerTest, AvailableTokenFetchFailAvailable) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchFailure("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents()); + + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchSuccess("user@example.com"); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "user@example.com"), + TrackingEvent(SIGN_IN, "user@example.com"))); +} + +TEST_F(IdentityAccountTrackerTest, MultiSignOutSignIn) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("alpha@example.com"); + ReturnOAuthUrlFetchSuccess("alpha@example.com"); + NotifyTokenAvailable("beta@example.com"); + ReturnOAuthUrlFetchSuccess("beta@example.com"); + + observer()->SortEventsByUser(); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "alpha@example.com"), + TrackingEvent(SIGN_IN, "alpha@example.com"), + TrackingEvent(ADDED, "beta@example.com"), + TrackingEvent(SIGN_IN, "beta@example.com"))); + + NotifyLogout(); + observer()->SortEventsByUser(); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(SIGN_OUT, "alpha@example.com"), + TrackingEvent(REMOVED, "alpha@example.com"), + TrackingEvent(SIGN_OUT, "beta@example.com"), + TrackingEvent(REMOVED, "beta@example.com"), + TrackingEvent(SIGN_OUT, kPrimaryAccountKey), + TrackingEvent(REMOVED, kPrimaryAccountKey))); + + // No events fire at all while profile is signed out. + NotifyTokenRevoked("alpha@example.com"); + NotifyTokenAvailable("gamma@example.com"); + EXPECT_TRUE(observer()->CheckEvents()); + + // Signing the profile in again will resume tracking all accounts. + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess("beta@example.com"); + ReturnOAuthUrlFetchSuccess("gamma@example.com"); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + observer()->SortEventsByUser(); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(ADDED, "beta@example.com"), + TrackingEvent(SIGN_IN, "beta@example.com"), + TrackingEvent(ADDED, "gamma@example.com"), + TrackingEvent(SIGN_IN, "gamma@example.com"), + TrackingEvent(ADDED, kPrimaryAccountKey), + TrackingEvent(SIGN_IN, kPrimaryAccountKey))); + + // Revoking the primary token does not affect other accounts. + NotifyTokenRevoked(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(SIGN_OUT, kPrimaryAccountKey))); + + NotifyTokenAvailable(kPrimaryAccountKey); + EXPECT_TRUE(observer()->CheckEvents( + TrackingEvent(SIGN_IN, kPrimaryAccountKey))); +} + +// Primary/non-primary interactions + +TEST_F(IdentityAccountTrackerTest, MultiNoEventsBeforeLogin) { + NotifyTokenAvailable(kPrimaryAccountKey); + NotifyTokenAvailable("user@example.com"); + NotifyTokenRevoked("user@example.com"); + NotifyTokenRevoked(kPrimaryAccountKey); + NotifyLogout(); + EXPECT_TRUE(observer()->CheckEvents()); +} + +TEST_F(IdentityAccountTrackerTest, MultiLogoutRemovesAllAccounts) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchSuccess("user@example.com"); + observer()->Clear(); + + NotifyLogout(); + observer()->SortEventsByUser(); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, kPrimaryAccountKey), + TrackingEvent(REMOVED, kPrimaryAccountKey), + TrackingEvent(SIGN_OUT, "user@example.com"), + TrackingEvent(REMOVED, "user@example.com"))); +} + +TEST_F(IdentityAccountTrackerTest, MultiRevokePrimaryDoesNotRemoveAllAccounts) { + NotifyLogin(kPrimaryAccountKey); + NotifyTokenAvailable(kPrimaryAccountKey); + ReturnOAuthUrlFetchSuccess(kPrimaryAccountKey); + NotifyTokenAvailable("user@example.com"); + ReturnOAuthUrlFetchSuccess("user@example.com"); + observer()->Clear(); + + NotifyTokenRevoked(kPrimaryAccountKey); + observer()->SortEventsByUser(); + EXPECT_TRUE( + observer()->CheckEvents(TrackingEvent(SIGN_OUT, kPrimaryAccountKey))); +} + +TEST_F(IdentityAccountTrackerTest, GetAccountsPrimary) { + SetupPrimaryLogin(); + + std::vector<AccountIds> ids = account_tracker()->GetAccounts(); + EXPECT_EQ(1ul, ids.size()); + EXPECT_EQ(kPrimaryAccountKey, ids[0].account_key); + EXPECT_EQ(AccountKeyToObfuscatedId(kPrimaryAccountKey), ids[0].gaia); +} + +TEST_F(IdentityAccountTrackerTest, GetAccountsSignedOut) { + std::vector<AccountIds> ids = account_tracker()->GetAccounts(); + EXPECT_EQ(0ul, ids.size()); +} + +TEST_F(IdentityAccountTrackerTest, GetAccountsOnlyReturnAccountsWithTokens) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("alpha@example.com"); + NotifyTokenAvailable("beta@example.com"); + ReturnOAuthUrlFetchSuccess("beta@example.com"); + + std::vector<AccountIds> ids = account_tracker()->GetAccounts(); + EXPECT_EQ(2ul, ids.size()); + EXPECT_EQ(kPrimaryAccountKey, ids[0].account_key); + EXPECT_EQ(AccountKeyToObfuscatedId(kPrimaryAccountKey), ids[0].gaia); + EXPECT_EQ("beta@example.com", ids[1].account_key); + EXPECT_EQ(AccountKeyToObfuscatedId("beta@example.com"), ids[1].gaia); +} + +TEST_F(IdentityAccountTrackerTest, GetAccountsSortOrder) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("zeta@example.com"); + ReturnOAuthUrlFetchSuccess("zeta@example.com"); + NotifyTokenAvailable("alpha@example.com"); + ReturnOAuthUrlFetchSuccess("alpha@example.com"); + + // The primary account will be first in the vector. Remaining accounts + // will be sorted by gaia ID. + std::vector<AccountIds> ids = account_tracker()->GetAccounts(); + EXPECT_EQ(3ul, ids.size()); + EXPECT_EQ(kPrimaryAccountKey, ids[0].account_key); + EXPECT_EQ(AccountKeyToObfuscatedId(kPrimaryAccountKey), ids[0].gaia); + EXPECT_EQ("alpha@example.com", ids[1].account_key); + EXPECT_EQ(AccountKeyToObfuscatedId("alpha@example.com"), ids[1].gaia); + EXPECT_EQ("zeta@example.com", ids[2].account_key); + EXPECT_EQ(AccountKeyToObfuscatedId("zeta@example.com"), ids[2].gaia); +} + +TEST_F(IdentityAccountTrackerTest, + GetAccountsReturnNothingWhenPrimarySignedOut) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("zeta@example.com"); + ReturnOAuthUrlFetchSuccess("zeta@example.com"); + NotifyTokenAvailable("alpha@example.com"); + ReturnOAuthUrlFetchSuccess("alpha@example.com"); + + NotifyTokenRevoked(kPrimaryAccountKey); + + std::vector<AccountIds> ids = account_tracker()->GetAccounts(); + EXPECT_EQ(0ul, ids.size()); +} + +TEST_F(IdentityAccountTrackerTest, FindAccountIdsByGaiaIdPrimary) { + SetupPrimaryLogin(); + + AccountIds ids = account_tracker()->FindAccountIdsByGaiaId( + AccountKeyToObfuscatedId(kPrimaryAccountKey)); + EXPECT_EQ(kPrimaryAccountKey, ids.account_key); + EXPECT_EQ(kPrimaryAccountKey, ids.email); + EXPECT_EQ(AccountKeyToObfuscatedId(kPrimaryAccountKey), ids.gaia); +} + +TEST_F(IdentityAccountTrackerTest, FindAccountIdsByGaiaIdNotFound) { + SetupPrimaryLogin(); + + AccountIds ids = account_tracker()->FindAccountIdsByGaiaId( + AccountKeyToObfuscatedId("notfound@example.com")); + EXPECT_TRUE(ids.account_key.empty()); + EXPECT_TRUE(ids.email.empty()); + EXPECT_TRUE(ids.gaia.empty()); +} + +TEST_F(IdentityAccountTrackerTest, + FindAccountIdsByGaiaIdReturnEmptyWhenPrimarySignedOut) { + SetupPrimaryLogin(); + + NotifyTokenAvailable("zeta@example.com"); + ReturnOAuthUrlFetchSuccess("zeta@example.com"); + NotifyTokenAvailable("alpha@example.com"); + ReturnOAuthUrlFetchSuccess("alpha@example.com"); + + NotifyTokenRevoked(kPrimaryAccountKey); + + AccountIds ids = + account_tracker()->FindAccountIdsByGaiaId(kPrimaryAccountKey); + EXPECT_TRUE(ids.account_key.empty()); + EXPECT_TRUE(ids.email.empty()); + EXPECT_TRUE(ids.gaia.empty()); + + ids = account_tracker()->FindAccountIdsByGaiaId("alpha@example.com"); + EXPECT_TRUE(ids.account_key.empty()); + EXPECT_TRUE(ids.email.empty()); + EXPECT_TRUE(ids.gaia.empty()); +} + +} // namespace gaia diff --git a/chromium/google_apis/gaia/fake_gaia.cc b/chromium/google_apis/gaia/fake_gaia.cc index 322ed54ff9f..4cfe6040e53 100644 --- a/chromium/google_apis/gaia/fake_gaia.cc +++ b/chromium/google_apis/gaia/fake_gaia.cc @@ -4,35 +4,123 @@ #include "google_apis/gaia/fake_gaia.h" +#include <vector> + #include "base/base_paths.h" +#include "base/bind.h" +#include "base/bind_helpers.h" #include "base/file_util.h" #include "base/files/file_path.h" #include "base/json/json_writer.h" #include "base/logging.h" +#include "base/memory/linked_ptr.h" #include "base/path_service.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" #include "base/values.h" +#include "google_apis/gaia/gaia_constants.h" #include "google_apis/gaia/gaia_urls.h" #include "net/base/url_util.h" +#include "net/cookies/parsed_cookie.h" #include "net/http/http_status_code.h" #include "net/test/embedded_test_server/http_request.h" #include "net/test/embedded_test_server/http_response.h" #include "url/url_parse.h" +#define REGISTER_RESPONSE_HANDLER(url, method) \ + request_handlers_.insert(std::make_pair( \ + url.path(), base::Bind(&FakeGaia::method, base::Unretained(this)))) + +#define REGISTER_PATH_RESPONSE_HANDLER(path, method) \ + request_handlers_.insert(std::make_pair( \ + path, base::Bind(&FakeGaia::method, base::Unretained(this)))) + using namespace net::test_server; namespace { + const base::FilePath::CharType kServiceLogin[] = FILE_PATH_LITERAL("google_apis/test/service_login.html"); + +// OAuth2 Authentication header value prefix. +const char kAuthHeaderBearer[] = "Bearer "; +const char kAuthHeaderOAuth[] = "OAuth "; + +const char kListAccountsResponseFormat[] = + "[\"gaia.l.a.r\",[[\"gaia.l.a\",1,\"\",\"%s\",\"\",1,1,0]]]"; + +typedef std::map<std::string, std::string> CookieMap; + +// Parses cookie name-value map our of |request|. +CookieMap GetRequestCookies(const HttpRequest& request) { + CookieMap result; + std::map<std::string, std::string>::const_iterator iter = + request.headers.find("Cookie"); + if (iter != request.headers.end()) { + std::vector<std::string> cookie_nv_pairs; + base::SplitString(iter->second, ' ', &cookie_nv_pairs); + for(std::vector<std::string>::const_iterator cookie_line = + cookie_nv_pairs.begin(); + cookie_line != cookie_nv_pairs.end(); + ++cookie_line) { + std::vector<std::string> name_value; + base::SplitString(*cookie_line, '=', &name_value); + if (name_value.size() != 2) + continue; + + std::string value = name_value[1]; + if (value.size() && value[value.size() - 1] == ';') + value = value.substr(0, value.size() -1); + + result.insert(std::make_pair(name_value[0], value)); + } + } + return result; +} + +// Extracts the |access_token| from authorization header of |request|. +bool GetAccessToken(const HttpRequest& request, + const char* auth_token_prefix, + std::string* access_token) { + std::map<std::string, std::string>::const_iterator auth_header_entry = + request.headers.find("Authorization"); + if (auth_header_entry != request.headers.end()) { + if (StartsWithASCII(auth_header_entry->second, auth_token_prefix, true)) { + *access_token = auth_header_entry->second.substr( + strlen(auth_token_prefix)); + return true; + } + } + + return false; +} + +void SetCookies(BasicHttpResponse* http_response, + const std::string& sid_cookie, + const std::string& lsid_cookie) { + http_response->AddCustomHeader( + "Set-Cookie", + base::StringPrintf("SID=%s; Path=/; HttpOnly;", sid_cookie.c_str())); + http_response->AddCustomHeader( + "Set-Cookie", + base::StringPrintf("LSID=%s; Path=/; HttpOnly;", lsid_cookie.c_str())); } +} // namespace + FakeGaia::AccessTokenInfo::AccessTokenInfo() : expires_in(3600) {} FakeGaia::AccessTokenInfo::~AccessTokenInfo() {} +FakeGaia::MergeSessionParams::MergeSessionParams() { +} + +FakeGaia::MergeSessionParams::~MergeSessionParams() { +} + FakeGaia::FakeGaia() { base::FilePath source_root_dir; PathService::Get(base::DIR_SOURCE_ROOT, &source_root_dir); @@ -43,100 +131,66 @@ FakeGaia::FakeGaia() { FakeGaia::~FakeGaia() {} -scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) { +void FakeGaia::SetMergeSessionParams( + const MergeSessionParams& params) { + merge_session_params_ = params; +} + +void FakeGaia::Initialize() { GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); + // Handles /MergeSession GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->merge_session_url(), HandleMergeSession); + + // Handles /o/oauth2/programmatic_auth GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->client_login_to_oauth2_url(), HandleProgramaticAuth); + + // Handles /ServiceLogin GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->service_login_url(), HandleServiceLogin); + // Handles /OAuthLogin GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->oauth1_login_url(), HandleOAuthLogin); + + // Handles /ServiceLoginAuth GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->service_login_auth_url(), HandleServiceLoginAuth); + + // Handles /SSO GAIA call (not GAIA, made up for SAML tests). + REGISTER_PATH_RESPONSE_HANDLER("/SSO", HandleSSO); + + // Handles /o/oauth2/token GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->oauth2_token_url(), HandleAuthToken); + + // Handles /oauth2/v2/tokeninfo GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->oauth2_token_info_url(), HandleTokenInfo); + + // Handles /oauth2/v2/IssueToken GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->oauth2_issue_token_url(), HandleIssueToken); + + // Handles /ListAccounts GAIA call. + REGISTER_RESPONSE_HANDLER( + gaia_urls->list_accounts_url(), HandleListAccounts); +} + +scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) { // The scheme and host of the URL is actually not important but required to // get a valid GURL in order to parse |request.relative_url|. GURL request_url = GURL("http://localhost").Resolve(request.relative_url); std::string request_path = request_url.path(); - scoped_ptr<BasicHttpResponse> http_response(new BasicHttpResponse()); - if (request_path == gaia_urls->service_login_url().path()) { - http_response->set_code(net::HTTP_OK); - http_response->set_content(service_login_response_); - http_response->set_content_type("text/html"); - } else if (request_path == gaia_urls->service_login_auth_url().path()) { - std::string continue_url = gaia_urls->service_login_url().spec(); - GetQueryParameter(request.content, "continue", &continue_url); - http_response->set_code(net::HTTP_OK); - const std::string redirect_js = - "document.location.href = '" + continue_url + "'"; - http_response->set_content( - "<HTML><HEAD><SCRIPT>\n" + redirect_js + "\n</SCRIPT></HEAD></HTML>"); - http_response->set_content_type("text/html"); - } else if (request_path == gaia_urls->oauth2_token_url().path()) { - std::string refresh_token; - std::string client_id; - std::string scope; - const AccessTokenInfo* token_info = NULL; - GetQueryParameter(request.content, "scope", &scope); - if (GetQueryParameter(request.content, "refresh_token", &refresh_token) && - GetQueryParameter(request.content, "client_id", &client_id) && - (token_info = GetAccessTokenInfo(refresh_token, client_id, scope))) { - base::DictionaryValue response_dict; - response_dict.SetString("access_token", token_info->token); - response_dict.SetInteger("expires_in", 3600); - FormatJSONResponse(response_dict, http_response.get()); - } else { - http_response->set_code(net::HTTP_BAD_REQUEST); - } - } else if (request_path == gaia_urls->oauth2_token_info_url().path()) { - const AccessTokenInfo* token_info = NULL; - std::string access_token; - if (GetQueryParameter(request.content, "access_token", &access_token)) { - for (AccessTokenInfoMap::const_iterator entry( - access_token_info_map_.begin()); - entry != access_token_info_map_.end(); - ++entry) { - if (entry->second.token == access_token) { - token_info = &(entry->second); - break; - } - } - } - - if (token_info) { - base::DictionaryValue response_dict; - response_dict.SetString("issued_to", token_info->issued_to); - response_dict.SetString("audience", token_info->audience); - response_dict.SetString("user_id", token_info->user_id); - std::vector<std::string> scope_vector(token_info->scopes.begin(), - token_info->scopes.end()); - response_dict.SetString("scope", JoinString(scope_vector, " ")); - response_dict.SetInteger("expires_in", token_info->expires_in); - response_dict.SetString("email", token_info->email); - FormatJSONResponse(response_dict, http_response.get()); - } else { - http_response->set_code(net::HTTP_BAD_REQUEST); - } - } else if (request_path == gaia_urls->oauth2_issue_token_url().path()) { - std::string access_token; - std::map<std::string, std::string>::const_iterator auth_header_entry = - request.headers.find("Authorization"); - if (auth_header_entry != request.headers.end()) { - if (StartsWithASCII(auth_header_entry->second, "Bearer ", true)) - access_token = auth_header_entry->second.substr(7); - } - - std::string scope; - std::string client_id; - const AccessTokenInfo* token_info = NULL; - if (GetQueryParameter(request.content, "scope", &scope) && - GetQueryParameter(request.content, "client_id", &client_id) && - (token_info = GetAccessTokenInfo(access_token, client_id, scope))) { - base::DictionaryValue response_dict; - response_dict.SetString("issueAdvice", "auto"); - response_dict.SetString("expiresIn", - base::IntToString(token_info->expires_in)); - response_dict.SetString("token", token_info->token); - FormatJSONResponse(response_dict, http_response.get()); - } else { - http_response->set_code(net::HTTP_BAD_REQUEST); - } + RequestHandlerMap::iterator iter = request_handlers_.find(request_path); + if (iter != request_handlers_.end()) { + LOG(WARNING) << "Serving request " << request_path; + iter->second.Run(request, http_response.get()); } else { - // Request not understood. - return scoped_ptr<HttpResponse>(); + LOG(ERROR) << "Unhandled request " << request_path; + return scoped_ptr<HttpResponse>(); // Request not understood. } return http_response.PassAs<HttpResponse>(); @@ -147,6 +201,102 @@ void FakeGaia::IssueOAuthToken(const std::string& auth_token, access_token_info_map_.insert(std::make_pair(auth_token, token_info)); } +void FakeGaia::RegisterSamlUser(const std::string& account_id, + const GURL& saml_idp) { + saml_account_idp_map_[account_id] = saml_idp; +} + +// static +bool FakeGaia::GetQueryParameter(const std::string& query, + const std::string& key, + std::string* value) { + // Name and scheme actually don't matter, but are required to get a valid URL + // for parsing. + GURL query_url("http://localhost?" + query); + return net::GetValueForKeyInQuery(query_url, key, value); +} + +void FakeGaia::HandleMergeSession(const HttpRequest& request, + BasicHttpResponse* http_response) { + http_response->set_code(net::HTTP_UNAUTHORIZED); + if (merge_session_params_.session_sid_cookie.empty() || + merge_session_params_.session_lsid_cookie.empty()) { + http_response->set_code(net::HTTP_BAD_REQUEST); + return; + } + + std::string uber_token; + if (!GetQueryParameter(request.content, "uberauth", &uber_token) || + uber_token != merge_session_params_.gaia_uber_token) { + LOG(ERROR) << "Missing or invalid 'uberauth' param in /MergeSession call"; + return; + } + + std::string continue_url; + if (!GetQueryParameter(request.content, "continue", &continue_url)) { + LOG(ERROR) << "Missing or invalid 'continue' param in /MergeSession call"; + return; + } + + std::string source; + if (!GetQueryParameter(request.content, "source", &source)) { + LOG(ERROR) << "Missing or invalid 'source' param in /MergeSession call"; + return; + } + + SetCookies(http_response, + merge_session_params_.session_sid_cookie, + merge_session_params_.session_lsid_cookie); + // TODO(zelidrag): Not used now. + http_response->set_content("OK"); + http_response->set_code(net::HTTP_OK); +} + +void FakeGaia::HandleProgramaticAuth( + const HttpRequest& request, + BasicHttpResponse* http_response) { + http_response->set_code(net::HTTP_UNAUTHORIZED); + if (merge_session_params_.auth_code.empty()) { + http_response->set_code(net::HTTP_BAD_REQUEST); + return; + } + + GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); + std::string scope; + if (!GetQueryParameter(request.content, "scope", &scope) || + GaiaConstants::kOAuth1LoginScope != scope) { + return; + } + + CookieMap cookies = GetRequestCookies(request); + CookieMap::const_iterator sid_iter = cookies.find("SID"); + if (sid_iter == cookies.end() || + sid_iter->second != merge_session_params_.auth_sid_cookie) { + LOG(ERROR) << "/o/oauth2/programmatic_auth missing SID cookie"; + return; + } + CookieMap::const_iterator lsid_iter = cookies.find("LSID"); + if (lsid_iter == cookies.end() || + lsid_iter->second != merge_session_params_.auth_lsid_cookie) { + LOG(ERROR) << "/o/oauth2/programmatic_auth missing LSID cookie"; + return; + } + + std::string client_id; + if (!GetQueryParameter(request.content, "client_id", &client_id) || + gaia_urls->oauth2_chrome_client_id() != client_id) { + return; + } + + http_response->AddCustomHeader( + "Set-Cookie", + base::StringPrintf( + "oauth_code=%s; Path=/o/GetOAuth2Token; Secure; HttpOnly;", + merge_session_params_.auth_code.c_str())); + http_response->set_code(net::HTTP_OK); + http_response->set_content_type("text/html"); +} + void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict, BasicHttpResponse* http_response) { std::string response_json; @@ -155,7 +305,7 @@ void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict, http_response->set_code(net::HTTP_OK); } -const FakeGaia::AccessTokenInfo* FakeGaia::GetAccessTokenInfo( +const FakeGaia::AccessTokenInfo* FakeGaia::FindAccessTokenInfo( const std::string& auth_token, const std::string& client_id, const std::string& scope_string) const { @@ -179,12 +329,203 @@ const FakeGaia::AccessTokenInfo* FakeGaia::GetAccessTokenInfo( return NULL; } -// static -bool FakeGaia::GetQueryParameter(const std::string& query, - const std::string& key, - std::string* value) { - // Name and scheme actually don't matter, but are required to get a valid URL - // for parsing. - GURL query_url("http://localhost?" + query); - return net::GetValueForKeyInQuery(query_url, key, value); +void FakeGaia::HandleServiceLogin(const HttpRequest& request, + BasicHttpResponse* http_response) { + http_response->set_code(net::HTTP_OK); + http_response->set_content(service_login_response_); + http_response->set_content_type("text/html"); +} + +void FakeGaia::HandleOAuthLogin(const HttpRequest& request, + BasicHttpResponse* http_response) { + http_response->set_code(net::HTTP_UNAUTHORIZED); + if (merge_session_params_.gaia_uber_token.empty()) { + http_response->set_code(net::HTTP_FORBIDDEN); + return; + } + + std::string access_token; + if (!GetAccessToken(request, kAuthHeaderOAuth, &access_token)) { + LOG(ERROR) << "/OAuthLogin missing access token in the header"; + return; + } + + GURL request_url = GURL("http://localhost").Resolve(request.relative_url); + std::string request_query = request_url.query(); + + std::string source; + if (!GetQueryParameter(request_query, "source", &source)) { + LOG(ERROR) << "Missing 'source' param in /OAuthLogin call"; + return; + } + + std::string issue_uberauth; + if (GetQueryParameter(request_query, "issueuberauth", &issue_uberauth) && + issue_uberauth == "1") { + http_response->set_content(merge_session_params_.gaia_uber_token); + http_response->set_code(net::HTTP_OK); + // Issue GAIA uber token. + } else { + LOG(FATAL) << "/OAuthLogin for SID/LSID is not supported"; + } +} + +void FakeGaia::HandleServiceLoginAuth(const HttpRequest& request, + BasicHttpResponse* http_response) { + std::string continue_url = + GaiaUrls::GetInstance()->service_login_url().spec(); + GetQueryParameter(request.content, "continue", &continue_url); + + std::string redirect_url = continue_url; + + std::string email; + if (GetQueryParameter(request.content, "Email", &email) && + saml_account_idp_map_.find(email) != saml_account_idp_map_.end()) { + GURL url(saml_account_idp_map_[email]); + url = net::AppendQueryParameter(url, "SAMLRequest", "fake_request"); + url = net::AppendQueryParameter(url, "RelayState", continue_url); + redirect_url = url.spec(); + } else if (!merge_session_params_.auth_sid_cookie.empty() && + !merge_session_params_.auth_lsid_cookie.empty()) { + SetCookies(http_response, + merge_session_params_.auth_sid_cookie, + merge_session_params_.auth_lsid_cookie); + } + + http_response->set_code(net::HTTP_TEMPORARY_REDIRECT); + http_response->AddCustomHeader("Location", redirect_url); +} + +void FakeGaia::HandleSSO(const HttpRequest& request, + BasicHttpResponse* http_response) { + if (!merge_session_params_.auth_sid_cookie.empty() && + !merge_session_params_.auth_lsid_cookie.empty()) { + SetCookies(http_response, + merge_session_params_.auth_sid_cookie, + merge_session_params_.auth_lsid_cookie); + } + std::string relay_state; + GetQueryParameter(request.content, "RelayState", &relay_state); + std::string redirect_url = relay_state; + http_response->set_code(net::HTTP_TEMPORARY_REDIRECT); + http_response->AddCustomHeader("Location", redirect_url); +} + +void FakeGaia::HandleAuthToken(const HttpRequest& request, + BasicHttpResponse* http_response) { + std::string grant_type; + std::string refresh_token; + std::string client_id; + std::string scope; + std::string auth_code; + const AccessTokenInfo* token_info = NULL; + GetQueryParameter(request.content, "scope", &scope); + + if (!GetQueryParameter(request.content, "grant_type", &grant_type)) { + http_response->set_code(net::HTTP_BAD_REQUEST); + LOG(ERROR) << "No 'grant_type' param in /o/oauth2/token"; + return; + } + + if (grant_type == "authorization_code") { + if (!GetQueryParameter(request.content, "code", &auth_code) || + auth_code != merge_session_params_.auth_code) { + http_response->set_code(net::HTTP_BAD_REQUEST); + LOG(ERROR) << "No 'code' param in /o/oauth2/token"; + return; + } + + if (GaiaConstants::kOAuth1LoginScope != scope) { + http_response->set_code(net::HTTP_BAD_REQUEST); + LOG(ERROR) << "Invalid scope for /o/oauth2/token - " << scope; + return; + } + + base::DictionaryValue response_dict; + response_dict.SetString("refresh_token", + merge_session_params_.refresh_token); + response_dict.SetString("access_token", + merge_session_params_.access_token); + response_dict.SetInteger("expires_in", 3600); + FormatJSONResponse(response_dict, http_response); + } else if (GetQueryParameter(request.content, + "refresh_token", + &refresh_token) && + GetQueryParameter(request.content, + "client_id", + &client_id) && + (token_info = FindAccessTokenInfo(refresh_token, + client_id, + scope))) { + base::DictionaryValue response_dict; + response_dict.SetString("access_token", token_info->token); + response_dict.SetInteger("expires_in", 3600); + FormatJSONResponse(response_dict, http_response); + } else { + LOG(ERROR) << "Bad request for /o/oauth2/token - " + << "refresh_token = " << refresh_token + << ", scope = " << scope + << ", client_id = " << client_id; + http_response->set_code(net::HTTP_BAD_REQUEST); + } +} + +void FakeGaia::HandleTokenInfo(const HttpRequest& request, + BasicHttpResponse* http_response) { + const AccessTokenInfo* token_info = NULL; + std::string access_token; + if (GetQueryParameter(request.content, "access_token", &access_token)) { + for (AccessTokenInfoMap::const_iterator entry( + access_token_info_map_.begin()); + entry != access_token_info_map_.end(); + ++entry) { + if (entry->second.token == access_token) { + token_info = &(entry->second); + break; + } + } + } + + if (token_info) { + base::DictionaryValue response_dict; + response_dict.SetString("issued_to", token_info->issued_to); + response_dict.SetString("audience", token_info->audience); + response_dict.SetString("user_id", token_info->user_id); + std::vector<std::string> scope_vector(token_info->scopes.begin(), + token_info->scopes.end()); + response_dict.SetString("scope", JoinString(scope_vector, " ")); + response_dict.SetInteger("expires_in", token_info->expires_in); + response_dict.SetString("email", token_info->email); + FormatJSONResponse(response_dict, http_response); + } else { + http_response->set_code(net::HTTP_BAD_REQUEST); + } +} + +void FakeGaia::HandleIssueToken(const HttpRequest& request, + BasicHttpResponse* http_response) { + std::string access_token; + std::string scope; + std::string client_id; + const AccessTokenInfo* token_info = NULL; + if (GetAccessToken(request, kAuthHeaderBearer, &access_token) && + GetQueryParameter(request.content, "scope", &scope) && + GetQueryParameter(request.content, "client_id", &client_id) && + (token_info = FindAccessTokenInfo(access_token, client_id, scope))) { + base::DictionaryValue response_dict; + response_dict.SetString("issueAdvice", "auto"); + response_dict.SetString("expiresIn", + base::IntToString(token_info->expires_in)); + response_dict.SetString("token", token_info->token); + FormatJSONResponse(response_dict, http_response); + } else { + http_response->set_code(net::HTTP_BAD_REQUEST); + } +} + +void FakeGaia::HandleListAccounts(const HttpRequest& request, + BasicHttpResponse* http_response) { + http_response->set_content(base::StringPrintf( + kListAccountsResponseFormat, merge_session_params_.email.c_str())); + http_response->set_code(net::HTTP_OK); } diff --git a/chromium/google_apis/gaia/fake_gaia.h b/chromium/google_apis/gaia/fake_gaia.h index de4201f7337..13a63e6b228 100644 --- a/chromium/google_apis/gaia/fake_gaia.h +++ b/chromium/google_apis/gaia/fake_gaia.h @@ -10,7 +10,9 @@ #include <string> #include "base/basictypes.h" +#include "base/callback.h" #include "base/memory/scoped_ptr.h" +#include "url/gurl.h" namespace base { class DictionaryValue; @@ -45,8 +47,44 @@ class FakeGaia { std::string email; }; + // Cookies and tokens for /MergeSession call seqeunce. + struct MergeSessionParams { + MergeSessionParams(); + ~MergeSessionParams(); + + // Values of SID and LSID cookie that are set by /ServiceLoginAuth or its + // equivalent at the end of the SAML login flow. + std::string auth_sid_cookie; + std::string auth_lsid_cookie; + + // auth_code cookie value response for /o/oauth2/programmatic_auth call. + std::string auth_code; + + // OAuth2 refresh and access token generated by /o/oauth2/token call + // with "...&grant_type=authorization_code". + std::string refresh_token; + std::string access_token; + + // Uber token response from /OAuthLogin call. + std::string gaia_uber_token; + + // Values of SID and LSID cookie generated from /MergeSession call. + std::string session_sid_cookie; + std::string session_lsid_cookie; + + // The e-mail address returned by /ListAccounts. + std::string email; + }; + FakeGaia(); - ~FakeGaia(); + virtual ~FakeGaia(); + + // Sets the initial value of tokens and cookies. + void SetMergeSessionParams(const MergeSessionParams& params); + + // Initializes HTTP request handlers. Should be called after switches + // for tweaking GaiaUrls are in place. + void Initialize(); // Handles a request and returns a response if the request was recognized as a // GAIA request. Note that this respects the switches::kGaiaUrl and friends so @@ -61,30 +99,74 @@ class FakeGaia { void IssueOAuthToken(const std::string& auth_token, const AccessTokenInfo& token_info); + // Associates an account id with a SAML IdP redirect endpoint. When a + // /ServiceLoginAuth request comes in for that user, it will be redirected + // to the associated redirect endpoint. + void RegisterSamlUser(const std::string& account_id, const GURL& saml_idp); + + // Extracts the parameter named |key| from |query| and places it in |value|. + // Returns false if no parameter is found. + static bool GetQueryParameter(const std::string& query, + const std::string& key, + std::string* value); + protected: + // HTTP handler for /MergeSession. + virtual void HandleMergeSession( + const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + private: typedef std::multimap<std::string, AccessTokenInfo> AccessTokenInfoMap; + typedef std::map<std::string, GURL> SamlAccountIdpMap; // Formats a JSON response with the data in |response_dict|. void FormatJSONResponse(const base::DictionaryValue& response_dict, net::test_server::BasicHttpResponse* http_response); + typedef base::Callback<void( + const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response)> + HttpRequestHandlerCallback; + typedef std::map<std::string, HttpRequestHandlerCallback> RequestHandlerMap; + + // HTTP request handlers. + void HandleProgramaticAuth( + const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleServiceLogin(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleOAuthLogin(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleServiceLoginAuth( + const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleSSO(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleAuthToken(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleTokenInfo(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleIssueToken(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandleListAccounts(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + void HandlePeopleGet(const net::test_server::HttpRequest& request, + net::test_server::BasicHttpResponse* http_response); + // Returns the access token associated with |auth_token| that matches the // given |client_id| and |scope_string|. If |scope_string| is empty, the first // token satisfying the other criteria is returned. Returns NULL if no token // matches. - const AccessTokenInfo* GetAccessTokenInfo(const std::string& auth_token, - const std::string& client_id, - const std::string& scope_string) + const AccessTokenInfo* FindAccessTokenInfo(const std::string& auth_token, + const std::string& client_id, + const std::string& scope_string) const; - // Extracts the parameter named |key| from |query| and places it in |value|. - // Returns false if no parameter is found. - static bool GetQueryParameter(const std::string& query, - const std::string& key, - std::string* value); - + MergeSessionParams merge_session_params_; AccessTokenInfoMap access_token_info_map_; + RequestHandlerMap request_handlers_; std::string service_login_response_; + SamlAccountIdpMap saml_account_idp_map_; DISALLOW_COPY_AND_ASSIGN(FakeGaia); }; diff --git a/chromium/google_apis/gaia/fake_identity_provider.cc b/chromium/google_apis/gaia/fake_identity_provider.cc new file mode 100644 index 00000000000..7360e41bc1d --- /dev/null +++ b/chromium/google_apis/gaia/fake_identity_provider.cc @@ -0,0 +1,40 @@ +// 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/gaia/fake_identity_provider.h" + +#include "google_apis/gaia/oauth2_token_service.h" + +FakeIdentityProvider::FakeIdentityProvider(OAuth2TokenService* token_service) + : token_service_(token_service) { +} + +FakeIdentityProvider::~FakeIdentityProvider() { +} + +void FakeIdentityProvider::LogIn(const std::string& account_id) { + account_id_ = account_id; + FireOnActiveAccountLogin(); +} + +void FakeIdentityProvider::LogOut() { + account_id_.clear(); + FireOnActiveAccountLogout(); +} + +std::string FakeIdentityProvider::GetActiveUsername() { + return account_id_; +} + +std::string FakeIdentityProvider::GetActiveAccountId() { + return account_id_; +} + +OAuth2TokenService* FakeIdentityProvider::GetTokenService() { + return token_service_; +} + +bool FakeIdentityProvider::RequestLogin() { + return false; +} diff --git a/chromium/google_apis/gaia/fake_identity_provider.h b/chromium/google_apis/gaia/fake_identity_provider.h new file mode 100644 index 00000000000..28d46d940a5 --- /dev/null +++ b/chromium/google_apis/gaia/fake_identity_provider.h @@ -0,0 +1,38 @@ +// 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_GAIA_FAKE_IDENTITY_PROVIDER_H_ +#define GOOGLE_APIS_GAIA_FAKE_IDENTITY_PROVIDER_H_ + +#include <string> + +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "google_apis/gaia/identity_provider.h" + +class OAuth2TokenService; + +// Fake identity provider implementation. +class FakeIdentityProvider : public IdentityProvider { + public: + explicit FakeIdentityProvider(OAuth2TokenService* token_service); + virtual ~FakeIdentityProvider(); + + void LogIn(const std::string& account_id); + void LogOut(); + + // IdentityProvider: + virtual std::string GetActiveUsername() OVERRIDE; + virtual std::string GetActiveAccountId() OVERRIDE; + virtual OAuth2TokenService* GetTokenService() OVERRIDE; + virtual bool RequestLogin() OVERRIDE; + + private: + std::string account_id_; + OAuth2TokenService* token_service_; + + DISALLOW_COPY_AND_ASSIGN(FakeIdentityProvider); +}; + +#endif // GOOGLE_APIS_GAIA_FAKE_IDENTITY_PROVIDER_H_ diff --git a/chromium/google_apis/gaia/fake_oauth2_token_service.cc b/chromium/google_apis/gaia/fake_oauth2_token_service.cc new file mode 100644 index 00000000000..0b01404bd87 --- /dev/null +++ b/chromium/google_apis/gaia/fake_oauth2_token_service.cc @@ -0,0 +1,89 @@ +// 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/gaia/fake_oauth2_token_service.h" + +FakeOAuth2TokenService::PendingRequest::PendingRequest() { +} + +FakeOAuth2TokenService::PendingRequest::~PendingRequest() { +} + +FakeOAuth2TokenService::FakeOAuth2TokenService() : request_context_(NULL) { +} + +FakeOAuth2TokenService::~FakeOAuth2TokenService() { +} + +std::vector<std::string> FakeOAuth2TokenService::GetAccounts() { + return std::vector<std::string>(account_ids_.begin(), account_ids_.end()); +} + +void FakeOAuth2TokenService::FetchOAuth2Token( + RequestImpl* request, + const std::string& account_id, + net::URLRequestContextGetter* getter, + const std::string& client_id, + const std::string& client_secret, + const ScopeSet& scopes) { + PendingRequest pending_request; + pending_request.account_id = account_id; + pending_request.client_id = client_id; + pending_request.client_secret = client_secret; + pending_request.scopes = scopes; + pending_request.request = request->AsWeakPtr(); + pending_requests_.push_back(pending_request); +} + +void FakeOAuth2TokenService::InvalidateOAuth2Token( + const std::string& account_id, + const std::string& client_id, + const ScopeSet& scopes, + const std::string& access_token) { +} + +net::URLRequestContextGetter* FakeOAuth2TokenService::GetRequestContext() { + return request_context_; +} + +bool FakeOAuth2TokenService::RefreshTokenIsAvailable( + const std::string& account_id) const { + return account_ids_.count(account_id) != 0; +}; + +void FakeOAuth2TokenService::AddAccount(const std::string& account_id) { + account_ids_.insert(account_id); + FireRefreshTokenAvailable(account_id); +} + +void FakeOAuth2TokenService::RemoveAccount(const std::string& account_id) { + account_ids_.erase(account_id); + FireRefreshTokenRevoked(account_id); +} + +void FakeOAuth2TokenService::IssueAllTokensForAccount( + const std::string& account_id, + const std::string& access_token, + const base::Time& expiration) { + + // Walk the requests and notify the callbacks. + for (std::vector<PendingRequest>::iterator it = pending_requests_.begin(); + it != pending_requests_.end(); ++it) { + if (it->request && (account_id == it->account_id)) { + it->request->InformConsumer( + GoogleServiceAuthError::AuthErrorNone(), access_token, expiration); + } + } +} + + +OAuth2AccessTokenFetcher* FakeOAuth2TokenService::CreateAccessTokenFetcher( + const std::string& account_id, + net::URLRequestContextGetter* getter, + OAuth2AccessTokenConsumer* consumer) { + // |FakeOAuth2TokenService| overrides |FetchOAuth2Token| and thus + // |CreateAccessTokenFetcher| should never be called. + NOTREACHED(); + return NULL; +} diff --git a/chromium/google_apis/gaia/fake_oauth2_token_service.h b/chromium/google_apis/gaia/fake_oauth2_token_service.h new file mode 100644 index 00000000000..3bdb67500a9 --- /dev/null +++ b/chromium/google_apis/gaia/fake_oauth2_token_service.h @@ -0,0 +1,84 @@ +// 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_GAIA_FAKE_OAUTH2_TOKEN_SERVICE_H_ +#define GOOGLE_APIS_GAIA_FAKE_OAUTH2_TOKEN_SERVICE_H_ + +#include <set> +#include <string> + +#include "base/compiler_specific.h" +#include "base/memory/weak_ptr.h" +#include "google_apis/gaia/oauth2_token_service.h" + +namespace net { +class URLRequestContextGetter; +} + +// Do-nothing implementation of OAuth2TokenService. +class FakeOAuth2TokenService : public OAuth2TokenService { + public: + FakeOAuth2TokenService(); + virtual ~FakeOAuth2TokenService(); + + virtual std::vector<std::string> GetAccounts() OVERRIDE; + + void AddAccount(const std::string& account_id); + void RemoveAccount(const std::string& account_id); + + // Helper routines to issue tokens for pending requests. + void IssueAllTokensForAccount(const std::string& account_id, + const std::string& access_token, + const base::Time& expiration); + + void set_request_context(net::URLRequestContextGetter* request_context) { + request_context_ = request_context; + } + + protected: + // OAuth2TokenService overrides. + virtual void FetchOAuth2Token(RequestImpl* request, + const std::string& account_id, + net::URLRequestContextGetter* getter, + const std::string& client_id, + const std::string& client_secret, + const ScopeSet& scopes) OVERRIDE; + + virtual void InvalidateOAuth2Token(const std::string& account_id, + const std::string& client_id, + const ScopeSet& scopes, + const std::string& access_token) OVERRIDE; + + virtual bool RefreshTokenIsAvailable(const std::string& account_id) const + OVERRIDE; + + private: + struct PendingRequest { + PendingRequest(); + ~PendingRequest(); + + std::string account_id; + std::string client_id; + std::string client_secret; + ScopeSet scopes; + base::WeakPtr<RequestImpl> request; + }; + + // OAuth2TokenService overrides. + virtual net::URLRequestContextGetter* GetRequestContext() OVERRIDE; + + virtual OAuth2AccessTokenFetcher* CreateAccessTokenFetcher( + const std::string& account_id, + net::URLRequestContextGetter* getter, + OAuth2AccessTokenConsumer* consumer) OVERRIDE; + + std::set<std::string> account_ids_; + std::vector<PendingRequest> pending_requests_; + + net::URLRequestContextGetter* request_context_; // weak + + DISALLOW_COPY_AND_ASSIGN(FakeOAuth2TokenService); +}; + +#endif // GOOGLE_APIS_GAIA_FAKE_OAUTH2_TOKEN_SERVICE_H_ diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.cc b/chromium/google_apis/gaia/gaia_auth_fetcher.cc index c5254f4235e..ca4cdc0588a 100644 --- a/chromium/google_apis/gaia/gaia_auth_fetcher.cc +++ b/chromium/google_apis/gaia/gaia_auth_fetcher.cc @@ -4,7 +4,6 @@ #include "google_apis/gaia/gaia_auth_fetcher.h" -#include <algorithm> #include <string> #include <utility> #include <vector> @@ -33,7 +32,12 @@ const int kLoadFlagsIgnoreCookies = net::LOAD_DO_NOT_SEND_COOKIES | static bool CookiePartsContains(const std::vector<std::string>& parts, const char* part) { - return std::find(parts.begin(), parts.end(), part) != parts.end(); + for (std::vector<std::string>::const_iterator it = parts.begin(); + it != parts.end(); ++it) { + if (LowerCaseEqualsASCII(*it, part)) + return true; + } + return false; } bool ExtractOAuth2TokenPairResponse(base::DictionaryValue* dict, @@ -84,6 +88,9 @@ const char GaiaAuthFetcher::kIssueAuthTokenFormat[] = const char GaiaAuthFetcher::kClientLoginToOAuth2BodyFormat[] = "scope=%s&client_id=%s"; // static +const char GaiaAuthFetcher::kClientLoginToOAuth2WithDeviceTypeBodyFormat[] = + "scope=%s&client_id=%s&device_type=chrome"; +// static const char GaiaAuthFetcher::kOAuth2CodeToTokenPairBodyFormat[] = "scope=%s&" "grant_type=authorization_code&" @@ -110,21 +117,15 @@ const char GaiaAuthFetcher::kOAuthLoginFormat[] = "service=%s&source=%s"; // static const char GaiaAuthFetcher::kAccountDeletedError[] = "AccountDeleted"; -const char GaiaAuthFetcher::kAccountDeletedErrorCode[] = "adel"; // static const char GaiaAuthFetcher::kAccountDisabledError[] = "AccountDisabled"; -const char GaiaAuthFetcher::kAccountDisabledErrorCode[] = "adis"; // static const char GaiaAuthFetcher::kBadAuthenticationError[] = "BadAuthentication"; -const char GaiaAuthFetcher::kBadAuthenticationErrorCode[] = "badauth"; // static const char GaiaAuthFetcher::kCaptchaError[] = "CaptchaRequired"; -const char GaiaAuthFetcher::kCaptchaErrorCode[] = "cr"; // static const char GaiaAuthFetcher::kServiceUnavailableError[] = "ServiceUnavailable"; -const char GaiaAuthFetcher::kServiceUnavailableErrorCode[] = - "ire"; // static const char GaiaAuthFetcher::kErrorParam[] = "Error"; // static @@ -156,10 +157,12 @@ const char GaiaAuthFetcher::kOAuthHeaderFormat[] = "Authorization: OAuth %s"; const char GaiaAuthFetcher::kOAuth2BearerHeaderFormat[] = "Authorization: Bearer %s"; // static -const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartSecure[] = "Secure"; +const char GaiaAuthFetcher::kDeviceIdHeaderFormat[] = "X-Device-ID: %s"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartSecure[] = "secure"; // static const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartHttpOnly[] = - "HttpOnly"; + "httponly"; // static const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartCodePrefix[] = "oauth_code="; @@ -298,21 +301,27 @@ std::string GaiaAuthFetcher::MakeIssueAuthTokenBody( } // static -std::string GaiaAuthFetcher::MakeGetAuthCodeBody() { +std::string GaiaAuthFetcher::MakeGetAuthCodeBody(bool include_device_type) { std::string encoded_scope = net::EscapeUrlEncodedData( - GaiaUrls::GetInstance()->oauth1_login_scope(), true); + GaiaConstants::kOAuth1LoginScope, true); std::string encoded_client_id = net::EscapeUrlEncodedData( GaiaUrls::GetInstance()->oauth2_chrome_client_id(), true); - return base::StringPrintf(kClientLoginToOAuth2BodyFormat, - encoded_scope.c_str(), - encoded_client_id.c_str()); + if (include_device_type) { + return base::StringPrintf(kClientLoginToOAuth2WithDeviceTypeBodyFormat, + encoded_scope.c_str(), + encoded_client_id.c_str()); + } else { + return base::StringPrintf(kClientLoginToOAuth2BodyFormat, + encoded_scope.c_str(), + encoded_client_id.c_str()); + } } // static std::string GaiaAuthFetcher::MakeGetTokenPairBody( const std::string& auth_code) { std::string encoded_scope = net::EscapeUrlEncodedData( - GaiaUrls::GetInstance()->oauth1_login_scope(), true); + GaiaConstants::kOAuth1LoginScope, true); std::string encoded_client_id = net::EscapeUrlEncodedData( GaiaUrls::GetInstance()->oauth2_chrome_client_id(), true); std::string encoded_client_secret = net::EscapeUrlEncodedData( @@ -516,7 +525,7 @@ void GaiaAuthFetcher::StartLsoForOAuthLoginTokenExchange( DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; DVLOG(1) << "Starting OAuth login token exchange with auth_token"; - request_body_ = MakeGetAuthCodeBody(); + request_body_ = MakeGetAuthCodeBody(false); client_login_to_oauth2_gurl_ = GaiaUrls::GetInstance()->client_login_to_oauth2_url(); @@ -547,10 +556,17 @@ void GaiaAuthFetcher::StartRevokeOAuth2Token(const std::string& auth_token) { void GaiaAuthFetcher::StartCookieForOAuthLoginTokenExchange( const std::string& session_index) { + StartCookieForOAuthLoginTokenExchangeWithDeviceId(session_index, + std::string()); +} + +void GaiaAuthFetcher::StartCookieForOAuthLoginTokenExchangeWithDeviceId( + const std::string& session_index, + const std::string& device_id) { DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; DVLOG(1) << "Starting OAuth login token fetch with cookie jar"; - request_body_ = MakeGetAuthCodeBody(); + request_body_ = MakeGetAuthCodeBody(!device_id.empty()); client_login_to_oauth2_gurl_ = GaiaUrls::GetInstance()->client_login_to_oauth2_url(); @@ -559,9 +575,15 @@ void GaiaAuthFetcher::StartCookieForOAuthLoginTokenExchange( client_login_to_oauth2_gurl_.Resolve("?authuser=" + session_index); } + std::string device_id_header; + if (!device_id.empty()) { + device_id_header = + base::StringPrintf(kDeviceIdHeaderFormat, device_id.c_str()); + } + fetcher_.reset(CreateGaiaFetcher(getter_, request_body_, - std::string(), + device_id_header, client_login_to_oauth2_gurl_, net::LOAD_NORMAL, this)); @@ -680,102 +702,42 @@ GoogleServiceAuthError GaiaAuthFetcher::GenerateAuthError( if (!status.is_success()) { if (status.status() == net::URLRequestStatus::CANCELED) { return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); - } else { - DLOG(WARNING) << "Could not reach Google Accounts servers: errno " - << status.error(); - return GoogleServiceAuthError::FromConnectionError(status.error()); - } - } else { - if (IsSecondFactorSuccess(data)) { - return GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); - } - - std::string error; - std::string url; - std::string captcha_url; - std::string captcha_token; - ParseClientLoginFailure(data, &error, &url, &captcha_url, &captcha_token); - DLOG(WARNING) << "ClientLogin failed with " << error; - - if (error == kCaptchaError) { - GURL image_url( - GaiaUrls::GetInstance()->captcha_base_url().Resolve(captcha_url)); - GURL unlock_url(url); - return GoogleServiceAuthError::FromClientLoginCaptchaChallenge( - captcha_token, image_url, unlock_url); - } - if (error == kAccountDeletedError) - return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DELETED); - if (error == kAccountDisabledError) - return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DISABLED); - if (error == kBadAuthenticationError) { - return GoogleServiceAuthError( - GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); - } - if (error == kServiceUnavailableError) { - return GoogleServiceAuthError( - GoogleServiceAuthError::SERVICE_UNAVAILABLE); } + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } - DLOG(WARNING) << "Incomprehensible response from Google Accounts servers."; + if (IsSecondFactorSuccess(data)) + return GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); + + std::string error; + std::string url; + std::string captcha_url; + std::string captcha_token; + ParseClientLoginFailure(data, &error, &url, &captcha_url, &captcha_token); + DLOG(WARNING) << "ClientLogin failed with " << error; + + if (error == kCaptchaError) { + return GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + captcha_token, + GURL(GaiaUrls::GetInstance()->captcha_base_url().Resolve(captcha_url)), + GURL(url)); + } + if (error == kAccountDeletedError) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DELETED); + if (error == kAccountDisabledError) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DISABLED); + if (error == kBadAuthenticationError) { return GoogleServiceAuthError( - GoogleServiceAuthError::SERVICE_UNAVAILABLE); + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); } - - NOTREACHED(); - return GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE); -} - -// static -GoogleServiceAuthError GaiaAuthFetcher::GenerateOAuthLoginError( - const std::string& data, - const net::URLRequestStatus& status) { - if (!status.is_success()) { - if (status.status() == net::URLRequestStatus::CANCELED) { - return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); - } else { - DLOG(WARNING) << "Could not reach Google Accounts servers: errno " - << status.error(); - return GoogleServiceAuthError::FromConnectionError(status.error()); - } - } else { - if (IsSecondFactorSuccess(data)) { - return GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); - } - - std::string error; - std::string url; - std::string captcha_url; - std::string captcha_token; - ParseClientLoginFailure(data, &error, &url, &captcha_url, &captcha_token); - LOG(WARNING) << "OAuthLogin failed with " << error; - - if (error == kCaptchaErrorCode) { - GURL image_url( - GaiaUrls::GetInstance()->captcha_base_url().Resolve(captcha_url)); - GURL unlock_url(url); - return GoogleServiceAuthError::FromClientLoginCaptchaChallenge( - captcha_token, image_url, unlock_url); - } - if (error == kAccountDeletedErrorCode) - return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DELETED); - if (error == kAccountDisabledErrorCode) - return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DISABLED); - if (error == kBadAuthenticationErrorCode) { - return GoogleServiceAuthError( - GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); - } - if (error == kServiceUnavailableErrorCode) { - return GoogleServiceAuthError( - GoogleServiceAuthError::SERVICE_UNAVAILABLE); - } - - DLOG(WARNING) << "Incomprehensible response from Google Accounts servers."; + if (error == kServiceUnavailableError) { return GoogleServiceAuthError( GoogleServiceAuthError::SERVICE_UNAVAILABLE); } - NOTREACHED(); + DLOG(WARNING) << "Incomprehensible response from Google Accounts servers."; return GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE); } @@ -819,7 +781,8 @@ void GaiaAuthFetcher::OnClientLoginToOAuth2Fetched( ParseClientLoginToOAuth2Response(cookies, &auth_code); StartAuthCodeForOAuth2TokenExchange(auth_code); } else { - consumer_->OnClientOAuthFailure(GenerateAuthError(data, status)); + GoogleServiceAuthError auth_error(GenerateAuthError(data, status)); + consumer_->OnClientOAuthFailure(auth_error); } } diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.h b/chromium/google_apis/gaia/gaia_auth_fetcher.h index 0864f187cfb..aa66a49e032 100644 --- a/chromium/google_apis/gaia/gaia_auth_fetcher.h +++ b/chromium/google_apis/gaia/gaia_auth_fetcher.h @@ -115,6 +115,19 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate { // called on the consumer on the original thread. void StartCookieForOAuthLoginTokenExchange(const std::string& session_index); + // Start a request to exchange the cookies of a signed-in user session + // for an OAuthLogin-scoped oauth2 token. In the case of a session with + // multiple accounts signed in, |session_index| indicate the which of accounts + // within the session. + // Resulting refresh token is annotated on the server with |device_id|. Format + // of device_id on the server is at most 64 unicode characters. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartCookieForOAuthLoginTokenExchangeWithDeviceId( + const std::string& session_index, + const std::string& device_id); + // Start a request to exchange the authorization code for an OAuthLogin-scoped // oauth2 token. // @@ -191,6 +204,9 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate { static const char kIssueAuthTokenFormat[]; // The format of the POST body to get OAuth2 auth code from auth token. static const char kClientLoginToOAuth2BodyFormat[]; + // The format of the POST body to get OAuth2 auth code from auth token. This + // format is used for request annotated with device_id. + static const char kClientLoginToOAuth2WithDeviceTypeBodyFormat[]; // The format of the POST body to get OAuth2 token pair from auth code. static const char kOAuth2CodeToTokenPairBodyFormat[]; // The format of the POST body to revoke an OAuth2 token. @@ -229,6 +245,7 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate { static const char kAuthHeaderFormat[]; static const char kOAuthHeaderFormat[]; static const char kOAuth2BearerHeaderFormat[]; + static const char kDeviceIdHeaderFormat[]; static const char kClientLoginToOAuth2CookiePartSecure[]; static const char kClientLoginToOAuth2CookiePartHttpOnly[]; static const char kClientLoginToOAuth2CookiePartCodePrefix[]; @@ -314,7 +331,7 @@ class GaiaAuthFetcher : public net::URLFetcherDelegate { const std::string& lsid, const char* const service); // Create body to get OAuth2 auth code. - static std::string MakeGetAuthCodeBody(); + static std::string MakeGetAuthCodeBody(bool include_device_type); // Given auth code, create body to get OAuth2 token pair. static std::string MakeGetTokenPairBody(const std::string& auth_code); // Given an OAuth2 token, create body to revoke the token. diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc index a12fb97b220..75a8db73afe 100644 --- a/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc +++ b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc @@ -402,7 +402,7 @@ TEST_F(GaiaAuthFetcherTest, AccountDisabledError) { EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DISABLED); } -TEST_F(GaiaAuthFetcherTest,BadAuthenticationError) { +TEST_F(GaiaAuthFetcherTest, BadAuthenticationError) { net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); std::string data = "Error=BadAuthentication\n"; GoogleServiceAuthError error = @@ -410,7 +410,7 @@ TEST_F(GaiaAuthFetcherTest,BadAuthenticationError) { EXPECT_EQ(error.state(), GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); } -TEST_F(GaiaAuthFetcherTest,IncomprehensibleError) { +TEST_F(GaiaAuthFetcherTest, IncomprehensibleError) { net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); std::string data = "Error=Gobbledygook\n"; GoogleServiceAuthError error = @@ -418,43 +418,11 @@ TEST_F(GaiaAuthFetcherTest,IncomprehensibleError) { EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); } -TEST_F(GaiaAuthFetcherTest,ServiceUnavailableError) { +TEST_F(GaiaAuthFetcherTest, ServiceUnavailableError) { net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); std::string data = "Error=ServiceUnavailable\n"; GoogleServiceAuthError error = - GaiaAuthFetcher::GenerateOAuthLoginError(data, status); - EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); -} - -TEST_F(GaiaAuthFetcherTest, OAuthAccountDeletedError) { - net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); - std::string data = "Error=adel\n"; - GoogleServiceAuthError error = - GaiaAuthFetcher::GenerateOAuthLoginError(data, status); - EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DELETED); -} - -TEST_F(GaiaAuthFetcherTest, OAuthAccountDisabledError) { - net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); - std::string data = "Error=adis\n"; - GoogleServiceAuthError error = - GaiaAuthFetcher::GenerateOAuthLoginError(data, status); - EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DISABLED); -} - -TEST_F(GaiaAuthFetcherTest, OAuthBadAuthenticationError) { - net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); - std::string data = "Error=badauth\n"; - GoogleServiceAuthError error = - GaiaAuthFetcher::GenerateOAuthLoginError(data, status); - EXPECT_EQ(error.state(), GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); -} - -TEST_F(GaiaAuthFetcherTest, OAuthServiceUnavailableError) { - net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); - std::string data = "Error=ire\n"; - GoogleServiceAuthError error = - GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + GaiaAuthFetcher::GenerateAuthError(data, status); EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); } @@ -601,6 +569,25 @@ TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenWithCookies) { net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); EXPECT_TRUE(NULL != fetcher); EXPECT_EQ(net::LOAD_NORMAL, fetcher->GetLoadFlags()); + EXPECT_FALSE(EndsWith(fetcher->upload_data(), "device_type=chrome", true)); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenWithCookies_DeviceId) { + MockGaiaConsumer consumer; + net::TestURLFetcherFactory factory; + std::string expected_device_id("ABCDE-12345"); + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartCookieForOAuthLoginTokenExchangeWithDeviceId("0", + expected_device_id); + net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); + EXPECT_TRUE(NULL != fetcher); + EXPECT_EQ(net::LOAD_NORMAL, fetcher->GetLoadFlags()); + EXPECT_TRUE(EndsWith(fetcher->upload_data(), "device_type=chrome", true)); + net::HttpRequestHeaders extra_request_headers; + fetcher->GetExtraRequestHeaders(&extra_request_headers); + std::string device_id; + EXPECT_TRUE(extra_request_headers.GetHeader("X-Device-ID", &device_id)); + EXPECT_EQ(device_id, expected_device_id); } TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenClientLoginToOAuth2Failure) { diff --git a/chromium/google_apis/gaia/gaia_auth_util.cc b/chromium/google_apis/gaia/gaia_auth_util.cc index f8f95c14f5b..287cd469827 100644 --- a/chromium/google_apis/gaia/gaia_auth_util.cc +++ b/chromium/google_apis/gaia/gaia_auth_util.cc @@ -15,22 +15,41 @@ namespace gaia { namespace { + const char kGmailDomain[] = "gmail.com"; -} +const char kGooglemailDomain[] = "googlemail.com"; -std::string CanonicalizeEmail(const std::string& email_address) { +std::string CanonicalizeEmailImpl(const std::string& email_address, + bool change_googlemail_to_gmail) { std::vector<std::string> parts; char at = '@'; base::SplitString(email_address, at, &parts); - if (parts.size() != 2U) - NOTREACHED() << "expecting exactly one @, but got " << parts.size(); - else if (parts[1] == kGmailDomain) // only strip '.' for gmail accounts. - base::RemoveChars(parts[0], ".", &parts[0]); + if (parts.size() != 2U) { + NOTREACHED() << "expecting exactly one @, but got " << parts.size()-1 << + " : " << email_address; + } else { + if (change_googlemail_to_gmail && parts[1] == kGooglemailDomain) + parts[1] = kGmailDomain; + + if (parts[1] == kGmailDomain) // only strip '.' for gmail accounts. + base::RemoveChars(parts[0], ".", &parts[0]); + } + std::string new_email = StringToLowerASCII(JoinString(parts, at)); VLOG(1) << "Canonicalized " << email_address << " to " << new_email; return new_email; } +} // namespace + +std::string CanonicalizeEmail(const std::string& email_address) { + // CanonicalizeEmail() is called to process email strings that are eventually + // shown to the user, and may also be used in persisting email strings. To + // avoid breaking this existing behavior, this function will not try to + // change googlemail to gmail. + return CanonicalizeEmailImpl(email_address, false); +} + std::string CanonicalizeDomain(const std::string& domain) { // Canonicalization of domain names means lower-casing them. Make sure to // update this function in sync with Canonicalize if this ever changes. @@ -50,8 +69,8 @@ std::string SanitizeEmail(const std::string& email_address) { } bool AreEmailsSame(const std::string& email1, const std::string& email2) { - return gaia::CanonicalizeEmail(gaia::SanitizeEmail(email1)) == - gaia::CanonicalizeEmail(gaia::SanitizeEmail(email2)); + return CanonicalizeEmailImpl(gaia::SanitizeEmail(email1), true) == + CanonicalizeEmailImpl(gaia::SanitizeEmail(email2), true); } std::string ExtractDomainName(const std::string& email_address) { @@ -73,35 +92,48 @@ bool IsGaiaSignonRealm(const GURL& url) { } -std::vector<std::string> ParseListAccountsData(const std::string& data) { - std::vector<std::string> account_ids; +bool ParseListAccountsData( + const std::string& data, + std::vector<std::pair<std::string, bool> >* accounts) { + accounts->clear(); // Parse returned data and make sure we have data. scoped_ptr<base::Value> value(base::JSONReader::Read(data)); if (!value) - return account_ids; + return false; base::ListValue* list; if (!value->GetAsList(&list) || list->GetSize() < 2) - return account_ids; + return false; // Get list of account info. - base::ListValue* accounts; - if (!list->GetList(1, &accounts) || accounts == NULL) - return account_ids; + base::ListValue* account_list; + if (!list->GetList(1, &account_list) || accounts == NULL) + return false; // Build a vector of accounts from the cookie. Order is important: the first // account in the list is the primary account. - for (size_t i = 0; i < accounts->GetSize(); ++i) { + for (size_t i = 0; i < account_list->GetSize(); ++i) { base::ListValue* account; - if (accounts->GetList(i, &account) && account != NULL) { + if (account_list->GetList(i, &account) && account != NULL) { std::string email; - if (account->GetString(3, &email) && !email.empty()) - account_ids.push_back(email); + // Canonicalize the email since ListAccounts returns "display email". + if (account->GetString(3, &email) && !email.empty()) { + // New version if ListAccounts indicates whether the email's session + // is still valid or not. If this value is present and false, assume + // its invalid. Otherwise assume it's valid to remain compatible with + // old version. + int is_email_valid = 1; + if (!account->GetInteger(9, &is_email_valid)) + is_email_valid = 1; + + accounts->push_back( + std::make_pair(CanonicalizeEmail(email), is_email_valid != 0)); + } } } - return account_ids; + return true; } } // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_auth_util.h b/chromium/google_apis/gaia/gaia_auth_util.h index 354d116e7a5..d28f54698bb 100644 --- a/chromium/google_apis/gaia/gaia_auth_util.h +++ b/chromium/google_apis/gaia/gaia_auth_util.h @@ -6,6 +6,7 @@ #define GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ #include <string> +#include <utility> #include <vector> class GURL; @@ -13,10 +14,7 @@ class GURL; namespace gaia { // Perform basic canonicalization of |email_address|, taking into account that -// gmail does not consider '.' or caps inside a username to matter. It also -// ignores everything after a '+'. For example, c.masone+abc@gmail.com == -// cMaSone@gmail.com, per -// http://mail.google.com/support/bin/answer.py?hl=en&ctx=mail&answer=10313# +// gmail does not consider '.' or caps inside a username to matter. std::string CanonicalizeEmail(const std::string& email_address); // Returns the canonical form of the given domain. @@ -35,9 +33,13 @@ std::string ExtractDomainName(const std::string& email); bool IsGaiaSignonRealm(const GURL& url); -// Parses JSON data returned by /ListAccounts call, returns vector of -// accounts (email addresses). -std::vector<std::string> ParseListAccountsData(const std::string& data); +// Parses JSON data returned by /ListAccounts call, returning a vector of +// email/valid pairs. An email addresses is considered valid if a passive +// login would succeed (i.e. the user does not need to reauthenticate). +// If there an error parsing the JSON, then false is returned. +bool ParseListAccountsData( + const std::string& data, + std::vector<std::pair<std::string, bool> >* accounts); } // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_auth_util_unittest.cc b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc index ebe8f876abe..b17ac57516a 100644 --- a/chromium/google_apis/gaia/gaia_auth_util_unittest.cc +++ b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc @@ -54,14 +54,9 @@ TEST(GaiaAuthUtilTest, EmailAddressDifferentOnesRejected) { CanonicalizeEmail("Us....E.r@what.com")); } -TEST(GaiaAuthUtilTest, EmailAddressIgnorePlusSuffix) { - const char with_plus[] = "user+cc@what.com"; - EXPECT_EQ(with_plus, CanonicalizeEmail(with_plus)); -} - -TEST(GaiaAuthUtilTest, EmailAddressIgnoreMultiPlusSuffix) { - const char multi_plus[] = "user+cc+bcc@what.com"; - EXPECT_EQ(multi_plus, CanonicalizeEmail(multi_plus)); +TEST(GaiaAuthUtilTest, GooglemailNotCanonicalizedToGmail) { + const char googlemail[] = "user@googlemail.com"; + EXPECT_EQ(googlemail, CanonicalizeEmail(googlemail)); } TEST(GaiaAuthUtilTest, CanonicalizeDomain) { @@ -93,6 +88,11 @@ TEST(GaiaAuthUtilTest, AreEmailsSame) { EXPECT_FALSE(AreEmailsSame("user@gmail.com", "foo@gmail.com")); } +TEST(GaiaAuthUtilTest, GmailAndGooglemailAreSame) { + EXPECT_TRUE(AreEmailsSame("foo@gmail.com", "foo@googlemail.com")); + EXPECT_FALSE(AreEmailsSame("bar@gmail.com", "foo@googlemail.com")); +} + TEST(GaiaAuthUtilTest, IsGaiaSignonRealm) { // Only https versions of Gaia URLs should be considered valid. EXPECT_TRUE(IsGaiaSignonRealm(GURL("https://accounts.google.com/"))); @@ -109,37 +109,79 @@ TEST(GaiaAuthUtilTest, IsGaiaSignonRealm) { } TEST(GaiaAuthUtilTest, ParseListAccountsData) { - std::vector<std::string> accounts; - accounts = ParseListAccountsData(""); + std::vector<std::pair<std::string, bool> > accounts; + ASSERT_FALSE(ParseListAccountsData("", &accounts)); ASSERT_EQ(0u, accounts.size()); - accounts = ParseListAccountsData("1"); + ASSERT_FALSE(ParseListAccountsData("1", &accounts)); ASSERT_EQ(0u, accounts.size()); - accounts = ParseListAccountsData("[]"); + ASSERT_FALSE(ParseListAccountsData("[]", &accounts)); ASSERT_EQ(0u, accounts.size()); - accounts = ParseListAccountsData("[\"foo\", \"bar\"]"); + ASSERT_FALSE(ParseListAccountsData("[\"foo\", \"bar\"]", &accounts)); ASSERT_EQ(0u, accounts.size()); - accounts = ParseListAccountsData("[\"foo\", []]"); + ASSERT_TRUE(ParseListAccountsData("[\"foo\", []]", &accounts)); ASSERT_EQ(0u, accounts.size()); - accounts = ParseListAccountsData( - "[\"foo\", [[\"bar\", 0, \"name\", 0, \"photo\", 0, 0, 0]]]"); + ASSERT_TRUE(ParseListAccountsData( + "[\"foo\", [[\"bar\", 0, \"name\", 0, \"photo\", 0, 0, 0]]]", &accounts)); ASSERT_EQ(0u, accounts.size()); - accounts = ParseListAccountsData( - "[\"foo\", [[\"bar\", 0, \"name\", \"email\", \"photo\", 0, 0, 0]]]"); + ASSERT_TRUE(ParseListAccountsData( + "[\"foo\", [[\"bar\", 0, \"name\", \"u@g.c\", \"photo\", 0, 0, 0]]]", + &accounts)); ASSERT_EQ(1u, accounts.size()); - ASSERT_EQ("email", accounts[0]); + ASSERT_EQ("u@g.c", accounts[0].first); + ASSERT_TRUE(accounts[0].second); - accounts = ParseListAccountsData( - "[\"foo\", [[\"bar1\", 0, \"name1\", \"email1\", \"photo1\", 0, 0, 0], " - "[\"bar2\", 0, \"name2\", \"email2\", \"photo2\", 0, 0, 0]]]"); + ASSERT_TRUE(ParseListAccountsData( + "[\"foo\", [[\"bar1\", 0, \"name1\", \"u1@g.c\", \"photo1\", 0, 0, 0], " + "[\"bar2\", 0, \"name2\", \"u2@g.c\", \"photo2\", 0, 0, 0]]]", + &accounts)); ASSERT_EQ(2u, accounts.size()); - ASSERT_EQ("email1", accounts[0]); - ASSERT_EQ("email2", accounts[1]); + ASSERT_EQ("u1@g.c", accounts[0].first); + ASSERT_TRUE(accounts[0].second); + ASSERT_EQ("u2@g.c", accounts[1].first); + ASSERT_TRUE(accounts[1].second); + + ASSERT_TRUE(ParseListAccountsData( + "[\"foo\", [[\"b1\", 0, \"name1\", \"U1@g.c\", \"photo1\", 0, 0, 0], " + "[\"b2\", 0, \"name2\", \"u.2@g.c\", \"photo2\", 0, 0, 0]]]", + &accounts)); + ASSERT_EQ(2u, accounts.size()); + ASSERT_EQ(CanonicalizeEmail("U1@g.c"), accounts[0].first); + ASSERT_TRUE(accounts[0].second); + ASSERT_EQ(CanonicalizeEmail("u.2@g.c"), accounts[1].first); + ASSERT_TRUE(accounts[1].second); +} + +TEST(GaiaAuthUtilTest, ParseListAccountsDataValidSession) { + std::vector<std::pair<std::string, bool> > accounts; + + // Missing valid session means: return account. + ASSERT_TRUE(ParseListAccountsData( + "[\"foo\", [[\"b\", 0, \"n\", \"u@g.c\", \"p\", 0, 0, 0]]]", + &accounts)); + ASSERT_EQ(1u, accounts.size()); + ASSERT_EQ("u@g.c", accounts[0].first); + ASSERT_TRUE(accounts[0].second); + + // Valid session is true means: return account. + ASSERT_TRUE(ParseListAccountsData( + "[\"foo\", [[\"b\", 0, \"n\", \"u@g.c\", \"p\", 0, 0, 0, 0, 1]]]", + &accounts)); + ASSERT_EQ(1u, accounts.size()); + ASSERT_EQ("u@g.c", accounts[0].first); + ASSERT_TRUE(accounts[0].second); + + // Valid session is false means: return account with valid bit false. + ASSERT_TRUE(ParseListAccountsData( + "[\"foo\", [[\"b\", 0, \"n\", \"u@g.c\", \"p\", 0, 0, 0, 0, 0]]]", + &accounts)); + ASSERT_EQ(1u, accounts.size()); + ASSERT_FALSE(accounts[0].second); } } // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_constants.cc b/chromium/google_apis/gaia/gaia_constants.cc index 1b1581d1545..95a5945f74e 100644 --- a/chromium/google_apis/gaia/gaia_constants.cc +++ b/chromium/google_apis/gaia/gaia_constants.cc @@ -23,6 +23,11 @@ const char kSyncService[] = "chromiumsync"; // Service name for remoting. const char kRemotingService[] = "chromoting"; +// OAuth scopes. +const char kOAuth1LoginScope[] = "https://www.google.com/accounts/OAuthLogin"; +const char kOAuthWrapBridgeUserInfoScope[] = + "https://www.googleapis.com/auth/userinfo.email"; + // Service/scope names for device management (cloud-based policy) server. const char kDeviceManagementServiceOAuth[] = "https://www.googleapis.com/auth/chromeosdevicemanagement"; @@ -34,7 +39,7 @@ const char kAnyApiOAuth2Scope[] = "https://www.googleapis.com/auth/any-api"; const char kChromeSyncOAuth2Scope[] = "https://www.googleapis.com/auth/chromesync"; // OAuth2 scope for access to the Chrome Sync APIs for managed profiles. -const char kChromeSyncManagedOAuth2Scope[] = +const char kChromeSyncSupervisedOAuth2Scope[] = "https://www.googleapis.com/auth/chromesync_playpen"; // OAuth2 scope for access to Google Talk APIs (XMPP). const char kGoogleTalkOAuth2Scope[] = diff --git a/chromium/google_apis/gaia/gaia_constants.h b/chromium/google_apis/gaia/gaia_constants.h index 3b45eee0dcd..ea8cc0f2f2c 100644 --- a/chromium/google_apis/gaia/gaia_constants.h +++ b/chromium/google_apis/gaia/gaia_constants.h @@ -18,10 +18,12 @@ extern const char kGaiaService[]; // uber token extern const char kPicasaService[]; extern const char kSyncService[]; extern const char kRemotingService[]; +extern const char kOAuth1LoginScope[]; +extern const char kOAuthWrapBridgeUserInfoScope[]; extern const char kDeviceManagementServiceOAuth[]; extern const char kAnyApiOAuth2Scope[]; extern const char kChromeSyncOAuth2Scope[]; -extern const char kChromeSyncManagedOAuth2Scope[]; +extern const char kChromeSyncSupervisedOAuth2Scope[]; extern const char kGoogleTalkOAuth2Scope[]; // Used with uber auth tokens when needed. diff --git a/chromium/google_apis/gaia/gaia_oauth_client.cc b/chromium/google_apis/gaia/gaia_oauth_client.cc index 8c6e1807369..1113ff67309 100644 --- a/chromium/google_apis/gaia/gaia_oauth_client.cc +++ b/chromium/google_apis/gaia/gaia_oauth_client.cc @@ -241,9 +241,12 @@ void GaiaOAuthClient::Core::HandleResponse( scoped_ptr<net::URLFetcher> old_request = request_.Pass(); DCHECK_EQ(source, old_request.get()); - // RC_BAD_REQUEST means the arguments are invalid. No point retrying. We are + // HTTP_BAD_REQUEST means the arguments are invalid. HTTP_UNAUTHORIZED means + // the access or refresh token is invalid. No point retrying. We are // done here. - if (source->GetResponseCode() == net::HTTP_BAD_REQUEST) { + int response_code = source->GetResponseCode(); + if (response_code == net::HTTP_BAD_REQUEST || + response_code == net::HTTP_UNAUTHORIZED) { delegate_->OnOAuthError(); return; } diff --git a/chromium/google_apis/gaia/gaia_oauth_client.h b/chromium/google_apis/gaia/gaia_oauth_client.h index 14a26a63d7e..8e01ef6a83a 100644 --- a/chromium/google_apis/gaia/gaia_oauth_client.h +++ b/chromium/google_apis/gaia/gaia_oauth_client.h @@ -49,7 +49,7 @@ class GaiaOAuthClient { virtual void OnGetUserIdResponse(const std::string& user_id) {} // Invoked on a successful response to the GetTokenInfo request. virtual void OnGetTokenInfoResponse( - scoped_ptr<DictionaryValue> token_info) {} + scoped_ptr<base::DictionaryValue> token_info) {} // Invoked when there is an OAuth error with one of the requests. virtual void OnOAuthError() = 0; // Invoked when there is a network error or upon receiving an invalid diff --git a/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc index 32e7c6620b4..d4014f73679 100644 --- a/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc +++ b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc @@ -199,15 +199,15 @@ class MockGaiaOAuthClientDelegate : public gaia::GaiaOAuthClient::Delegate { // override the problematic method to call through to it. // https://groups.google.com/a/chromium.org/d/msg/chromium-dev/01sDxsJ1OYw/I_S0xCBRF2oJ MOCK_METHOD1(OnGetTokenInfoResponsePtr, - void(const DictionaryValue* token_info)); - virtual void OnGetTokenInfoResponse(scoped_ptr<DictionaryValue> token_info) - OVERRIDE { + void(const base::DictionaryValue* token_info)); + virtual void OnGetTokenInfoResponse( + scoped_ptr<base::DictionaryValue> token_info) OVERRIDE { token_info_.reset(token_info.release()); OnGetTokenInfoResponsePtr(token_info_.get()); } private: - scoped_ptr<DictionaryValue> token_info_; + scoped_ptr<base::DictionaryValue> token_info_; DISALLOW_COPY_AND_ASSIGN(MockGaiaOAuthClientDelegate); }; @@ -328,7 +328,7 @@ TEST_F(GaiaOAuthClientTest, GetUserId) { } TEST_F(GaiaOAuthClientTest, GetTokenInfo) { - const DictionaryValue* captured_result; + const base::DictionaryValue* captured_result; MockGaiaOAuthClientDelegate delegate; EXPECT_CALL(delegate, OnGetTokenInfoResponsePtr(_)) diff --git a/chromium/google_apis/gaia/gaia_switches.cc b/chromium/google_apis/gaia/gaia_switches.cc index 16228c3837b..a1879d1c994 100644 --- a/chromium/google_apis/gaia/gaia_switches.cc +++ b/chromium/google_apis/gaia/gaia_switches.cc @@ -6,12 +6,10 @@ namespace switches { -const char kClientLoginToOAuth2Url[] = "client-login-to-oauth2-url"; const char kGaiaUrl[] = "gaia-url"; const char kGoogleApisUrl[] = "google-apis-url"; const char kLsoUrl[] = "lso-url"; -const char kOAuth1LoginScope[] = "oauth1-login-scope"; -const char kOAuthWrapBridgeUserInfoScope[] = - "oauth-wrap-bridge-user-info-scope"; +const char kOAuth2ClientID[] = "oauth2-client-id"; +const char kOAuth2ClientSecret[] = "oauth2-client-secret"; } // namespace switches diff --git a/chromium/google_apis/gaia/gaia_switches.h b/chromium/google_apis/gaia/gaia_switches.h index 0700f36c70c..bc69ebe5bab 100644 --- a/chromium/google_apis/gaia/gaia_switches.h +++ b/chromium/google_apis/gaia/gaia_switches.h @@ -7,9 +7,6 @@ namespace switches { -// Supplies custom client login to OAuth2 URL for testing purposes. -extern const char kClientLoginToOAuth2Url[]; - // Specifies the path for GAIA authentication URL. The default value is // "https://accounts.google.com". extern const char kGaiaUrl[]; @@ -22,11 +19,11 @@ extern const char kGoogleApisUrl[]; // "https://accounts.google.com". extern const char kLsoUrl[]; -// Specifies custom OAuth1 login scope for testing purposes. -extern const char kOAuth1LoginScope[]; +// Specifies custom OAuth2 client id for testing purposes. +extern const char kOAuth2ClientID[]; -// Overrides OAuth wrap bridge user info scope. -extern const char kOAuthWrapBridgeUserInfoScope[]; +// Specifies custom OAuth2 client secret for testing purposes. +extern const char kOAuth2ClientSecret[]; } // namespace switches diff --git a/chromium/google_apis/gaia/gaia_urls.cc b/chromium/google_apis/gaia/gaia_urls.cc index 053f7a37e29..0b44a911c74 100644 --- a/chromium/google_apis/gaia/gaia_urls.cc +++ b/chromium/google_apis/gaia/gaia_urls.cc @@ -28,15 +28,10 @@ const char kOAuthGetAccessTokenUrlSuffix[] = "OAuthGetAccessToken"; const char kOAuthWrapBridgeUrlSuffix[] = "OAuthWrapBridge"; const char kOAuth1LoginUrlSuffix[] = "OAuthLogin"; const char kOAuthRevokeTokenUrlSuffix[] = "AuthSubRevokeToken"; -const char kListAccountsSuffix[] = "ListAccounts"; +const char kListAccountsSuffix[] = "ListAccounts?json=standard"; const char kEmbeddedSigninSuffix[] = "EmbeddedSignIn"; const char kAddAccountSuffix[] = "AddSession"; -// OAuth scopes -const char kOAuth1LoginScope[] = "https://www.google.com/accounts/OAuthLogin"; -const char kOAuthWrapBridgeUserInfoScope[] = - "https://www.googleapis.com/auth/userinfo.email"; - // API calls from accounts.google.com (LSO) const char kGetOAuthTokenUrlSuffix[] = "o/oauth/GetOAuthToken/"; const char kClientLoginToOAuth2UrlSuffix[] = "o/oauth2/programmatic_auth"; @@ -127,14 +122,6 @@ GaiaUrls::GaiaUrls() { google_apis_origin_url_.Resolve(kOAuthUserInfoUrlSuffix); gaia_login_form_realm_ = gaia_url_; - - // OAuth scopes. - GetSwitchValueWithDefault(switches::kOAuthWrapBridgeUserInfoScope, - kOAuthWrapBridgeUserInfoScope, - &oauth_wrap_bridge_user_info_scope_); - GetSwitchValueWithDefault(switches::kOAuth1LoginScope, - kOAuth1LoginScope, - &oauth1_login_scope_); } GaiaUrls::~GaiaUrls() { @@ -216,14 +203,6 @@ const GURL& GaiaUrls::add_account_url() const { return add_account_url_; } -const std::string& GaiaUrls::oauth1_login_scope() const { - return oauth1_login_scope_; -} - -const std::string& GaiaUrls::oauth_wrap_bridge_user_info_scope() const { - return oauth_wrap_bridge_user_info_scope_; -} - const std::string& GaiaUrls::oauth2_chrome_client_id() const { return oauth2_chrome_client_id_; } diff --git a/chromium/google_apis/gaia/gaia_urls.h b/chromium/google_apis/gaia/gaia_urls.h index 2ec6499663e..f96e89ce230 100644 --- a/chromium/google_apis/gaia/gaia_urls.h +++ b/chromium/google_apis/gaia/gaia_urls.h @@ -36,9 +36,6 @@ class GaiaUrls { const GURL& embedded_signin_url() const; const GURL& add_account_url() const; - const std::string& oauth1_login_scope() const; - const std::string& oauth_wrap_bridge_user_info_scope() const; - const std::string& oauth2_chrome_client_id() const; const std::string& oauth2_chrome_client_secret() const; const GURL& client_login_to_oauth2_url() const; @@ -80,9 +77,6 @@ class GaiaUrls { GURL embedded_signin_url_; GURL add_account_url_; - std::string oauth1_login_scope_; - std::string oauth_wrap_bridge_user_info_scope_; - std::string oauth2_chrome_client_id_; std::string oauth2_chrome_client_secret_; diff --git a/chromium/google_apis/gaia/google_service_auth_error.cc b/chromium/google_apis/gaia/google_service_auth_error.cc index 736bbd62785..d968136de0d 100644 --- a/chromium/google_apis/gaia/google_service_auth_error.cc +++ b/chromium/google_apis/gaia/google_service_auth_error.cc @@ -81,12 +81,6 @@ GoogleServiceAuthError::GoogleServiceAuthError( error_message_(error_message) { } -GoogleServiceAuthError::GoogleServiceAuthError(const std::string& error_message) - : state_(INVALID_GAIA_CREDENTIALS), - network_error_(0), - error_message_(error_message) { -} - // static GoogleServiceAuthError GoogleServiceAuthError::FromConnectionError(int error) { @@ -259,14 +253,3 @@ GoogleServiceAuthError::GoogleServiceAuthError( captcha_unlock_url, image_width, image_height), network_error_(0) { } - -GoogleServiceAuthError::GoogleServiceAuthError( - State s, - const std::string& captcha_token, - const std::string& prompt_text, - const std::string& alternate_text, - int field_length) - : state_(s), - second_factor_(captcha_token, prompt_text, alternate_text, field_length), - network_error_(0) { -} diff --git a/chromium/google_apis/gaia/google_service_auth_error.h b/chromium/google_apis/gaia/google_service_auth_error.h index 19fdb515405..3d441ef9bae 100644 --- a/chromium/google_apis/gaia/google_service_auth_error.h +++ b/chromium/google_apis/gaia/google_service_auth_error.h @@ -189,8 +189,6 @@ class GoogleServiceAuthError { // Construct a GoogleServiceAuthError from |state| and |error_message|. GoogleServiceAuthError(State state, const std::string& error_message); - explicit GoogleServiceAuthError(const std::string& error_message); - GoogleServiceAuthError(State s, const std::string& captcha_token, const GURL& captcha_audio_url, const GURL& captcha_image_url, @@ -198,11 +196,6 @@ class GoogleServiceAuthError { int image_width, int image_height); - GoogleServiceAuthError(State s, const std::string& captcha_token, - const std::string& prompt_text, - const std::string& alternate_text, - int field_length); - State state_; Captcha captcha_; SecondFactor second_factor_; diff --git a/chromium/google_apis/gaia/google_service_auth_error_unittest.cc b/chromium/google_apis/gaia/google_service_auth_error_unittest.cc index d1f920ab9c2..8e221cc631a 100644 --- a/chromium/google_apis/gaia/google_service_auth_error_unittest.cc +++ b/chromium/google_apis/gaia/google_service_auth_error_unittest.cc @@ -20,7 +20,7 @@ class GoogleServiceAuthErrorTest : public testing::Test {}; void TestSimpleState(GoogleServiceAuthError::State state) { GoogleServiceAuthError error(state); - scoped_ptr<DictionaryValue> value(error.ToValue()); + scoped_ptr<base::DictionaryValue> value(error.ToValue()); EXPECT_EQ(1u, value->size()); std::string state_str; EXPECT_TRUE(value->GetString("state", &state_str)); @@ -38,7 +38,7 @@ TEST_F(GoogleServiceAuthErrorTest, SimpleToValue) { TEST_F(GoogleServiceAuthErrorTest, None) { GoogleServiceAuthError error(GoogleServiceAuthError::AuthErrorNone()); - scoped_ptr<DictionaryValue> value(error.ToValue()); + scoped_ptr<base::DictionaryValue> value(error.ToValue()); EXPECT_EQ(1u, value->size()); ExpectDictStringValue("NONE", *value, "state"); } @@ -46,7 +46,7 @@ TEST_F(GoogleServiceAuthErrorTest, None) { TEST_F(GoogleServiceAuthErrorTest, ConnectionFailed) { GoogleServiceAuthError error( GoogleServiceAuthError::FromConnectionError(net::OK)); - scoped_ptr<DictionaryValue> value(error.ToValue()); + scoped_ptr<base::DictionaryValue> value(error.ToValue()); EXPECT_EQ(2u, value->size()); ExpectDictStringValue("CONNECTION_FAILED", *value, "state"); ExpectDictStringValue("net::OK", *value, "networkError"); diff --git a/chromium/google_apis/gaia/identity_provider.cc b/chromium/google_apis/gaia/identity_provider.cc new file mode 100644 index 00000000000..842891e67f3 --- /dev/null +++ b/chromium/google_apis/gaia/identity_provider.cc @@ -0,0 +1,71 @@ +// 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/gaia/identity_provider.h" + +IdentityProvider::Observer::~Observer() {} + +IdentityProvider::~IdentityProvider() {} + +void IdentityProvider::AddActiveAccountRefreshTokenObserver( + OAuth2TokenService::Observer* observer) { + OAuth2TokenService* token_service = GetTokenService(); + if (!token_service || token_service_observers_.HasObserver(observer)) + return; + + token_service_observers_.AddObserver(observer); + if (++token_service_observer_count_ == 1) + token_service->AddObserver(this); +} + +void IdentityProvider::RemoveActiveAccountRefreshTokenObserver( + OAuth2TokenService::Observer* observer) { + OAuth2TokenService* token_service = GetTokenService(); + if (!token_service || !token_service_observers_.HasObserver(observer)) + return; + + token_service_observers_.RemoveObserver(observer); + if (--token_service_observer_count_ == 0) + token_service->RemoveObserver(this); +} + +void IdentityProvider::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void IdentityProvider::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void IdentityProvider::OnRefreshTokenAvailable(const std::string& account_id) { + if (account_id != GetActiveAccountId()) + return; + FOR_EACH_OBSERVER(OAuth2TokenService::Observer, + token_service_observers_, + OnRefreshTokenAvailable(account_id)); +} + +void IdentityProvider::OnRefreshTokenRevoked(const std::string& account_id) { + if (account_id != GetActiveAccountId()) + return; + FOR_EACH_OBSERVER(OAuth2TokenService::Observer, + token_service_observers_, + OnRefreshTokenRevoked(account_id)); +} + +void IdentityProvider::OnRefreshTokensLoaded() { + FOR_EACH_OBSERVER(OAuth2TokenService::Observer, + token_service_observers_, + OnRefreshTokensLoaded()); +} + +IdentityProvider::IdentityProvider() : token_service_observer_count_(0) {} + +void IdentityProvider::FireOnActiveAccountLogin() { + FOR_EACH_OBSERVER(Observer, observers_, OnActiveAccountLogin()); +} + +void IdentityProvider::FireOnActiveAccountLogout() { + FOR_EACH_OBSERVER(Observer, observers_, OnActiveAccountLogout()); +} diff --git a/chromium/google_apis/gaia/identity_provider.h b/chromium/google_apis/gaia/identity_provider.h new file mode 100644 index 00000000000..e0e99ee688c --- /dev/null +++ b/chromium/google_apis/gaia/identity_provider.h @@ -0,0 +1,93 @@ +// 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_GAIA_IDENTITY_PROVIDER_H_ +#define GOOGLE_APIS_GAIA_IDENTITY_PROVIDER_H_ + +#include <string> + +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "base/observer_list.h" +#include "google_apis/gaia/oauth2_token_service.h" + +// Helper class that provides access to information about logged-in GAIA +// accounts. Each instance of this class references an entity who may be logged +// in to zero, one or multiple GAIA accounts. The class provides access to the +// OAuth tokens for all logged-in accounts and indicates which of these is +// currently active. +// The main purpose of this abstraction layer is to isolate consumers of GAIA +// information from the different sources and various token service +// implementations. Whenever possible, consumers of GAIA information should be +// provided with an instance of this class instead of accessing other GAIA APIs +// directly. +class IdentityProvider : public OAuth2TokenService::Observer { + public: + class Observer { + public: + // Called when a GAIA account logs in and becomes the active account. All + // account information is available when this method is called and all + // |IdentityProvider| methods will return valid data. + virtual void OnActiveAccountLogin() {} + + // Called when the active GAIA account logs out. The account information may + // have been cleared already when this method is called. The + // |IdentityProvider| methods may return inconsistent or outdated + // information if called from within OnLogout(). + virtual void OnActiveAccountLogout() {} + + protected: + virtual ~Observer(); + }; + + virtual ~IdentityProvider(); + + // Adds and removes observers that will be notified of changes to the refresh + // token availability for the active account. + void AddActiveAccountRefreshTokenObserver( + OAuth2TokenService::Observer* observer); + void RemoveActiveAccountRefreshTokenObserver( + OAuth2TokenService::Observer* observer); + + // Gets the active account's user name. + virtual std::string GetActiveUsername() = 0; + + // Gets the active account's account ID. + virtual std::string GetActiveAccountId() = 0; + + // Gets the token service vending OAuth tokens for all logged-in accounts. + virtual OAuth2TokenService* GetTokenService() = 0; + + // Requests login to a GAIA account. Implementations can show a login UI, log + // in automatically if sufficient credentials are available or may ignore the + // request. Returns true if the login request was processed and false if it + // was ignored. + virtual bool RequestLogin() = 0; + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // OAuth2TokenService::Observer: + virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE; + virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE; + virtual void OnRefreshTokensLoaded() OVERRIDE; + + protected: + IdentityProvider(); + + // Fires an OnActiveAccountLogin notification. + void FireOnActiveAccountLogin(); + + // Fires an OnActiveAccountLogout notification. + void FireOnActiveAccountLogout(); + + private: + ObserverList<Observer, true> observers_; + ObserverList<OAuth2TokenService::Observer, true> token_service_observers_; + int token_service_observer_count_; + + DISALLOW_COPY_AND_ASSIGN(IdentityProvider); +}; + +#endif // GOOGLE_APIS_GAIA_IDENTITY_PROVIDER_H_ diff --git a/chromium/google_apis/gaia/merge_session_helper.cc b/chromium/google_apis/gaia/merge_session_helper.cc new file mode 100644 index 00000000000..c3c8a2ead10 --- /dev/null +++ b/chromium/google_apis/gaia/merge_session_helper.cc @@ -0,0 +1,178 @@ +// 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/gaia/merge_session_helper.h" + +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_constants.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/oauth2_token_service.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" + +MergeSessionHelper::MergeSessionHelper( + OAuth2TokenService* token_service, + net::URLRequestContextGetter* request_context, + Observer* observer) + : token_service_(token_service), + request_context_(request_context) { + if (observer) + AddObserver(observer); +} + +MergeSessionHelper::~MergeSessionHelper() { + DCHECK(accounts_.empty()); +} + +void MergeSessionHelper::LogIn(const std::string& account_id) { + DCHECK(!account_id.empty()); + accounts_.push_back(account_id); + if (accounts_.size() == 1) + StartFetching(); +} + +void MergeSessionHelper::AddObserver(Observer* observer) { + observer_list_.AddObserver(observer); +} + +void MergeSessionHelper::RemoveObserver(Observer* observer) { + observer_list_.RemoveObserver(observer); +} + +void MergeSessionHelper::CancelAll() { + gaia_auth_fetcher_.reset(); + uber_token_fetcher_.reset(); + accounts_.clear(); +} + +void MergeSessionHelper::LogOut( + const std::string& account_id, + const std::vector<std::string>& accounts) { + DCHECK(!account_id.empty()); + LogOutInternal(account_id, accounts); +} + +void MergeSessionHelper::LogOutInternal( + const std::string& account_id, + const std::vector<std::string>& accounts) { + bool pending = !accounts_.empty(); + + if (pending) { + for (std::deque<std::string>::const_iterator it = accounts_.begin() + 1; + it != accounts_.end(); it++) { + if (!it->empty() && + (std::find(accounts.begin(), accounts.end(), *it) == accounts.end() || + *it == account_id)) { + // We have a pending log in request for an account followed by + // a signout. + GoogleServiceAuthError error(GoogleServiceAuthError::REQUEST_CANCELED); + SignalComplete(*it, error); + } + } + + // Remove every thing in the work list besides the one that is running. + accounts_.resize(1); + } + + // Signal a logout to be the next thing to do unless the pending + // action is already a logout. + if (!pending || !accounts_.front().empty()) + accounts_.push_back(""); + + for (std::vector<std::string>::const_iterator it = accounts.begin(); + it != accounts.end(); it++) { + if (*it != account_id) { + DCHECK(!it->empty()); + accounts_.push_back(*it); + } + } + + if (!pending) + StartLogOutUrlFetch(); +} + +void MergeSessionHelper::LogOutAllAccounts() { + LogOutInternal("", std::vector<std::string>()); +} + +void MergeSessionHelper::SignalComplete( + const std::string& account_id, + const GoogleServiceAuthError& error) { + // Its possible for the observer to delete |this| object. Don't access + // access any members after this calling the observer. This method should + // be the last call in any other method. + FOR_EACH_OBSERVER(Observer, observer_list_, + MergeSessionCompleted(account_id, error)); +} + +void MergeSessionHelper::StartLogOutUrlFetch() { + DCHECK(accounts_.front().empty()); + GURL logout_url(GaiaUrls::GetInstance()->service_logout_url()); + net::URLFetcher* fetcher = + net::URLFetcher::Create(logout_url, net::URLFetcher::GET, this); + fetcher->SetRequestContext(request_context_); + fetcher->Start(); +} + +void MergeSessionHelper::OnUbertokenSuccess(const std::string& uber_token) { + VLOG(1) << "MergeSessionHelper::OnUbertokenSuccess" + << " account=" << accounts_.front(); + gaia_auth_fetcher_.reset(new GaiaAuthFetcher(this, + GaiaConstants::kChromeSource, + request_context_)); + gaia_auth_fetcher_->StartMergeSession(uber_token); +} + +void MergeSessionHelper::OnUbertokenFailure( + const GoogleServiceAuthError& error) { + VLOG(1) << "Failed to retrieve ubertoken" + << " account=" << accounts_.front() + << " error=" << error.ToString(); + const std::string account_id = accounts_.front(); + HandleNextAccount(); + SignalComplete(account_id, error); +} + +void MergeSessionHelper::OnMergeSessionSuccess(const std::string& data) { + DVLOG(1) << "MergeSession successful account=" << accounts_.front(); + const std::string account_id = accounts_.front(); + HandleNextAccount(); + SignalComplete(account_id, GoogleServiceAuthError::AuthErrorNone()); +} + +void MergeSessionHelper::OnMergeSessionFailure( + const GoogleServiceAuthError& error) { + VLOG(1) << "Failed MergeSession" + << " account=" << accounts_.front() + << " error=" << error.ToString(); + const std::string account_id = accounts_.front(); + HandleNextAccount(); + SignalComplete(account_id, error); +} + +void MergeSessionHelper::StartFetching() { + uber_token_fetcher_.reset(new UbertokenFetcher(token_service_, + this, + request_context_)); + uber_token_fetcher_->StartFetchingToken(accounts_.front()); +} + +void MergeSessionHelper::OnURLFetchComplete(const net::URLFetcher* source) { + DCHECK(accounts_.front().empty()); + HandleNextAccount(); +} + +void MergeSessionHelper::HandleNextAccount() { + accounts_.pop_front(); + gaia_auth_fetcher_.reset(); + if (accounts_.empty()) { + uber_token_fetcher_.reset(); + } else { + if (accounts_.front().empty()) { + StartLogOutUrlFetch(); + } else { + StartFetching(); + } + } +} diff --git a/chromium/google_apis/gaia/merge_session_helper.h b/chromium/google_apis/gaia/merge_session_helper.h new file mode 100644 index 00000000000..f4af8f04132 --- /dev/null +++ b/chromium/google_apis/gaia/merge_session_helper.h @@ -0,0 +1,117 @@ +// 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_GAIA_MERGE_SESSION_HELPER_H_ +#define GOOGLE_APIS_GAIA_MERGE_SESSION_HELPER_H_ + +#include <deque> + +#include "base/observer_list.h" +#include "google_apis/gaia/gaia_auth_consumer.h" +#include "google_apis/gaia/ubertoken_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" + +class GaiaAuthFetcher; +class GoogleServiceAuthError; +class OAuth2TokenService; + +namespace net { +class URLRequestContextGetter; +} + +// Merges a Google account known to Chrome into the cookie jar. When merging +// multiple accounts, one instance of the helper is better than multiple +// instances if there is the possibility that they run concurrently, since +// changes to the cookie must be serialized. +// +// By default instances of MergeSessionHelper delete themselves when done. +class MergeSessionHelper : public GaiaAuthConsumer, + public UbertokenConsumer, + public net::URLFetcherDelegate { + public: + class Observer { + public: + // Called whenever a merge session is completed. The account that was + // merged is given by |account_id|. If |error| is equal to + // GoogleServiceAuthError::AuthErrorNone() then the merge succeeeded. + virtual void MergeSessionCompleted(const std::string& account_id, + const GoogleServiceAuthError& error) = 0; + protected: + virtual ~Observer() {} + }; + + MergeSessionHelper(OAuth2TokenService* token_service, + net::URLRequestContextGetter* request_context, + Observer* observer); + virtual ~MergeSessionHelper(); + + void LogIn(const std::string& account_id); + + // Add or remove observers of this helper. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Cancel all login requests. + void CancelAll(); + + // Signout of |account_id| given a list of accounts already signed in. + // Since this involves signing out of all accounts and resigning back in, + // the order which |accounts| are given is important as it will dictate + // the sign in order. |account_id| does not have to be in |accounts|. + void LogOut(const std::string& account_id, + const std::vector<std::string>& accounts); + + // Signout all accounts. + void LogOutAllAccounts(); + + // Call observers when merge session completes. This public so that callers + // that know that a given account is already in the cookie jar can simply + // inform the observers. + void SignalComplete(const std::string& account_id, + const GoogleServiceAuthError& error); + + private: + // Overridden from UbertokenConsumer. + virtual void OnUbertokenSuccess(const std::string& token) OVERRIDE; + virtual void OnUbertokenFailure(const GoogleServiceAuthError& error) OVERRIDE; + + // Overridden from GaiaAuthConsumer. + virtual void OnMergeSessionSuccess(const std::string& data) OVERRIDE; + virtual void OnMergeSessionFailure(const GoogleServiceAuthError& error) + OVERRIDE; + + void LogOutInternal(const std::string& account_id, + const std::vector<std::string>& accounts); + + // Starts the proess of fetching the uber token and performing a merge session + // for the next account. Virtual so that it can be overriden in tests. + virtual void StartFetching(); + + // Virtual for testing purpose. + virtual void StartLogOutUrlFetch(); + + // Start the next merge session, if needed. + void HandleNextAccount(); + + // Overridden from URLFetcherDelgate. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + OAuth2TokenService* token_service_; + net::URLRequestContextGetter* request_context_; + scoped_ptr<GaiaAuthFetcher> gaia_auth_fetcher_; + scoped_ptr<UbertokenFetcher> uber_token_fetcher_; + + // A worklist for this class. Accounts names are stored here if + // we are pending a signin action for that account. Empty strings + // represent a signout request. + std::deque<std::string> accounts_; + + // List of observers to notify when merge session completes. + // Makes sure list is empty on destruction. + ObserverList<Observer, true> observer_list_; + + DISALLOW_COPY_AND_ASSIGN(MergeSessionHelper); +}; + +#endif // GOOGLE_APIS_GAIA_MERGE_SESSION_HELPER_H_ diff --git a/chromium/google_apis/gaia/merge_session_helper_unittest.cc b/chromium/google_apis/gaia/merge_session_helper_unittest.cc new file mode 100644 index 00000000000..1fbe8a6898a --- /dev/null +++ b/chromium/google_apis/gaia/merge_session_helper_unittest.cc @@ -0,0 +1,307 @@ +// 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 <algorithm> +#include <string> +#include <vector> + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/stringprintf.h" +#include "google_apis/gaia/fake_oauth2_token_service.h" +#include "google_apis/gaia/gaia_constants.h" +#include "google_apis/gaia/merge_session_helper.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class MockObserver : public MergeSessionHelper::Observer { + public: + explicit MockObserver(MergeSessionHelper* helper) : helper_(helper) { + helper_->AddObserver(this); + } + + ~MockObserver() { + helper_->RemoveObserver(this); + } + + MOCK_METHOD2(MergeSessionCompleted, + void(const std::string&, + const GoogleServiceAuthError& )); + private: + MergeSessionHelper* helper_; + + DISALLOW_COPY_AND_ASSIGN(MockObserver); +}; + +// Counts number of InstrumentedMergeSessionHelper created. +// We can EXPECT_* to be zero at the end of our unit tests +// to make sure everything is properly deleted. + +int total = 0; + +class InstrumentedMergeSessionHelper : public MergeSessionHelper { + public: + InstrumentedMergeSessionHelper( + OAuth2TokenService* token_service, + net::URLRequestContextGetter* request_context) : + MergeSessionHelper(token_service, request_context, NULL) { + total++; + } + + virtual ~InstrumentedMergeSessionHelper() { + total--; + } + + MOCK_METHOD0(StartFetching, void()); + MOCK_METHOD0(StartLogOutUrlFetch, void()); + + private: + DISALLOW_COPY_AND_ASSIGN(InstrumentedMergeSessionHelper); +}; + +class MergeSessionHelperTest : public testing::Test { + public: + MergeSessionHelperTest() + : no_error_(GoogleServiceAuthError::NONE), + error_(GoogleServiceAuthError::SERVICE_ERROR), + canceled_(GoogleServiceAuthError::REQUEST_CANCELED), + request_context_getter_(new net::TestURLRequestContextGetter( + base::MessageLoopProxy::current())) {} + + OAuth2TokenService* token_service() { return &token_service_; } + net::URLRequestContextGetter* request_context() { + return request_context_getter_; + } + + void SimulateUbertokenFailure(UbertokenConsumer* consumer, + const GoogleServiceAuthError& error) { + consumer->OnUbertokenFailure(error); + } + + void SimulateMergeSessionSuccess(GaiaAuthConsumer* consumer, + const std::string& data) { + consumer->OnMergeSessionSuccess(data); + } + + void SimulateMergeSessionFailure(GaiaAuthConsumer* consumer, + const GoogleServiceAuthError& error) { + consumer->OnMergeSessionFailure(error); + } + + void SimulateLogoutSuccess(net::URLFetcherDelegate* consumer) { + consumer->OnURLFetchComplete(NULL); + } + + const GoogleServiceAuthError& no_error() { return no_error_; } + const GoogleServiceAuthError& error() { return error_; } + const GoogleServiceAuthError& canceled() { return canceled_; } + + private: + base::MessageLoop message_loop_; + net::TestURLFetcherFactory factory_; + FakeOAuth2TokenService token_service_; + GoogleServiceAuthError no_error_; + GoogleServiceAuthError error_; + GoogleServiceAuthError canceled_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; +}; + +} // namespace + +using ::testing::_; + +TEST_F(MergeSessionHelperTest, Success) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + EXPECT_CALL(helper, StartFetching()); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", no_error())); + + helper.LogIn("acc1@gmail.com"); + SimulateMergeSessionSuccess(&helper, "token"); +} + +TEST_F(MergeSessionHelperTest, FailedMergeSession) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + EXPECT_CALL(helper, StartFetching()); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", error())); + + helper.LogIn("acc1@gmail.com"); + SimulateMergeSessionFailure(&helper, error()); +} + +TEST_F(MergeSessionHelperTest, FailedUbertoken) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + EXPECT_CALL(helper, StartFetching()); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", error())); + + helper.LogIn("acc1@gmail.com"); + SimulateUbertokenFailure(&helper, error()); +} + +TEST_F(MergeSessionHelperTest, ContinueAfterSuccess) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + EXPECT_CALL(helper, StartFetching()).Times(2); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", no_error())); + EXPECT_CALL(observer, MergeSessionCompleted("acc2@gmail.com", no_error())); + + helper.LogIn("acc1@gmail.com"); + helper.LogIn("acc2@gmail.com"); + SimulateMergeSessionSuccess(&helper, "token1"); + SimulateMergeSessionSuccess(&helper, "token2"); +} + +TEST_F(MergeSessionHelperTest, ContinueAfterFailure1) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + EXPECT_CALL(helper, StartFetching()).Times(2); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", error())); + EXPECT_CALL(observer, MergeSessionCompleted("acc2@gmail.com", no_error())); + + helper.LogIn("acc1@gmail.com"); + helper.LogIn("acc2@gmail.com"); + SimulateMergeSessionFailure(&helper, error()); + SimulateMergeSessionSuccess(&helper, "token2"); +} + +TEST_F(MergeSessionHelperTest, ContinueAfterFailure2) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + EXPECT_CALL(helper, StartFetching()).Times(2); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", error())); + EXPECT_CALL(observer, MergeSessionCompleted("acc2@gmail.com", no_error())); + + helper.LogIn("acc1@gmail.com"); + helper.LogIn("acc2@gmail.com"); + SimulateUbertokenFailure(&helper, error()); + SimulateMergeSessionSuccess(&helper, "token2"); +} + +TEST_F(MergeSessionHelperTest, AllRequestsInMultipleGoes) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + EXPECT_CALL(helper, StartFetching()).Times(4); + EXPECT_CALL(observer, MergeSessionCompleted(_, no_error())).Times(4); + + helper.LogIn("acc1@gmail.com"); + helper.LogIn("acc2@gmail.com"); + + SimulateMergeSessionSuccess(&helper, "token1"); + + helper.LogIn("acc3@gmail.com"); + + SimulateMergeSessionSuccess(&helper, "token2"); + SimulateMergeSessionSuccess(&helper, "token3"); + + helper.LogIn("acc4@gmail.com"); + + SimulateMergeSessionSuccess(&helper, "token4"); +} + +TEST_F(MergeSessionHelperTest, LogOut) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + std::vector<std::string> current_accounts; + current_accounts.push_back("acc1@gmail.com"); + current_accounts.push_back("acc2@gmail.com"); + current_accounts.push_back("acc3@gmail.com"); + + EXPECT_CALL(helper, StartLogOutUrlFetch()); + EXPECT_CALL(helper, StartFetching()).Times(2); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", no_error())); + EXPECT_CALL(observer, MergeSessionCompleted("acc3@gmail.com", no_error())); + + helper.LogOut("acc2@gmail.com", current_accounts); + SimulateLogoutSuccess(&helper); + SimulateMergeSessionSuccess(&helper, "token1"); + SimulateMergeSessionSuccess(&helper, "token3"); +} + +TEST_F(MergeSessionHelperTest, PendingSigninThenSignout) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + std::vector<std::string> current_accounts; + current_accounts.push_back("acc2@gmail.com"); + current_accounts.push_back("acc3@gmail.com"); + + // From the first Signin. + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", no_error())); + + // From the sign out and then re-sign in. + EXPECT_CALL(helper, StartLogOutUrlFetch()); + EXPECT_CALL(observer, MergeSessionCompleted("acc3@gmail.com", no_error())); + + // Total sign in 2 times, not enforcing ordered sequences. + EXPECT_CALL(helper, StartFetching()).Times(2); + + helper.LogIn("acc1@gmail.com"); + helper.LogOut("acc2@gmail.com", current_accounts); + + SimulateMergeSessionSuccess(&helper, "token1"); + SimulateLogoutSuccess(&helper); + SimulateMergeSessionSuccess(&helper, "token3"); +} + +TEST_F(MergeSessionHelperTest, CancelSignIn) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + std::vector<std::string> current_accounts; + + EXPECT_CALL(helper, StartFetching()); + EXPECT_CALL(observer, MergeSessionCompleted("acc2@gmail.com", canceled())); + EXPECT_CALL(observer, MergeSessionCompleted("acc1@gmail.com", no_error())); + EXPECT_CALL(helper, StartLogOutUrlFetch()); + + helper.LogIn("acc1@gmail.com"); + helper.LogIn("acc2@gmail.com"); + helper.LogOut("acc2@gmail.com", current_accounts); + + SimulateMergeSessionSuccess(&helper, "token1"); + SimulateLogoutSuccess(&helper); +} + +TEST_F(MergeSessionHelperTest, DoubleSignout) { + InstrumentedMergeSessionHelper helper(token_service(), request_context()); + MockObserver observer(&helper); + + std::vector<std::string> current_accounts1; + current_accounts1.push_back("acc1@gmail.com"); + current_accounts1.push_back("acc2@gmail.com"); + current_accounts1.push_back("acc3@gmail.com"); + + std::vector<std::string> current_accounts2; + current_accounts2.push_back("acc1@gmail.com"); + current_accounts2.push_back("acc3@gmail.com"); + + EXPECT_CALL(helper, StartFetching()).Times(2); + EXPECT_CALL(observer, MergeSessionCompleted("acc3@gmail.com", canceled())); + EXPECT_CALL(observer, + MergeSessionCompleted("acc1@gmail.com", no_error())).Times(2); + EXPECT_CALL(helper, StartLogOutUrlFetch()); + + helper.LogIn("acc1@gmail.com"); + helper.LogOut("acc2@gmail.com", current_accounts1); + helper.LogOut("acc3@gmail.com", current_accounts2); + + SimulateMergeSessionSuccess(&helper, "token1"); + SimulateLogoutSuccess(&helper); + SimulateMergeSessionSuccess(&helper, "token1"); +} diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc index ab382056d4b..12e464c67ff 100644 --- a/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc @@ -4,315 +4,19 @@ #include "google_apis/gaia/oauth2_access_token_fetcher.h" -#include <algorithm> -#include <string> -#include <vector> - -#include "base/json/json_reader.h" -#include "base/metrics/histogram.h" -#include "base/metrics/sparse_histogram.h" -#include "base/strings/string_util.h" -#include "base/strings/stringprintf.h" -#include "base/time/time.h" -#include "base/values.h" -#include "google_apis/gaia/gaia_urls.h" -#include "google_apis/gaia/google_service_auth_error.h" -#include "net/base/escape.h" -#include "net/base/load_flags.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" - -using net::ResponseCookies; -using net::URLFetcher; -using net::URLFetcherDelegate; -using net::URLRequestContextGetter; -using net::URLRequestStatus; - -namespace { -static const char kGetAccessTokenBodyFormat[] = - "client_id=%s&" - "client_secret=%s&" - "grant_type=refresh_token&" - "refresh_token=%s"; - -static const char kGetAccessTokenBodyWithScopeFormat[] = - "client_id=%s&" - "client_secret=%s&" - "grant_type=refresh_token&" - "refresh_token=%s&" - "scope=%s"; - -static const char kAccessTokenKey[] = "access_token"; -static const char kExpiresInKey[] = "expires_in"; -static const char kErrorKey[] = "error"; - -// Enumerated constants for logging server responses on 400 errors, matching -// RFC 6749. -enum OAuth2ErrorCodesForHistogram { - OAUTH2_ACCESS_ERROR_INVALID_REQUEST = 0, - OAUTH2_ACCESS_ERROR_INVALID_CLIENT, - OAUTH2_ACCESS_ERROR_INVALID_GRANT, - OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT, - OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE, - OAUTH2_ACCESS_ERROR_INVALID_SCOPE, - OAUTH2_ACCESS_ERROR_UNKNOWN, - OAUTH2_ACCESS_ERROR_COUNT -}; - -OAuth2ErrorCodesForHistogram OAuth2ErrorToHistogramValue( - const std::string& error) { - if (error == "invalid_request") - return OAUTH2_ACCESS_ERROR_INVALID_REQUEST; - else if (error == "invalid_client") - return OAUTH2_ACCESS_ERROR_INVALID_CLIENT; - else if (error == "invalid_grant") - return OAUTH2_ACCESS_ERROR_INVALID_GRANT; - else if (error == "unauthorized_client") - return OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT; - else if (error == "unsupported_grant_type") - return OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE; - else if (error == "invalid_scope") - return OAUTH2_ACCESS_ERROR_INVALID_SCOPE; - - return OAUTH2_ACCESS_ERROR_UNKNOWN; -} - -static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { - CHECK(!status.is_success()); - if (status.status() == URLRequestStatus::CANCELED) { - return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); - } else { - DLOG(WARNING) << "Could not reach Google Accounts servers: errno " - << status.error(); - return GoogleServiceAuthError::FromConnectionError(status.error()); - } -} - -static URLFetcher* CreateFetcher(URLRequestContextGetter* getter, - const GURL& url, - const std::string& body, - URLFetcherDelegate* delegate) { - bool empty_body = body.empty(); - URLFetcher* result = net::URLFetcher::Create( - 0, url, - empty_body ? URLFetcher::GET : URLFetcher::POST, - delegate); - - result->SetRequestContext(getter); - result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | - net::LOAD_DO_NOT_SAVE_COOKIES); - // Fetchers are sometimes cancelled because a network change was detected, - // especially at startup and after sign-in on ChromeOS. Retrying once should - // be enough in those cases; let the fetcher retry up to 3 times just in case. - // http://crbug.com/163710 - result->SetAutomaticallyRetryOnNetworkChanges(3); - - if (!empty_body) - result->SetUploadData("application/x-www-form-urlencoded", body); - - return result; -} -} // namespace - OAuth2AccessTokenFetcher::OAuth2AccessTokenFetcher( - OAuth2AccessTokenConsumer* consumer, - URLRequestContextGetter* getter) - : consumer_(consumer), - getter_(getter), - state_(INITIAL) { } - -OAuth2AccessTokenFetcher::~OAuth2AccessTokenFetcher() { } + OAuth2AccessTokenConsumer* consumer) + : consumer_(consumer) {} -void OAuth2AccessTokenFetcher::CancelRequest() { - fetcher_.reset(); -} - -void OAuth2AccessTokenFetcher::Start(const std::string& client_id, - const std::string& client_secret, - const std::string& refresh_token, - const std::vector<std::string>& scopes) { - client_id_ = client_id; - client_secret_ = client_secret; - refresh_token_ = refresh_token; - scopes_ = scopes; - StartGetAccessToken(); -} - -void OAuth2AccessTokenFetcher::StartGetAccessToken() { - CHECK_EQ(INITIAL, state_); - state_ = GET_ACCESS_TOKEN_STARTED; - fetcher_.reset(CreateFetcher( - getter_, - MakeGetAccessTokenUrl(), - MakeGetAccessTokenBody( - client_id_, client_secret_, refresh_token_, scopes_), - this)); - fetcher_->Start(); // OnURLFetchComplete will be called. -} +OAuth2AccessTokenFetcher::~OAuth2AccessTokenFetcher() {} -void OAuth2AccessTokenFetcher::EndGetAccessToken( - const net::URLFetcher* source) { - CHECK_EQ(GET_ACCESS_TOKEN_STARTED, state_); - state_ = GET_ACCESS_TOKEN_DONE; - - URLRequestStatus status = source->GetStatus(); - int histogram_value = status.is_success() ? source->GetResponseCode() : - status.error(); - UMA_HISTOGRAM_SPARSE_SLOWLY("Gaia.ResponseCodesForOAuth2AccessToken", - histogram_value); - if (!status.is_success()) { - OnGetTokenFailure(CreateAuthError(status)); - return; - } - - switch (source->GetResponseCode()) { - case net::HTTP_OK: - break; - case net::HTTP_FORBIDDEN: - case net::HTTP_INTERNAL_SERVER_ERROR: - // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be - // '403 Rate Limit Exeeded.' 500 is always treated as transient. - OnGetTokenFailure(GoogleServiceAuthError( - GoogleServiceAuthError::SERVICE_UNAVAILABLE)); - return; - case net::HTTP_BAD_REQUEST: { - // HTTP_BAD_REQUEST (400) usually contains error as per - // http://tools.ietf.org/html/rfc6749#section-5.2. - std::string gaia_error; - if (!ParseGetAccessTokenFailureResponse(source, &gaia_error)) { - OnGetTokenFailure(GoogleServiceAuthError( - GoogleServiceAuthError::SERVICE_ERROR)); - return; - } - - OAuth2ErrorCodesForHistogram access_error(OAuth2ErrorToHistogramValue( - gaia_error)); - UMA_HISTOGRAM_ENUMERATION("Gaia.BadRequestTypeForOAuth2AccessToken", - access_error, OAUTH2_ACCESS_ERROR_COUNT); - - OnGetTokenFailure(access_error == OAUTH2_ACCESS_ERROR_INVALID_GRANT ? - GoogleServiceAuthError( - GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) : - GoogleServiceAuthError( - GoogleServiceAuthError::SERVICE_ERROR)); - return; - } - default: - // The other errors are treated as permanent error. - OnGetTokenFailure(GoogleServiceAuthError( - GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); - return; - } - - // The request was successfully fetched and it returned OK. - // Parse out the access token and the expiration time. - std::string access_token; - int expires_in; - if (!ParseGetAccessTokenSuccessResponse( - source, &access_token, &expires_in)) { - DLOG(WARNING) << "Response doesn't match expected format"; - OnGetTokenFailure( - GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); - return; - } - // The token will expire in |expires_in| seconds. Take a 10% error margin to - // prevent reusing a token too close to its expiration date. - OnGetTokenSuccess( - access_token, - base::Time::Now() + base::TimeDelta::FromSeconds(9 * expires_in / 10)); -} - -void OAuth2AccessTokenFetcher::OnGetTokenSuccess( +void OAuth2AccessTokenFetcher::FireOnGetTokenSuccess( const std::string& access_token, const base::Time& expiration_time) { consumer_->OnGetTokenSuccess(access_token, expiration_time); } -void OAuth2AccessTokenFetcher::OnGetTokenFailure( +void OAuth2AccessTokenFetcher::FireOnGetTokenFailure( const GoogleServiceAuthError& error) { - state_ = ERROR_STATE; consumer_->OnGetTokenFailure(error); } - -void OAuth2AccessTokenFetcher::OnURLFetchComplete( - const net::URLFetcher* source) { - CHECK(source); - CHECK(state_ == GET_ACCESS_TOKEN_STARTED); - EndGetAccessToken(source); -} - -// static -GURL OAuth2AccessTokenFetcher::MakeGetAccessTokenUrl() { - return GaiaUrls::GetInstance()->oauth2_token_url(); -} - -// static -std::string OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( - const std::string& client_id, - const std::string& client_secret, - const std::string& refresh_token, - const std::vector<std::string>& scopes) { - std::string enc_client_id = net::EscapeUrlEncodedData(client_id, true); - std::string enc_client_secret = - net::EscapeUrlEncodedData(client_secret, true); - std::string enc_refresh_token = - net::EscapeUrlEncodedData(refresh_token, true); - if (scopes.empty()) { - return base::StringPrintf( - kGetAccessTokenBodyFormat, - enc_client_id.c_str(), - enc_client_secret.c_str(), - enc_refresh_token.c_str()); - } else { - std::string scopes_string = JoinString(scopes, ' '); - return base::StringPrintf( - kGetAccessTokenBodyWithScopeFormat, - enc_client_id.c_str(), - enc_client_secret.c_str(), - enc_refresh_token.c_str(), - net::EscapeUrlEncodedData(scopes_string, true).c_str()); - } -} - -scoped_ptr<base::DictionaryValue> ParseGetAccessTokenResponse( - const net::URLFetcher* source) { - CHECK(source); - - std::string data; - source->GetResponseAsString(&data); - scoped_ptr<base::Value> value(base::JSONReader::Read(data)); - if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) - value.reset(); - - return scoped_ptr<base::DictionaryValue>( - static_cast<base::DictionaryValue*>(value.release())); -} - -// static -bool OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( - const net::URLFetcher* source, - std::string* access_token, - int* expires_in) { - CHECK(access_token); - scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse( - source); - if (value.get() == NULL) - return false; - - return value->GetString(kAccessTokenKey, access_token) && - value->GetInteger(kExpiresInKey, expires_in); -} - -// static -bool OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse( - const net::URLFetcher* source, - std::string* error) { - CHECK(error); - scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse( - source); - if (value.get() == NULL) - return false; - return value->GetString(kErrorKey, error); -} diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.h b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h index 90805c09963..cd3ee5c7ba4 100644 --- a/chromium/google_apis/gaia/oauth2_access_token_fetcher.h +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h @@ -8,33 +8,12 @@ #include <string> #include <vector> -#include "base/gtest_prod_util.h" -#include "base/memory/scoped_ptr.h" #include "google_apis/gaia/oauth2_access_token_consumer.h" #include "net/url_request/url_fetcher_delegate.h" -#include "url/gurl.h" -class OAuth2AccessTokenFetcherTest; +class OAuth2AccessTokenConsumer; -namespace base { -class Time; -} - -namespace net { -class URLFetcher; -class URLRequestContextGetter; -class URLRequestStatus; -} - -// Abstracts the details to get OAuth2 access token token from -// OAuth2 refresh token. -// See "Using the Refresh Token" section in: -// http://code.google.com/apis/accounts/docs/OAuth2WebServer.html -// -// This class should be used on a single thread, but it can be whichever thread -// that you like. -// Also, do not reuse the same instance. Once Start() is called, the instance -// should not be reused. +// Interface of a OAuth2 access token fetcher. // // Usage: // * Create an instance with a consumer. @@ -44,10 +23,9 @@ class URLRequestStatus; // // This class can handle one request at a time. To parallelize requests, // create multiple instances. -class OAuth2AccessTokenFetcher : public net::URLFetcherDelegate { +class OAuth2AccessTokenFetcher { public: - OAuth2AccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, - net::URLRequestContextGetter* getter); + explicit OAuth2AccessTokenFetcher(OAuth2AccessTokenConsumer* consumer); virtual ~OAuth2AccessTokenFetcher(); // Starts the flow with the given parameters. @@ -58,65 +36,21 @@ class OAuth2AccessTokenFetcher : public net::URLFetcherDelegate { // a super-set of the specified scopes. virtual void Start(const std::string& client_id, const std::string& client_secret, - const std::string& refresh_token, - const std::vector<std::string>& scopes); - - void CancelRequest(); - - // Implementation of net::URLFetcherDelegate - virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; - - private: - enum State { - INITIAL, - GET_ACCESS_TOKEN_STARTED, - GET_ACCESS_TOKEN_DONE, - ERROR_STATE, - }; - - // Helper methods for the flow. - void StartGetAccessToken(); - void EndGetAccessToken(const net::URLFetcher* source); - - // Helper mehtods for reporting back results. - void OnGetTokenSuccess(const std::string& access_token, - const base::Time& expiration_time); - void OnGetTokenFailure(const GoogleServiceAuthError& error); + const std::vector<std::string>& scopes) = 0; - // Other helpers. - static GURL MakeGetAccessTokenUrl(); - static std::string MakeGetAccessTokenBody( - const std::string& client_id, - const std::string& client_secret, - const std::string& refresh_token, - const std::vector<std::string>& scopes); + // Cancels the current request and informs the consumer. + virtual void CancelRequest() = 0; - static bool ParseGetAccessTokenSuccessResponse( - const net::URLFetcher* source, - std::string* access_token, - int* expires_in); + protected: + // Fires |OnGetTokenSuccess| on |consumer_|. + void FireOnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time); - static bool ParseGetAccessTokenFailureResponse( - const net::URLFetcher* source, - std::string* error); + // Fires |OnGetTokenFailure| on |consumer_|. + void FireOnGetTokenFailure(const GoogleServiceAuthError& error); - // State that is set during construction. + private: OAuth2AccessTokenConsumer* const consumer_; - net::URLRequestContextGetter* const getter_; - State state_; - - // While a fetch is in progress. - scoped_ptr<net::URLFetcher> fetcher_; - std::string client_id_; - std::string client_secret_; - std::string refresh_token_; - std::vector<std::string> scopes_; - - friend class OAuth2AccessTokenFetcherTest; - FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherTest, - ParseGetAccessTokenResponse); - FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherTest, - MakeGetAccessTokenBody); DISALLOW_COPY_AND_ASSIGN(OAuth2AccessTokenFetcher); }; diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl.cc new file mode 100644 index 00000000000..f4cd4f04f1e --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl.cc @@ -0,0 +1,313 @@ +// 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/gaia/oauth2_access_token_fetcher_impl.h" + +#include <algorithm> +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/metrics/histogram.h" +#include "base/metrics/sparse_histogram.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/base/load_flags.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" + +using net::ResponseCookies; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { +static const char kGetAccessTokenBodyFormat[] = + "client_id=%s&" + "client_secret=%s&" + "grant_type=refresh_token&" + "refresh_token=%s"; + +static const char kGetAccessTokenBodyWithScopeFormat[] = + "client_id=%s&" + "client_secret=%s&" + "grant_type=refresh_token&" + "refresh_token=%s&" + "scope=%s"; + +static const char kAccessTokenKey[] = "access_token"; +static const char kExpiresInKey[] = "expires_in"; +static const char kErrorKey[] = "error"; + +// Enumerated constants for logging server responses on 400 errors, matching +// RFC 6749. +enum OAuth2ErrorCodesForHistogram { + OAUTH2_ACCESS_ERROR_INVALID_REQUEST = 0, + OAUTH2_ACCESS_ERROR_INVALID_CLIENT, + OAUTH2_ACCESS_ERROR_INVALID_GRANT, + OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT, + OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE, + OAUTH2_ACCESS_ERROR_INVALID_SCOPE, + OAUTH2_ACCESS_ERROR_UNKNOWN, + OAUTH2_ACCESS_ERROR_COUNT +}; + +OAuth2ErrorCodesForHistogram OAuth2ErrorToHistogramValue( + const std::string& error) { + if (error == "invalid_request") + return OAUTH2_ACCESS_ERROR_INVALID_REQUEST; + else if (error == "invalid_client") + return OAUTH2_ACCESS_ERROR_INVALID_CLIENT; + else if (error == "invalid_grant") + return OAUTH2_ACCESS_ERROR_INVALID_GRANT; + else if (error == "unauthorized_client") + return OAUTH2_ACCESS_ERROR_UNAUTHORIZED_CLIENT; + else if (error == "unsupported_grant_type") + return OAUTH2_ACCESS_ERROR_UNSUPPORTED_GRANT_TYPE; + else if (error == "invalid_scope") + return OAUTH2_ACCESS_ERROR_INVALID_SCOPE; + + return OAUTH2_ACCESS_ERROR_UNKNOWN; +} + +static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { + CHECK(!status.is_success()); + if (status.status() == URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } +} + +static URLFetcher* CreateFetcher(URLRequestContextGetter* getter, + const GURL& url, + const std::string& body, + URLFetcherDelegate* delegate) { + bool empty_body = body.empty(); + URLFetcher* result = net::URLFetcher::Create( + 0, url, empty_body ? URLFetcher::GET : URLFetcher::POST, delegate); + + result->SetRequestContext(getter); + result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + // Fetchers are sometimes cancelled because a network change was detected, + // especially at startup and after sign-in on ChromeOS. Retrying once should + // be enough in those cases; let the fetcher retry up to 3 times just in case. + // http://crbug.com/163710 + result->SetAutomaticallyRetryOnNetworkChanges(3); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + + return result; +} + +scoped_ptr<base::DictionaryValue> ParseGetAccessTokenResponse( + const net::URLFetcher* source) { + CHECK(source); + + std::string data; + source->GetResponseAsString(&data); + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + value.reset(); + + return scoped_ptr<base::DictionaryValue>( + static_cast<base::DictionaryValue*>(value.release())); +} + +} // namespace + +OAuth2AccessTokenFetcherImpl::OAuth2AccessTokenFetcherImpl( + OAuth2AccessTokenConsumer* consumer, + net::URLRequestContextGetter* getter, + const std::string& refresh_token) + : OAuth2AccessTokenFetcher(consumer), + getter_(getter), + refresh_token_(refresh_token), + state_(INITIAL) {} + +OAuth2AccessTokenFetcherImpl::~OAuth2AccessTokenFetcherImpl() {} + +void OAuth2AccessTokenFetcherImpl::CancelRequest() { fetcher_.reset(); } + +void OAuth2AccessTokenFetcherImpl::Start( + const std::string& client_id, + const std::string& client_secret, + const std::vector<std::string>& scopes) { + client_id_ = client_id; + client_secret_ = client_secret; + scopes_ = scopes; + StartGetAccessToken(); +} + +void OAuth2AccessTokenFetcherImpl::StartGetAccessToken() { + CHECK_EQ(INITIAL, state_); + state_ = GET_ACCESS_TOKEN_STARTED; + fetcher_.reset( + CreateFetcher(getter_, + MakeGetAccessTokenUrl(), + MakeGetAccessTokenBody( + client_id_, client_secret_, refresh_token_, scopes_), + this)); + fetcher_->Start(); // OnURLFetchComplete will be called. +} + +void OAuth2AccessTokenFetcherImpl::EndGetAccessToken( + const net::URLFetcher* source) { + CHECK_EQ(GET_ACCESS_TOKEN_STARTED, state_); + state_ = GET_ACCESS_TOKEN_DONE; + + URLRequestStatus status = source->GetStatus(); + int histogram_value = + status.is_success() ? source->GetResponseCode() : status.error(); + UMA_HISTOGRAM_SPARSE_SLOWLY("Gaia.ResponseCodesForOAuth2AccessToken", + histogram_value); + if (!status.is_success()) { + OnGetTokenFailure(CreateAuthError(status)); + return; + } + + switch (source->GetResponseCode()) { + case net::HTTP_OK: + break; + case net::HTTP_FORBIDDEN: + case net::HTTP_INTERNAL_SERVER_ERROR: + // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be + // '403 Rate Limit Exeeded.' 500 is always treated as transient. + OnGetTokenFailure( + GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); + return; + case net::HTTP_BAD_REQUEST: { + // HTTP_BAD_REQUEST (400) usually contains error as per + // http://tools.ietf.org/html/rfc6749#section-5.2. + std::string gaia_error; + if (!ParseGetAccessTokenFailureResponse(source, &gaia_error)) { + OnGetTokenFailure( + GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_ERROR)); + return; + } + + OAuth2ErrorCodesForHistogram access_error( + OAuth2ErrorToHistogramValue(gaia_error)); + UMA_HISTOGRAM_ENUMERATION("Gaia.BadRequestTypeForOAuth2AccessToken", + access_error, + OAUTH2_ACCESS_ERROR_COUNT); + + OnGetTokenFailure( + access_error == OAUTH2_ACCESS_ERROR_INVALID_GRANT + ? GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) + : GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_ERROR)); + return; + } + default: + // The other errors are treated as permanent error. + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + return; + } + + // The request was successfully fetched and it returned OK. + // Parse out the access token and the expiration time. + std::string access_token; + int expires_in; + if (!ParseGetAccessTokenSuccessResponse(source, &access_token, &expires_in)) { + DLOG(WARNING) << "Response doesn't match expected format"; + OnGetTokenFailure( + GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); + return; + } + // The token will expire in |expires_in| seconds. Take a 10% error margin to + // prevent reusing a token too close to its expiration date. + OnGetTokenSuccess( + access_token, + base::Time::Now() + base::TimeDelta::FromSeconds(9 * expires_in / 10)); +} + +void OAuth2AccessTokenFetcherImpl::OnGetTokenSuccess( + const std::string& access_token, + const base::Time& expiration_time) { + FireOnGetTokenSuccess(access_token, expiration_time); +} + +void OAuth2AccessTokenFetcherImpl::OnGetTokenFailure( + const GoogleServiceAuthError& error) { + state_ = ERROR_STATE; + FireOnGetTokenFailure(error); +} + +void OAuth2AccessTokenFetcherImpl::OnURLFetchComplete( + const net::URLFetcher* source) { + CHECK(source); + CHECK(state_ == GET_ACCESS_TOKEN_STARTED); + EndGetAccessToken(source); +} + +// static +GURL OAuth2AccessTokenFetcherImpl::MakeGetAccessTokenUrl() { + return GaiaUrls::GetInstance()->oauth2_token_url(); +} + +// static +std::string OAuth2AccessTokenFetcherImpl::MakeGetAccessTokenBody( + const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes) { + std::string enc_client_id = net::EscapeUrlEncodedData(client_id, true); + std::string enc_client_secret = + net::EscapeUrlEncodedData(client_secret, true); + std::string enc_refresh_token = + net::EscapeUrlEncodedData(refresh_token, true); + if (scopes.empty()) { + return base::StringPrintf(kGetAccessTokenBodyFormat, + enc_client_id.c_str(), + enc_client_secret.c_str(), + enc_refresh_token.c_str()); + } else { + std::string scopes_string = JoinString(scopes, ' '); + return base::StringPrintf( + kGetAccessTokenBodyWithScopeFormat, + enc_client_id.c_str(), + enc_client_secret.c_str(), + enc_refresh_token.c_str(), + net::EscapeUrlEncodedData(scopes_string, true).c_str()); + } +} + +// static +bool OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenSuccessResponse( + const net::URLFetcher* source, + std::string* access_token, + int* expires_in) { + CHECK(access_token); + scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse(source); + if (value.get() == NULL) + return false; + + return value->GetString(kAccessTokenKey, access_token) && + value->GetInteger(kExpiresInKey, expires_in); +} + +// static +bool OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenFailureResponse( + const net::URLFetcher* source, + std::string* error) { + CHECK(error); + scoped_ptr<base::DictionaryValue> value = ParseGetAccessTokenResponse(source); + if (value.get() == NULL) + return false; + return value->GetString(kErrorKey, error); +} diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl.h b/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl.h new file mode 100644 index 00000000000..8839b6c13cb --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl.h @@ -0,0 +1,118 @@ +// 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_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_IMPL_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_IMPL_H_ + +#include <string> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" +#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "url/gurl.h" + +class OAuth2AccessTokenFetcherImplTest; + +namespace base { +class Time; +} + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +class URLRequestStatus; +} + +// Abstracts the details to get OAuth2 access token token from +// OAuth2 refresh token. +// See "Using the Refresh Token" section in: +// http://code.google.com/apis/accounts/docs/OAuth2WebServer.html +// +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// Also, do not reuse the same instance. Once Start() is called, the instance +// should not be reused. +// +// Usage: +// * Create an instance with a consumer. +// * Call Start() +// * The consumer passed in the constructor will be called on the same +// thread Start was called with the results. +// +// This class can handle one request at a time. To parallelize requests, +// create multiple instances. +class OAuth2AccessTokenFetcherImpl : public OAuth2AccessTokenFetcher, + public net::URLFetcherDelegate { + public: + OAuth2AccessTokenFetcherImpl(OAuth2AccessTokenConsumer* consumer, + net::URLRequestContextGetter* getter, + const std::string& refresh_token); + virtual ~OAuth2AccessTokenFetcherImpl(); + + // Implementation of OAuth2AccessTokenFetcher + virtual void Start(const std::string& client_id, + const std::string& client_secret, + const std::vector<std::string>& scopes) OVERRIDE; + + virtual void CancelRequest() OVERRIDE; + + // Implementation of net::URLFetcherDelegate + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + enum State { + INITIAL, + GET_ACCESS_TOKEN_STARTED, + GET_ACCESS_TOKEN_DONE, + ERROR_STATE, + }; + + // Helper methods for the flow. + void StartGetAccessToken(); + void EndGetAccessToken(const net::URLFetcher* source); + + // Helper mehtods for reporting back results. + void OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time); + void OnGetTokenFailure(const GoogleServiceAuthError& error); + + // Other helpers. + static GURL MakeGetAccessTokenUrl(); + static std::string MakeGetAccessTokenBody( + const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes); + + static bool ParseGetAccessTokenSuccessResponse(const net::URLFetcher* source, + std::string* access_token, + int* expires_in); + + static bool ParseGetAccessTokenFailureResponse(const net::URLFetcher* source, + std::string* error); + + // State that is set during construction. + net::URLRequestContextGetter* const getter_; + std::string refresh_token_; + State state_; + + // While a fetch is in progress. + scoped_ptr<net::URLFetcher> fetcher_; + std::string client_id_; + std::string client_secret_; + std::vector<std::string> scopes_; + + friend class OAuth2AccessTokenFetcherImplTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherImplTest, + ParseGetAccessTokenResponse); + FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherImplTest, + MakeGetAccessTokenBody); + + DISALLOW_COPY_AND_ASSIGN(OAuth2AccessTokenFetcherImpl); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_IMPL_H_ diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl_unittest.cc index 135e292d7c0..f263932d4cf 100644 --- a/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher_impl_unittest.cc @@ -1,17 +1,17 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// 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. // -// A complete set of unit tests for OAuth2AccessTokenFetcher. +// A complete set of unit tests for OAuth2AccessTokenFetcherImpl. #include <string> #include "base/memory/scoped_ptr.h" -#include "content/public/test/test_browser_thread_bundle.h" +#include "base/run_loop.h" #include "google_apis/gaia/gaia_urls.h" #include "google_apis/gaia/google_service_auth_error.h" #include "google_apis/gaia/oauth2_access_token_consumer.h" -#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "google_apis/gaia/oauth2_access_token_fetcher_impl.h" #include "net/http/http_status_code.h" #include "net/url_request/test_url_fetcher_factory.h" #include "net/url_request/url_fetcher.h" @@ -57,18 +57,15 @@ static const char kValidFailureTokenResponse[] = class MockUrlFetcherFactory : public ScopedURLFetcherFactory, public URLFetcherFactory { -public: - MockUrlFetcherFactory() - : ScopedURLFetcherFactory(this) { - } + public: + MockUrlFetcherFactory() : ScopedURLFetcherFactory(this) {} virtual ~MockUrlFetcherFactory() {} - MOCK_METHOD4( - CreateURLFetcher, - URLFetcher* (int id, - const GURL& url, - URLFetcher::RequestType request_type, - URLFetcherDelegate* d)); + MOCK_METHOD4(CreateURLFetcher, + URLFetcher*(int id, + const GURL& url, + URLFetcher::RequestType request_type, + URLFetcherDelegate* d)); }; class MockOAuth2AccessTokenConsumer : public OAuth2AccessTokenConsumer { @@ -76,26 +73,28 @@ class MockOAuth2AccessTokenConsumer : public OAuth2AccessTokenConsumer { MockOAuth2AccessTokenConsumer() {} ~MockOAuth2AccessTokenConsumer() {} - MOCK_METHOD2(OnGetTokenSuccess, void(const std::string& access_token, - const base::Time& expiration_time)); - MOCK_METHOD1(OnGetTokenFailure, - void(const GoogleServiceAuthError& error)); + MOCK_METHOD2(OnGetTokenSuccess, + void(const std::string& access_token, + const base::Time& expiration_time)); + MOCK_METHOD1(OnGetTokenFailure, void(const GoogleServiceAuthError& error)); }; } // namespace -class OAuth2AccessTokenFetcherTest : public testing::Test { +class OAuth2AccessTokenFetcherImplTest : public testing::Test { public: - OAuth2AccessTokenFetcherTest() - : request_context_getter_(new net::TestURLRequestContextGetter( - base::MessageLoopProxy::current())), - fetcher_(&consumer_, request_context_getter_) { + OAuth2AccessTokenFetcherImplTest() + : request_context_getter_(new net::TestURLRequestContextGetter( + base::MessageLoopProxy::current())), + fetcher_(&consumer_, request_context_getter_, "refresh_token") { + base::RunLoop().RunUntilIdle(); } - virtual ~OAuth2AccessTokenFetcherTest() {} + virtual ~OAuth2AccessTokenFetcherImplTest() {} - virtual TestURLFetcher* SetupGetAccessToken( - bool fetch_succeeds, int response_code, const std::string& body) { + virtual TestURLFetcher* SetupGetAccessToken(bool fetch_succeeds, + int response_code, + const std::string& body) { GURL url(GaiaUrls::GetInstance()->oauth2_token_url()); TestURLFetcher* url_fetcher = new TestURLFetcher(0, url, &fetcher_); URLRequestStatus::Status status = @@ -114,47 +113,49 @@ class OAuth2AccessTokenFetcherTest : public testing::Test { } protected: - content::TestBrowserThreadBundle thread_bundle_; + base::MessageLoop message_loop_; MockUrlFetcherFactory factory_; MockOAuth2AccessTokenConsumer consumer_; scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; - OAuth2AccessTokenFetcher fetcher_; + OAuth2AccessTokenFetcherImpl fetcher_; }; // These four tests time out, see http://crbug.com/113446. -TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_GetAccessTokenRequestFailure) { +TEST_F(OAuth2AccessTokenFetcherImplTest, + DISABLED_GetAccessTokenRequestFailure) { TestURLFetcher* url_fetcher = SetupGetAccessToken(false, 0, std::string()); EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); - fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.Start("client_id", "client_secret", ScopeList()); fetcher_.OnURLFetchComplete(url_fetcher); } -TEST_F(OAuth2AccessTokenFetcherTest, +TEST_F(OAuth2AccessTokenFetcherImplTest, DISABLED_GetAccessTokenResponseCodeFailure) { TestURLFetcher* url_fetcher = SetupGetAccessToken(true, net::HTTP_FORBIDDEN, std::string()); EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); - fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.Start("client_id", "client_secret", ScopeList()); fetcher_.OnURLFetchComplete(url_fetcher); } -TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_Success) { - TestURLFetcher* url_fetcher = SetupGetAccessToken( - true, net::HTTP_OK, kValidTokenResponse); +TEST_F(OAuth2AccessTokenFetcherImplTest, DISABLED_Success) { + TestURLFetcher* url_fetcher = + SetupGetAccessToken(true, net::HTTP_OK, kValidTokenResponse); EXPECT_CALL(consumer_, OnGetTokenSuccess("at1", _)).Times(1); - fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.Start("client_id", "client_secret", ScopeList()); fetcher_.OnURLFetchComplete(url_fetcher); } -TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_MakeGetAccessTokenBody) { +TEST_F(OAuth2AccessTokenFetcherImplTest, DISABLED_MakeGetAccessTokenBody) { { // No scope. std::string body = "client_id=cid1&" "client_secret=cs1&" "grant_type=refresh_token&" "refresh_token=rt1"; - EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( - "cid1", "cs1", "rt1", ScopeList())); + EXPECT_EQ(body, + OAuth2AccessTokenFetcherImpl::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", ScopeList())); } { // One scope. @@ -166,8 +167,9 @@ TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_MakeGetAccessTokenBody) { "scope=https://www.googleapis.com/foo"; ScopeList scopes; scopes.push_back("https://www.googleapis.com/foo"); - EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( - "cid1", "cs1", "rt1", scopes)); + EXPECT_EQ(body, + OAuth2AccessTokenFetcherImpl::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", scopes)); } { // Multiple scopes. @@ -183,8 +185,9 @@ TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_MakeGetAccessTokenBody) { scopes.push_back("https://www.googleapis.com/foo"); scopes.push_back("https://www.googleapis.com/bar"); scopes.push_back("https://www.googleapis.com/baz"); - EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( - "cid1", "cs1", "rt1", scopes)); + EXPECT_EQ(body, + OAuth2AccessTokenFetcherImpl::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", scopes)); } } @@ -193,15 +196,16 @@ TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_MakeGetAccessTokenBody) { #define MAYBE_ParseGetAccessTokenResponse DISABLED_ParseGetAccessTokenResponse #else #define MAYBE_ParseGetAccessTokenResponse ParseGetAccessTokenResponse -#endif // defined(OS_WIN) -TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { +#endif // defined(OS_WIN) +TEST_F(OAuth2AccessTokenFetcherImplTest, MAYBE_ParseGetAccessTokenResponse) { { // No body. TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); std::string at; int expires_in; - EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( - &url_fetcher, &at, &expires_in)); + EXPECT_FALSE( + OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenSuccessResponse( + &url_fetcher, &at, &expires_in)); EXPECT_TRUE(at.empty()); } { // Bad json. @@ -210,8 +214,9 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { std::string at; int expires_in; - EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( - &url_fetcher, &at, &expires_in)); + EXPECT_FALSE( + OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenSuccessResponse( + &url_fetcher, &at, &expires_in)); EXPECT_TRUE(at.empty()); } { // Valid json: access token missing. @@ -220,8 +225,9 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { std::string at; int expires_in; - EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( - &url_fetcher, &at, &expires_in)); + EXPECT_FALSE( + OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenSuccessResponse( + &url_fetcher, &at, &expires_in)); EXPECT_TRUE(at.empty()); } { // Valid json: all good. @@ -230,27 +236,30 @@ TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { std::string at; int expires_in; - EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenSuccessResponse( - &url_fetcher, &at, &expires_in)); + EXPECT_TRUE( + OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenSuccessResponse( + &url_fetcher, &at, &expires_in)); EXPECT_EQ("at1", at); EXPECT_EQ(3600, expires_in); } { // Valid json: invalid error response. - TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); - url_fetcher.SetResponseString(kTokenResponseNoAccessToken); - - std::string error; - EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse( - &url_fetcher, &error)); - EXPECT_TRUE(error.empty()); - } - { // Valid json: error response. - TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); - url_fetcher.SetResponseString(kValidFailureTokenResponse); - - std::string error; - EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenFailureResponse( - &url_fetcher, &error)); - EXPECT_EQ("invalid_grant", error); - } + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kTokenResponseNoAccessToken); + + std::string error; + EXPECT_FALSE( + OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenFailureResponse( + &url_fetcher, &error)); + EXPECT_TRUE(error.empty()); + } + { // Valid json: error response. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kValidFailureTokenResponse); + + std::string error; + EXPECT_TRUE( + OAuth2AccessTokenFetcherImpl::ParseGetAccessTokenFailureResponse( + &url_fetcher, &error)); + EXPECT_EQ("invalid_grant", error); + } } diff --git a/chromium/google_apis/gaia/oauth2_api_call_flow.cc b/chromium/google_apis/gaia/oauth2_api_call_flow.cc index 4c4940d9853..08312ebe5d3 100644 --- a/chromium/google_apis/gaia/oauth2_api_call_flow.cc +++ b/chromium/google_apis/gaia/oauth2_api_call_flow.cc @@ -10,6 +10,7 @@ #include "base/basictypes.h" #include "base/strings/stringprintf.h" #include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/oauth2_access_token_fetcher_impl.h" #include "net/base/escape.h" #include "net/base/load_flags.h" #include "net/http/http_status_code.h" @@ -108,7 +109,6 @@ void OAuth2ApiCallFlow::BeginMintAccessToken() { oauth2_access_token_fetcher_->Start( GaiaUrls::GetInstance()->oauth2_chrome_client_id(), GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), - refresh_token_, scopes_); } @@ -126,7 +126,7 @@ void OAuth2ApiCallFlow::EndMintAccessToken( } OAuth2AccessTokenFetcher* OAuth2ApiCallFlow::CreateAccessTokenFetcher() { - return new OAuth2AccessTokenFetcher(this, context_); + return new OAuth2AccessTokenFetcherImpl(this, context_, refresh_token_); } void OAuth2ApiCallFlow::OnURLFetchComplete(const net::URLFetcher* source) { diff --git a/chromium/google_apis/gaia/oauth2_api_call_flow.h b/chromium/google_apis/gaia/oauth2_api_call_flow.h index fccd1e753be..b29bc8a1390 100644 --- a/chromium/google_apis/gaia/oauth2_api_call_flow.h +++ b/chromium/google_apis/gaia/oauth2_api_call_flow.h @@ -7,10 +7,12 @@ #include <string> +#include "base/gtest_prod_util.h" #include "base/memory/scoped_ptr.h" #include "google_apis/gaia/oauth2_access_token_consumer.h" #include "google_apis/gaia/oauth2_access_token_fetcher.h" #include "net/url_request/url_fetcher_delegate.h" +#include "url/gurl.h" class GoogleServiceAuthError; class OAuth2MintTokenFlowTest; diff --git a/chromium/google_apis/gaia/oauth2_api_call_flow_unittest.cc b/chromium/google_apis/gaia/oauth2_api_call_flow_unittest.cc index d56a613eded..754b15e95fb 100644 --- a/chromium/google_apis/gaia/oauth2_api_call_flow_unittest.cc +++ b/chromium/google_apis/gaia/oauth2_api_call_flow_unittest.cc @@ -13,7 +13,7 @@ #include "google_apis/gaia/gaia_urls.h" #include "google_apis/gaia/google_service_auth_error.h" #include "google_apis/gaia/oauth2_access_token_consumer.h" -#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "google_apis/gaia/oauth2_access_token_fetcher_impl.h" #include "google_apis/gaia/oauth2_api_call_flow.h" #include "net/http/http_request_headers.h" #include "net/http/http_status_code.h" @@ -70,18 +70,18 @@ class MockUrlFetcherFactory : public ScopedURLFetcherFactory, URLFetcherDelegate* d)); }; -class MockAccessTokenFetcher : public OAuth2AccessTokenFetcher { +class MockAccessTokenFetcher : public OAuth2AccessTokenFetcherImpl { public: MockAccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, - net::URLRequestContextGetter* getter) - : OAuth2AccessTokenFetcher(consumer, getter) {} + net::URLRequestContextGetter* getter, + const std::string& refresh_token) + : OAuth2AccessTokenFetcherImpl(consumer, getter, refresh_token) {} ~MockAccessTokenFetcher() {} - MOCK_METHOD4(Start, - void (const std::string& client_id, - const std::string& client_secret, - const std::string& refresh_token, - const std::vector<std::string>& scopes)); + MOCK_METHOD3(Start, + void(const std::string& client_id, + const std::string& client_secret, + const std::vector<std::string>& scopes)); }; class MockApiCallFlow : public OAuth2ApiCallFlow { @@ -110,13 +110,11 @@ class MockApiCallFlow : public OAuth2ApiCallFlow { class OAuth2ApiCallFlowTest : public testing::Test { protected: - void SetupAccessTokenFetcher( - const std::string& rt, const std::vector<std::string>& scopes) { + void SetupAccessTokenFetcher(const std::vector<std::string>& scopes) { EXPECT_CALL(*access_token_fetcher_, - Start(GaiaUrls::GetInstance()->oauth2_chrome_client_id(), - GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), - rt, scopes)) - .Times(1); + Start(GaiaUrls::GetInstance()->oauth2_chrome_client_id(), + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), + scopes)).Times(1); EXPECT_CALL(*flow_, CreateAccessTokenFetcher()) .WillOnce(Return(access_token_fetcher_.release())); } @@ -146,8 +144,8 @@ class OAuth2ApiCallFlowTest : public testing::Test { message_loop_.message_loop_proxy()); flow_.reset(new MockApiCallFlow( request_context_getter, refresh_token, access_token, scopes)); - access_token_fetcher_.reset( - new MockAccessTokenFetcher(flow_.get(), request_context_getter)); + access_token_fetcher_.reset(new MockAccessTokenFetcher( + flow_.get(), request_context_getter, refresh_token)); } TestURLFetcher* SetupApiCall(bool succeeds, net::HttpStatusCode status) { @@ -188,7 +186,7 @@ TEST_F(OAuth2ApiCallFlowTest, SecondApiCallSucceeds) { CreateFlow(rt, at, scopes); TestURLFetcher* url_fetcher1 = SetupApiCall(true, net::HTTP_UNAUTHORIZED); flow_->Start(); - SetupAccessTokenFetcher(rt, scopes); + SetupAccessTokenFetcher(scopes); flow_->OnURLFetchComplete(url_fetcher1); TestURLFetcher* url_fetcher2 = SetupApiCall(true, net::HTTP_OK); EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher2)); @@ -206,7 +204,7 @@ TEST_F(OAuth2ApiCallFlowTest, SecondApiCallFails) { CreateFlow(rt, at, scopes); TestURLFetcher* url_fetcher1 = SetupApiCall(true, net::HTTP_UNAUTHORIZED); flow_->Start(); - SetupAccessTokenFetcher(rt, scopes); + SetupAccessTokenFetcher(scopes); flow_->OnURLFetchComplete(url_fetcher1); TestURLFetcher* url_fetcher2 = SetupApiCall(false, net::HTTP_UNAUTHORIZED); EXPECT_CALL(*flow_, ProcessApiCallFailure(url_fetcher2)); @@ -224,7 +222,7 @@ TEST_F(OAuth2ApiCallFlowTest, NewTokenGenerationFails) { CreateFlow(rt, at, scopes); TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_UNAUTHORIZED); flow_->Start(); - SetupAccessTokenFetcher(rt, scopes); + SetupAccessTokenFetcher(scopes); flow_->OnURLFetchComplete(url_fetcher); GoogleServiceAuthError error( GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); @@ -238,7 +236,7 @@ TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenFirstApiCallSucceeds) { std::vector<std::string> scopes(CreateTestScopes()); CreateFlow(rt, std::string(), scopes); - SetupAccessTokenFetcher(rt, scopes); + SetupAccessTokenFetcher(scopes); TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_OK); EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher)); flow_->Start(); @@ -254,7 +252,7 @@ TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenApiCallFails) { std::vector<std::string> scopes(CreateTestScopes()); CreateFlow(rt, std::string(), scopes); - SetupAccessTokenFetcher(rt, scopes); + SetupAccessTokenFetcher(scopes); TestURLFetcher* url_fetcher = SetupApiCall(false, net::HTTP_BAD_GATEWAY); EXPECT_CALL(*flow_, ProcessApiCallFailure(url_fetcher)); flow_->Start(); @@ -270,7 +268,7 @@ TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenNewTokenGenerationFails) { std::vector<std::string> scopes(CreateTestScopes()); CreateFlow(rt, std::string(), scopes); - SetupAccessTokenFetcher(rt, scopes); + SetupAccessTokenFetcher(scopes); GoogleServiceAuthError error( GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); EXPECT_CALL(*flow_, ProcessMintAccessTokenFailure(error)); diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.cc b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc index a1e3ff3cfeb..2318b50576f 100644 --- a/chromium/google_apis/gaia/oauth2_mint_token_flow.cc +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc @@ -65,15 +65,15 @@ static GoogleServiceAuthError CreateAuthError(const net::URLFetcher* source) { std::string response_body; source->GetResponseAsString(&response_body); - scoped_ptr<Value> value(base::JSONReader::Read(response_body)); - DictionaryValue* response; + scoped_ptr<base::Value> value(base::JSONReader::Read(response_body)); + base::DictionaryValue* response; if (!value.get() || !value->GetAsDictionary(&response)) { return GoogleServiceAuthError::FromUnexpectedServiceResponse( base::StringPrintf( "Not able to parse a JSON object from a service response. " "HTTP Status of the response is: %d", source->GetResponseCode())); } - DictionaryValue* error; + base::DictionaryValue* error; if (!response->GetDictionary(kError, &error)) { return GoogleServiceAuthError::FromUnexpectedServiceResponse( "Not able to find a detailed error in a service response."); @@ -266,12 +266,12 @@ bool OAuth2MintTokenFlow::ParseIssueAdviceResponse( break; } - TrimWhitespace(entry.description, TRIM_ALL, &entry.description); + base::TrimWhitespace(entry.description, base::TRIM_ALL, &entry.description); static const base::string16 detail_separators = - ASCIIToUTF16(kDetailSeparators); + base::ASCIIToUTF16(kDetailSeparators); Tokenize(detail, detail_separators, &entry.details); for (size_t i = 0; i < entry.details.size(); i++) - TrimWhitespace(entry.details[i], TRIM_ALL, &entry.details[i]); + base::TrimWhitespace(entry.details[i], base::TRIM_ALL, &entry.details[i]); issue_advice->push_back(entry); } diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.h b/chromium/google_apis/gaia/oauth2_mint_token_flow.h index 06c823ce4bc..6c53c945648 100644 --- a/chromium/google_apis/gaia/oauth2_mint_token_flow.h +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.h @@ -8,9 +8,11 @@ #include <string> #include <vector> +#include "base/gtest_prod_util.h" #include "base/memory/weak_ptr.h" #include "base/strings/string16.h" #include "google_apis/gaia/oauth2_api_call_flow.h" +#include "url/gurl.h" class GoogleServiceAuthError; class OAuth2MintTokenFlowTest; diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow_unittest.cc b/chromium/google_apis/gaia/oauth2_mint_token_flow_unittest.cc index 7cd3d672c62..8fd359f8d50 100644 --- a/chromium/google_apis/gaia/oauth2_mint_token_flow_unittest.cc +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow_unittest.cc @@ -111,13 +111,13 @@ std::vector<std::string> CreateTestScopes() { static IssueAdviceInfo CreateIssueAdvice() { IssueAdviceInfo ia; IssueAdviceInfoEntry e1; - e1.description = ASCIIToUTF16("Manage your calendars"); - e1.details.push_back(ASCIIToUTF16("View and manage your calendars")); + e1.description = base::ASCIIToUTF16("Manage your calendars"); + e1.details.push_back(base::ASCIIToUTF16("View and manage your calendars")); ia.push_back(e1); IssueAdviceInfoEntry e2; - e2.description = ASCIIToUTF16("Manage your documents"); - e2.details.push_back(ASCIIToUTF16("View your documents")); - e2.details.push_back(ASCIIToUTF16("Upload new documents")); + e2.description = base::ASCIIToUTF16("Manage your documents"); + e2.details.push_back(base::ASCIIToUTF16("View your documents")); + e2.details.push_back(base::ASCIIToUTF16("Upload new documents")); ia.push_back(e2); return ia; } @@ -170,9 +170,9 @@ class OAuth2MintTokenFlowTest : public testing::Test { // Helper to parse the given string to DictionaryValue. static base::DictionaryValue* ParseJson(const std::string& str) { - scoped_ptr<Value> value(base::JSONReader::Read(str)); + scoped_ptr<base::Value> value(base::JSONReader::Read(str)); EXPECT_TRUE(value.get()); - EXPECT_EQ(Value::TYPE_DICTIONARY, value->GetType()); + EXPECT_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); return static_cast<base::DictionaryValue*>(value.release()); } diff --git a/chromium/google_apis/gaia/oauth2_token_service.cc b/chromium/google_apis/gaia/oauth2_token_service.cc index 9684281097b..7a183b6e6c5 100644 --- a/chromium/google_apis/gaia/oauth2_token_service.cc +++ b/chromium/google_apis/gaia/oauth2_token_service.cc @@ -15,6 +15,7 @@ #include "base/timer/timer.h" #include "google_apis/gaia/gaia_urls.h" #include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_access_token_fetcher_impl.h" #include "net/url_request/url_request_context_getter.h" int OAuth2TokenService::max_fetch_retry_num_ = 5; @@ -61,6 +62,10 @@ std::string OAuth2TokenService::RequestImpl::GetAccountId() const { return account_id_; } +std::string OAuth2TokenService::RequestImpl::GetConsumerId() const { + return consumer_->id(); +} + void OAuth2TokenService::RequestImpl::InformConsumer( const GoogleServiceAuthError& error, const std::string& access_token, @@ -72,10 +77,8 @@ void OAuth2TokenService::RequestImpl::InformConsumer( consumer_->OnGetTokenFailure(this, error); } -// Class that fetches an OAuth2 access token for a given set of scopes and -// OAuth2 refresh token. - -// Class that fetches OAuth2 access tokens for given scopes and refresh token. +// Class that fetches an OAuth2 access token for a given account id and set of +// scopes. // // It aims to meet OAuth2TokenService's requirements on token fetching. Retry // mechanism is used to handle failures. @@ -105,14 +108,13 @@ void OAuth2TokenService::RequestImpl::InformConsumer( class OAuth2TokenService::Fetcher : public OAuth2AccessTokenConsumer { public: // Creates a Fetcher and starts fetching an OAuth2 access token for - // |refresh_token| and |scopes| in the request context obtained by |getter|. + // |account_id| and |scopes| in the request context obtained by |getter|. // The given |oauth2_token_service| will be informed when fetching is done. static Fetcher* CreateAndStart(OAuth2TokenService* oauth2_token_service, const std::string& account_id, net::URLRequestContextGetter* getter, const std::string& client_id, const std::string& client_secret, - const std::string& refresh_token, const ScopeSet& scopes, base::WeakPtr<RequestImpl> waiting_request); virtual ~Fetcher(); @@ -123,10 +125,13 @@ class OAuth2TokenService::Fetcher : public OAuth2AccessTokenConsumer { // Returns count of waiting requests. size_t GetWaitingRequestCount() const; + const std::vector<base::WeakPtr<RequestImpl> >& waiting_requests() const { + return waiting_requests_; + } + void Cancel(); const ScopeSet& GetScopeSet() const; - const std::string& GetRefreshToken() const; const std::string& GetClientId() const; const std::string& GetAccountId() const; @@ -146,7 +151,6 @@ class OAuth2TokenService::Fetcher : public OAuth2AccessTokenConsumer { net::URLRequestContextGetter* getter, const std::string& client_id, const std::string& client_secret, - const std::string& refresh_token, const OAuth2TokenService::ScopeSet& scopes, base::WeakPtr<RequestImpl> waiting_request); void Start(); @@ -162,7 +166,6 @@ class OAuth2TokenService::Fetcher : public OAuth2AccessTokenConsumer { OAuth2TokenService* const oauth2_token_service_; scoped_refptr<net::URLRequestContextGetter> getter_; const std::string account_id_; - const std::string refresh_token_; const ScopeSet scopes_; std::vector<base::WeakPtr<RequestImpl> > waiting_requests_; @@ -191,7 +194,6 @@ OAuth2TokenService::Fetcher* OAuth2TokenService::Fetcher::CreateAndStart( net::URLRequestContextGetter* getter, const std::string& client_id, const std::string& client_secret, - const std::string& refresh_token, const OAuth2TokenService::ScopeSet& scopes, base::WeakPtr<RequestImpl> waiting_request) { OAuth2TokenService::Fetcher* fetcher = new Fetcher( @@ -200,7 +202,6 @@ OAuth2TokenService::Fetcher* OAuth2TokenService::Fetcher::CreateAndStart( getter, client_id, client_secret, - refresh_token, scopes, waiting_request); fetcher->Start(); @@ -213,13 +214,11 @@ OAuth2TokenService::Fetcher::Fetcher( net::URLRequestContextGetter* getter, const std::string& client_id, const std::string& client_secret, - const std::string& refresh_token, const OAuth2TokenService::ScopeSet& scopes, base::WeakPtr<RequestImpl> waiting_request) : oauth2_token_service_(oauth2_token_service), getter_(getter), account_id_(account_id), - refresh_token_(refresh_token), scopes_(scopes), retry_number_(0), error_(GoogleServiceAuthError::SERVICE_UNAVAILABLE), @@ -227,7 +226,6 @@ OAuth2TokenService::Fetcher::Fetcher( client_secret_(client_secret) { DCHECK(oauth2_token_service_); DCHECK(getter_.get()); - DCHECK(refresh_token_.length()); waiting_requests_.push_back(waiting_request); } @@ -238,10 +236,11 @@ OAuth2TokenService::Fetcher::~Fetcher() { } void OAuth2TokenService::Fetcher::Start() { - fetcher_.reset(new OAuth2AccessTokenFetcher(this, getter_.get())); + fetcher_.reset(oauth2_token_service_->CreateAccessTokenFetcher( + account_id_, getter_.get(), this)); + DCHECK(fetcher_); fetcher_->Start(client_id_, client_secret_, - refresh_token_, std::vector<std::string>(scopes_.begin(), scopes_.end())); retry_timer_.Stop(); } @@ -273,11 +272,12 @@ void OAuth2TokenService::Fetcher::OnGetTokenFailure( fetcher_.reset(); if (ShouldRetry(error) && retry_number_ < max_fetch_retry_num_) { - int64 backoff = ComputeExponentialBackOffMilliseconds(retry_number_); + base::TimeDelta backoff = base::TimeDelta::FromMilliseconds( + ComputeExponentialBackOffMilliseconds(retry_number_)); ++retry_number_; retry_timer_.Stop(); retry_timer_.Start(FROM_HERE, - base::TimeDelta::FromMilliseconds(backoff), + backoff, this, &OAuth2TokenService::Fetcher::Start); return; @@ -346,10 +346,6 @@ const OAuth2TokenService::ScopeSet& OAuth2TokenService::Fetcher::GetScopeSet() return scopes_; } -const std::string& OAuth2TokenService::Fetcher::GetRefreshToken() const { - return refresh_token_; -} - const std::string& OAuth2TokenService::Fetcher::GetClientId() const { return client_id_; } @@ -364,8 +360,8 @@ OAuth2TokenService::Request::Request() { OAuth2TokenService::Request::~Request() { } -OAuth2TokenService::Consumer::Consumer() { -} +OAuth2TokenService::Consumer::Consumer(const std::string& id) + : id_(id) {} OAuth2TokenService::Consumer::~Consumer() { } @@ -387,10 +383,13 @@ void OAuth2TokenService::RemoveObserver(Observer* observer) { observer_list_.RemoveObserver(observer); } -bool OAuth2TokenService::RefreshTokenIsAvailable( - const std::string& account_id) { - DCHECK(CalledOnValidThread()); - return !GetRefreshToken(account_id).empty(); +void OAuth2TokenService::AddDiagnosticsObserver(DiagnosticsObserver* observer) { + diagnostics_observer_list_.AddObserver(observer); +} + +void OAuth2TokenService::RemoveDiagnosticsObserver( + DiagnosticsObserver* observer) { + diagnostics_observer_list_.RemoveObserver(observer); } std::vector<std::string> OAuth2TokenService::GetAccounts() { @@ -451,13 +450,24 @@ OAuth2TokenService::StartRequestForClientWithContext( Consumer* consumer) { DCHECK(CalledOnValidThread()); - scoped_ptr<RequestImpl> request = CreateRequest(account_id, consumer); + scoped_ptr<RequestImpl> request(new RequestImpl(account_id, consumer)); + FOR_EACH_OBSERVER(DiagnosticsObserver, diagnostics_observer_list_, + OnAccessTokenRequested(account_id, + consumer->id(), + scopes)); if (!RefreshTokenIsAvailable(account_id)) { + GoogleServiceAuthError error(GoogleServiceAuthError::USER_NOT_SIGNED_UP); + + FOR_EACH_OBSERVER(DiagnosticsObserver, diagnostics_observer_list_, + OnFetchAccessTokenComplete( + account_id, consumer->id(), scopes, error, + base::Time())); + base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind( &RequestImpl::InformConsumer, request->AsWeakPtr(), - GoogleServiceAuthError(GoogleServiceAuthError::USER_NOT_SIGNED_UP), + error, std::string(), base::Time())); return request.PassAs<Request>(); @@ -479,20 +489,12 @@ OAuth2TokenService::StartRequestForClientWithContext( return request.PassAs<Request>(); } -scoped_ptr<OAuth2TokenService::RequestImpl> OAuth2TokenService::CreateRequest( - const std::string& account_id, - Consumer* consumer) { - return scoped_ptr<RequestImpl>(new RequestImpl(account_id, consumer)); -} - void OAuth2TokenService::FetchOAuth2Token(RequestImpl* request, const std::string& account_id, net::URLRequestContextGetter* getter, const std::string& client_id, const std::string& client_secret, const ScopeSet& scopes) { - std::string refresh_token = GetRefreshToken(account_id); - // If there is already a pending fetcher for |scopes| and |account_id|, // simply register this |request| for those results rather than starting // a new fetcher. @@ -512,7 +514,6 @@ void OAuth2TokenService::FetchOAuth2Token(RequestImpl* request, getter, client_id, client_secret, - refresh_token, scopes, request->AsWeakPtr()); } @@ -523,6 +524,13 @@ void OAuth2TokenService::StartCacheLookupRequest( OAuth2TokenService::Consumer* consumer) { CHECK(HasCacheEntry(request_parameters)); const CacheEntry* cache_entry = GetCacheEntry(request_parameters); + FOR_EACH_OBSERVER(DiagnosticsObserver, diagnostics_observer_list_, + OnFetchAccessTokenComplete( + request_parameters.account_id, + consumer->id(), + request_parameters.scopes, + GoogleServiceAuthError::AuthErrorNone(), + cache_entry->expiration_date)); base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind( &RequestImpl::InformConsumer, request->AsWeakPtr(), @@ -594,11 +602,26 @@ void OAuth2TokenService::OnFetchComplete(Fetcher* fetcher) { // By (1), |fetcher| is created by this service. // Then by (2), |fetcher| is recorded in |pending_fetchers_|. // Then by (3), |fetcher_| is mapped to its refresh token and ScopeSet. + RequestParameters request_param(fetcher->GetClientId(), + fetcher->GetAccountId(), + fetcher->GetScopeSet()); + + const OAuth2TokenService::CacheEntry* entry = GetCacheEntry(request_param); + const std::vector<base::WeakPtr<RequestImpl> >& requests = + fetcher->waiting_requests(); + for (size_t i = 0; i < requests.size(); ++i) { + const RequestImpl* req = requests[i].get(); + if (req) { + FOR_EACH_OBSERVER(DiagnosticsObserver, diagnostics_observer_list_, + OnFetchAccessTokenComplete( + req->GetAccountId(), req->GetConsumerId(), + fetcher->GetScopeSet(), fetcher->error(), + entry ? entry->expiration_date : base::Time())); + } + } + std::map<RequestParameters, Fetcher*>::iterator iter = - pending_fetchers_.find(RequestParameters( - fetcher->GetClientId(), - fetcher->GetAccountId(), - fetcher->GetScopeSet())); + pending_fetchers_.find(request_param); DCHECK(iter != pending_fetchers_.end()); DCHECK_EQ(fetcher, iter->second); pending_fetchers_.erase(iter); @@ -630,6 +653,9 @@ bool OAuth2TokenService::RemoveCacheEntry( TokenCache::iterator token_iterator = token_cache_.find(request_parameters); if (token_iterator != token_cache_.end() && token_iterator->second.access_token == token_to_remove) { + FOR_EACH_OBSERVER(DiagnosticsObserver, diagnostics_observer_list_, + OnTokenRemoved(request_parameters.account_id, + request_parameters.scopes)); token_cache_.erase(token_iterator); return true; } @@ -659,6 +685,13 @@ void OAuth2TokenService::UpdateAuthError( void OAuth2TokenService::ClearCache() { DCHECK(CalledOnValidThread()); + for (TokenCache::iterator iter = token_cache_.begin(); + iter != token_cache_.end(); ++iter) { + FOR_EACH_OBSERVER(DiagnosticsObserver, diagnostics_observer_list_, + OnTokenRemoved(iter->first.account_id, + iter->first.scopes)); + } + token_cache_.clear(); } @@ -668,6 +701,8 @@ void OAuth2TokenService::ClearCacheForAccount(const std::string& account_id) { iter != token_cache_.end(); /* iter incremented in body */) { if (iter->first.account_id == account_id) { + FOR_EACH_OBSERVER(DiagnosticsObserver, diagnostics_observer_list_, + OnTokenRemoved(account_id, iter->first.scopes)); token_cache_.erase(iter++); } else { ++iter; diff --git a/chromium/google_apis/gaia/oauth2_token_service.h b/chromium/google_apis/gaia/oauth2_token_service.h index 1745315abf4..685b9dbb340 100644 --- a/chromium/google_apis/gaia/oauth2_token_service.h +++ b/chromium/google_apis/gaia/oauth2_token_service.h @@ -26,6 +26,7 @@ class URLRequestContextGetter; } class GoogleServiceAuthError; +class OAuth2AccessTokenFetcher; // Abstract base class for a service that fetches and caches OAuth2 access // tokens. Concrete subclasses should implement GetRefreshToken to return @@ -52,6 +53,9 @@ class GoogleServiceAuthError; // delete the request even once the callback has been invoked. class OAuth2TokenService : public base::NonThreadSafe { public: + // A set of scopes in OAuth2 authentication. + typedef std::set<std::string> ScopeSet; + // Class representing a request that fetches an OAuth2 access token. class Request { public: @@ -65,8 +69,11 @@ class OAuth2TokenService : public base::NonThreadSafe { // which will be called back when the request completes. class Consumer { public: - Consumer(); + Consumer(const std::string& id); virtual ~Consumer(); + + std::string id() const { return id_; } + // |request| is a Request that is started by this consumer and has // completed. virtual void OnGetTokenSuccess(const Request* request, @@ -74,10 +81,12 @@ class OAuth2TokenService : public base::NonThreadSafe { const base::Time& expiration_time) = 0; virtual void OnGetTokenFailure(const Request* request, const GoogleServiceAuthError& error) = 0; + private: + std::string id_; }; - // Classes that want to listen for token availability should implement this - // interface and register with the AddObserver() call. + // Classes that want to listen for refresh token availability should + // implement this interface and register with the AddObserver() call. class Observer { public: // Called whenever a new login-scoped refresh token is available for @@ -91,12 +100,31 @@ class OAuth2TokenService : public base::NonThreadSafe { // Called after all refresh tokens are loaded during OAuth2TokenService // startup. virtual void OnRefreshTokensLoaded() {} + protected: virtual ~Observer() {} }; - // A set of scopes in OAuth2 authentication. - typedef std::set<std::string> ScopeSet; + // Classes that want to monitor status of access token and access token + // request should implement this interface and register with the + // AddDiagnosticsObserver() call. + class DiagnosticsObserver { + public: + // Called when receiving request for access token. + virtual void OnAccessTokenRequested(const std::string& account_id, + const std::string& consumer_id, + const ScopeSet& scopes) = 0; + // Called when access token fetching finished successfully or + // unsuccessfully. |expiration_time| are only valid with + // successful completion. + virtual void OnFetchAccessTokenComplete(const std::string& account_id, + const std::string& consumer_id, + const ScopeSet& scopes, + GoogleServiceAuthError error, + base::Time expiration_time) = 0; + virtual void OnTokenRemoved(const std::string& account_id, + const ScopeSet& scopes) = 0; + }; OAuth2TokenService(); virtual ~OAuth2TokenService(); @@ -105,6 +133,10 @@ class OAuth2TokenService : public base::NonThreadSafe { void AddObserver(Observer* observer); void RemoveObserver(Observer* observer); + // Add or remove observers of this token service. + void AddDiagnosticsObserver(DiagnosticsObserver* observer); + void RemoveDiagnosticsObserver(DiagnosticsObserver* observer); + // Checks in the cache for a valid access token for a specified |account_id| // and |scopes|, and if not found starts a request for an OAuth2 access token // using the OAuth2 refresh token maintained by this instance for that @@ -141,7 +173,7 @@ class OAuth2TokenService : public base::NonThreadSafe { // Returns true if a refresh token exists for |account_id|. If false, calls to // |StartRequest| will result in a Consumer::OnGetTokenFailure callback. - virtual bool RefreshTokenIsAvailable(const std::string& account_id); + virtual bool RefreshTokenIsAvailable(const std::string& account_id) const = 0; // Mark an OAuth2 |access_token| issued for |account_id| and |scopes| as // invalid. This should be done if the token was received from this class, @@ -184,6 +216,8 @@ class OAuth2TokenService : public base::NonThreadSafe { // Overridden from Request: virtual std::string GetAccountId() const OVERRIDE; + std::string GetConsumerId() const; + // Informs |consumer_| that this request is completed. void InformConsumer(const GoogleServiceAuthError& error, const std::string& access_token, @@ -195,10 +229,6 @@ class OAuth2TokenService : public base::NonThreadSafe { Consumer* const consumer_; }; - // Subclasses should return the maintained refresh token for |account_id|. - // If no token is available, return an empty string. - virtual std::string GetRefreshToken(const std::string& account_id) = 0; - // Subclasses can override if they want to report errors to the user. virtual void UpdateAuthError( const std::string& account_id, @@ -232,13 +262,6 @@ class OAuth2TokenService : public base::NonThreadSafe { virtual void FireRefreshTokenRevoked(const std::string& account_id); virtual void FireRefreshTokensLoaded(); - // Creates a request implementation. Can be overriden by derived classes to - // provide additional control of token consumption. |consumer| will outlive - // the created request. - virtual scoped_ptr<RequestImpl> CreateRequest( - const std::string& account_id, - Consumer* consumer); - // Fetches an OAuth token for the specified client/scopes. Virtual so it can // be overridden for tests and for platform-specific behavior on Android. virtual void FetchOAuth2Token(RequestImpl* request, @@ -248,6 +271,16 @@ class OAuth2TokenService : public base::NonThreadSafe { const std::string& client_secret, const ScopeSet& scopes); + // Creates an access token fetcher for the given account id. + // + // Subclasses should override to create an access token fetcher for the given + // |account_id|. This method is only called if subclasses use the default + // implementation of |FetchOAuth2Token|. + virtual OAuth2AccessTokenFetcher* CreateAccessTokenFetcher( + const std::string& account_id, + net::URLRequestContextGetter* getter, + OAuth2AccessTokenConsumer* consumer) = 0; + // Invalidates the |access_token| issued for |account_id|, |client_id| and // |scopes|. Virtual so it can be overriden for tests and for platform- // specifc behavior. @@ -334,10 +367,13 @@ class OAuth2TokenService : public base::NonThreadSafe { // token using these parameters. PendingFetcherMap pending_fetchers_; - // List of observers to notify when token availability changes. + // List of observers to notify when refresh token availability changes. // Makes sure list is empty on destruction. ObserverList<Observer, true> observer_list_; + // List of observers to notify when access token status changes. + ObserverList<DiagnosticsObserver, true> diagnostics_observer_list_; + // Maximum number of retries in fetching an OAuth2 access token. static int max_fetch_retry_num_; diff --git a/chromium/google_apis/gaia/oauth2_token_service_request.cc b/chromium/google_apis/gaia/oauth2_token_service_request.cc new file mode 100644 index 00000000000..67201524177 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_token_service_request.cc @@ -0,0 +1,360 @@ +// 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/gaia/oauth2_token_service_request.h" + +#include "base/bind.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/single_thread_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" + +OAuth2TokenServiceRequest::TokenServiceProvider::TokenServiceProvider() { +} + +OAuth2TokenServiceRequest::TokenServiceProvider::~TokenServiceProvider() { +} + +// Core serves as the base class for OAuth2TokenService operations. Each +// operation should be modeled as a derived type. +// +// Core is used like this: +// +// 1. Constructed on owner thread. +// +// 2. Start() is called on owner thread, which calls StartOnTokenServiceThread() +// on token service thread. +// +// 3. Request is executed. +// +// 4. Stop() is called on owner thread, which calls StopOnTokenServiceThread() +// on token service thread. +// +// 5. Core is destroyed on owner thread. +class OAuth2TokenServiceRequest::Core + : public base::NonThreadSafe, + public base::RefCountedThreadSafe<OAuth2TokenServiceRequest::Core> { + public: + // Note the thread where an instance of Core is constructed is referred to as + // the "owner thread" here. + Core(OAuth2TokenServiceRequest* owner, TokenServiceProvider* provider); + + // Starts the core. Must be called on the owner thread. + void Start(); + + // Stops the core. Must be called on the owner thread. + void Stop(); + + // Returns true if this object has been stopped. Must be called on the owner + // thread. + bool IsStopped() const; + + protected: + // Core must be destroyed on the owner thread. If data members must be + // cleaned up or destroyed on the token service thread, do so in the + // StopOnTokenServiceThread method. + virtual ~Core(); + + // Called on the token service thread. + virtual void StartOnTokenServiceThread() = 0; + + // Called on the token service thread. + virtual void StopOnTokenServiceThread() = 0; + + base::SingleThreadTaskRunner* token_service_task_runner(); + OAuth2TokenService* token_service(); + OAuth2TokenServiceRequest* owner(); + + private: + friend class base::RefCountedThreadSafe<OAuth2TokenServiceRequest::Core>; + + void DoNothing(); + + scoped_refptr<base::SingleThreadTaskRunner> token_service_task_runner_; + OAuth2TokenServiceRequest* owner_; + TokenServiceProvider* provider_; + DISALLOW_COPY_AND_ASSIGN(Core); +}; + +OAuth2TokenServiceRequest::Core::Core(OAuth2TokenServiceRequest* owner, + TokenServiceProvider* provider) + : owner_(owner), provider_(provider) { + DCHECK(owner_); + DCHECK(provider_); + token_service_task_runner_ = provider_->GetTokenServiceTaskRunner(); + DCHECK(token_service_task_runner_); +} + +OAuth2TokenServiceRequest::Core::~Core() { +} + +void OAuth2TokenServiceRequest::Core::Start() { + DCHECK(CalledOnValidThread()); + token_service_task_runner_->PostTask( + FROM_HERE, + base::Bind(&OAuth2TokenServiceRequest::Core::StartOnTokenServiceThread, + this)); +} + +void OAuth2TokenServiceRequest::Core::Stop() { + DCHECK(CalledOnValidThread()); + DCHECK(!IsStopped()); + + // Detaches |owner_| from this instance so |owner_| will be called back only + // if |Stop()| has never been called. + owner_ = NULL; + + // We are stopping and will likely be destroyed soon. Use a reply closure + // (DoNothing) to retain "this" and ensure we are destroyed in the owner + // thread, not the task runner thread. PostTaskAndReply guarantees that the + // reply closure will execute after StopOnTokenServiceThread has completed. + token_service_task_runner_->PostTaskAndReply( + FROM_HERE, + base::Bind(&OAuth2TokenServiceRequest::Core::StopOnTokenServiceThread, + this), + base::Bind(&OAuth2TokenServiceRequest::Core::DoNothing, this)); +} + +bool OAuth2TokenServiceRequest::Core::IsStopped() const { + DCHECK(CalledOnValidThread()); + return owner_ == NULL; +} + +base::SingleThreadTaskRunner* +OAuth2TokenServiceRequest::Core::token_service_task_runner() { + return token_service_task_runner_; +} + +OAuth2TokenService* OAuth2TokenServiceRequest::Core::token_service() { + DCHECK(token_service_task_runner_->BelongsToCurrentThread()); + return provider_->GetTokenService(); +} + +OAuth2TokenServiceRequest* OAuth2TokenServiceRequest::Core::owner() { + DCHECK(CalledOnValidThread()); + return owner_; +} + +void OAuth2TokenServiceRequest::Core::DoNothing() { + DCHECK(CalledOnValidThread()); +} + +namespace { + +// An implementation of Core for getting an access token. +class RequestCore : public OAuth2TokenServiceRequest::Core, + public OAuth2TokenService::Consumer { + public: + RequestCore(OAuth2TokenServiceRequest* owner, + OAuth2TokenServiceRequest::TokenServiceProvider* provider, + OAuth2TokenService::Consumer* consumer, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes); + + // OAuth2TokenService::Consumer. Must be called on the token service thread. + virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) OVERRIDE; + + private: + friend class base::RefCountedThreadSafe<RequestCore>; + + // Must be destroyed on the owner thread. + virtual ~RequestCore(); + + // Core implementation. + virtual void StartOnTokenServiceThread() OVERRIDE; + virtual void StopOnTokenServiceThread() OVERRIDE; + + void InformOwnerOnGetTokenSuccess(std::string access_token, + base::Time expiration_time); + void InformOwnerOnGetTokenFailure(GoogleServiceAuthError error); + + scoped_refptr<base::SingleThreadTaskRunner> owner_task_runner_; + OAuth2TokenService::Consumer* const consumer_; + std::string account_id_; + OAuth2TokenService::ScopeSet scopes_; + + // OAuth2TokenService request for fetching OAuth2 access token; it should be + // created, reset and accessed only on the token service thread. + scoped_ptr<OAuth2TokenService::Request> request_; + + DISALLOW_COPY_AND_ASSIGN(RequestCore); +}; + +RequestCore::RequestCore( + OAuth2TokenServiceRequest* owner, + OAuth2TokenServiceRequest::TokenServiceProvider* provider, + OAuth2TokenService::Consumer* consumer, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes) + : OAuth2TokenServiceRequest::Core(owner, provider), + OAuth2TokenService::Consumer("oauth2_token_service"), + owner_task_runner_(base::ThreadTaskRunnerHandle::Get()), + consumer_(consumer), + account_id_(account_id), + scopes_(scopes) { + DCHECK(consumer_); + DCHECK(!account_id_.empty()); + DCHECK(!scopes_.empty()); +} + +RequestCore::~RequestCore() { +} + +void RequestCore::StartOnTokenServiceThread() { + DCHECK(token_service_task_runner()->BelongsToCurrentThread()); + request_ = token_service()->StartRequest(account_id_, scopes_, this).Pass(); +} + +void RequestCore::StopOnTokenServiceThread() { + DCHECK(token_service_task_runner()->BelongsToCurrentThread()); + request_.reset(); +} + +void RequestCore::OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) { + DCHECK(token_service_task_runner()->BelongsToCurrentThread()); + DCHECK_EQ(request_.get(), request); + owner_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RequestCore::InformOwnerOnGetTokenSuccess, + this, + access_token, + expiration_time)); + request_.reset(); +} + +void RequestCore::OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) { + DCHECK(token_service_task_runner()->BelongsToCurrentThread()); + DCHECK_EQ(request_.get(), request); + owner_task_runner_->PostTask( + FROM_HERE, + base::Bind(&RequestCore::InformOwnerOnGetTokenFailure, this, error)); + request_.reset(); +} + +void RequestCore::InformOwnerOnGetTokenSuccess(std::string access_token, + base::Time expiration_time) { + DCHECK(CalledOnValidThread()); + if (!IsStopped()) { + consumer_->OnGetTokenSuccess(owner(), access_token, expiration_time); + } +} + +void RequestCore::InformOwnerOnGetTokenFailure(GoogleServiceAuthError error) { + DCHECK(CalledOnValidThread()); + if (!IsStopped()) { + consumer_->OnGetTokenFailure(owner(), error); + } +} + +// An implementation of Core for invalidating an access token. +class InvalidateCore : public OAuth2TokenServiceRequest::Core { + public: + InvalidateCore(OAuth2TokenServiceRequest* owner, + OAuth2TokenServiceRequest::TokenServiceProvider* provider, + const std::string& access_token, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes); + + private: + friend class base::RefCountedThreadSafe<InvalidateCore>; + + // Must be destroyed on the owner thread. + virtual ~InvalidateCore(); + + // Core implementation. + virtual void StartOnTokenServiceThread() OVERRIDE; + virtual void StopOnTokenServiceThread() OVERRIDE; + + std::string access_token_; + std::string account_id_; + OAuth2TokenService::ScopeSet scopes_; + + DISALLOW_COPY_AND_ASSIGN(InvalidateCore); +}; + +InvalidateCore::InvalidateCore( + OAuth2TokenServiceRequest* owner, + OAuth2TokenServiceRequest::TokenServiceProvider* provider, + const std::string& access_token, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes) + : OAuth2TokenServiceRequest::Core(owner, provider), + access_token_(access_token), + account_id_(account_id), + scopes_(scopes) { + DCHECK(!access_token_.empty()); + DCHECK(!account_id_.empty()); + DCHECK(!scopes.empty()); +} + +InvalidateCore::~InvalidateCore() { +} + +void InvalidateCore::StartOnTokenServiceThread() { + DCHECK(token_service_task_runner()->BelongsToCurrentThread()); + token_service()->InvalidateToken(account_id_, scopes_, access_token_); +} + +void InvalidateCore::StopOnTokenServiceThread() { + DCHECK(token_service_task_runner()->BelongsToCurrentThread()); + // Nothing to do. +} + +} // namespace + +// static +scoped_ptr<OAuth2TokenServiceRequest> OAuth2TokenServiceRequest::CreateAndStart( + TokenServiceProvider* provider, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes, + OAuth2TokenService::Consumer* consumer) { + scoped_ptr<OAuth2TokenServiceRequest> request( + new OAuth2TokenServiceRequest(account_id)); + scoped_refptr<Core> core( + new RequestCore(request.get(), provider, consumer, account_id, scopes)); + request->StartWithCore(core); + return request.Pass(); +} + +// static +void OAuth2TokenServiceRequest::InvalidateToken( + OAuth2TokenServiceRequest::TokenServiceProvider* provider, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes, + const std::string& access_token) { + scoped_ptr<OAuth2TokenServiceRequest> request( + new OAuth2TokenServiceRequest(account_id)); + scoped_refptr<Core> core(new InvalidateCore( + request.get(), provider, access_token, account_id, scopes)); + request->StartWithCore(core); +} + +OAuth2TokenServiceRequest::~OAuth2TokenServiceRequest() { + core_->Stop(); +} + +std::string OAuth2TokenServiceRequest::GetAccountId() const { + return account_id_; +} + +OAuth2TokenServiceRequest::OAuth2TokenServiceRequest( + const std::string& account_id) + : account_id_(account_id) { + DCHECK(!account_id_.empty()); +} + +void OAuth2TokenServiceRequest::StartWithCore(const scoped_refptr<Core>& core) { + DCHECK(core); + core_ = core; + core_->Start(); +} diff --git a/chromium/google_apis/gaia/oauth2_token_service_request.h b/chromium/google_apis/gaia/oauth2_token_service_request.h new file mode 100644 index 00000000000..972e5184b85 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_token_service_request.h @@ -0,0 +1,96 @@ +// 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_GAIA_OAUTH2_TOKEN_SERVICE_REQUEST_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_TOKEN_SERVICE_REQUEST_H_ + +#include <set> +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/single_thread_task_runner.h" +#include "base/threading/non_thread_safe.h" +#include "google_apis/gaia/oauth2_token_service.h" + +// OAuth2TokenServiceRequest represents an asynchronous request to an +// OAuth2TokenService that may live in another thread. +// +// An OAuth2TokenServiceRequest can be created and started from any thread. +class OAuth2TokenServiceRequest : public OAuth2TokenService::Request, + public base::NonThreadSafe { + public: + class Core; + + // Interface for providing an OAuth2TokenService. + class TokenServiceProvider { + public: + TokenServiceProvider(); + virtual ~TokenServiceProvider(); + + // Returns the task runner on which the token service lives. + // + // This method may be called from any thread. + virtual scoped_refptr<base::SingleThreadTaskRunner> + GetTokenServiceTaskRunner() = 0; + + // Returns a pointer to a token service. + // + // Caller does not own the token service and must not delete it. The token + // service must outlive all instances of OAuth2TokenServiceRequest. + // + // This method may only be called from the task runner returned by + // |GetTokenServiceTaskRunner|. + virtual OAuth2TokenService* GetTokenService() = 0; + }; + + // Creates and starts an access token request for |account_id| and |scopes|. + // + // |provider| is used to get the OAuth2TokenService and must outlive the + // returned request object. + // + // |account_id| must not be empty. + // + // |scopes| must not be empty. + // + // |consumer| will be invoked in the same thread that invoked CreateAndStart + // and must outlive the returned request object. Destroying the request + // object ensure that |consumer| will not be called. However, the actual + // network activities may not be canceled and the cache in OAuth2TokenService + // may be populated with the fetched results. + static scoped_ptr<OAuth2TokenServiceRequest> CreateAndStart( + TokenServiceProvider* provider, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes, + OAuth2TokenService::Consumer* consumer); + + // Invalidates |access_token| for |account_id| and |scopes|. + // + // |provider| is used to get the OAuth2TokenService and must outlive the + // returned request object. + // + // |account_id| must not be empty. + // + // |scopes| must not be empty. + static void InvalidateToken(TokenServiceProvider* provider, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes, + const std::string& access_token); + + virtual ~OAuth2TokenServiceRequest(); + + // OAuth2TokenService::Request. + virtual std::string GetAccountId() const OVERRIDE; + + private: + OAuth2TokenServiceRequest(const std::string& account_id); + + void StartWithCore(const scoped_refptr<Core>& core); + + const std::string account_id_; + scoped_refptr<Core> core_; + + DISALLOW_COPY_AND_ASSIGN(OAuth2TokenServiceRequest); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_TOKEN_SERVICE_REQUEST_H_ diff --git a/chromium/google_apis/gaia/oauth2_token_service_request_unittest.cc b/chromium/google_apis/gaia/oauth2_token_service_request_unittest.cc new file mode 100644 index 00000000000..0a86cfd0e2d --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_token_service_request_unittest.cc @@ -0,0 +1,263 @@ +// 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/gaia/oauth2_token_service_request.h" + +#include <set> +#include <string> +#include <vector> +#include "base/threading/thread.h" +#include "google_apis/gaia/fake_oauth2_token_service.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_token_service.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kAccessToken[] = "access_token"; +const char kAccountId[] = "test_user@gmail.com"; +const char kScope[] = "SCOPE"; + +class TestingOAuth2TokenServiceConsumer : public OAuth2TokenService::Consumer { + public: + TestingOAuth2TokenServiceConsumer(); + virtual ~TestingOAuth2TokenServiceConsumer(); + + virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) OVERRIDE; + + int num_get_token_success_; + int num_get_token_failure_; + std::string last_token_; + GoogleServiceAuthError last_error_; +}; + +TestingOAuth2TokenServiceConsumer::TestingOAuth2TokenServiceConsumer() + : OAuth2TokenService::Consumer("test"), + num_get_token_success_(0), + num_get_token_failure_(0), + last_error_(GoogleServiceAuthError::AuthErrorNone()) { +} + +TestingOAuth2TokenServiceConsumer::~TestingOAuth2TokenServiceConsumer() { +} + +void TestingOAuth2TokenServiceConsumer::OnGetTokenSuccess( + const OAuth2TokenService::Request* request, + const std::string& token, + const base::Time& expiration_date) { + last_token_ = token; + ++num_get_token_success_; +} + +void TestingOAuth2TokenServiceConsumer::OnGetTokenFailure( + const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) { + last_error_ = error; + ++num_get_token_failure_; +} + +// A mock implementation of an OAuth2TokenService. +// +// Use SetResponse to vary the response to token requests. +class MockOAuth2TokenService : public FakeOAuth2TokenService { + public: + MockOAuth2TokenService(); + virtual ~MockOAuth2TokenService(); + + void SetResponse(const GoogleServiceAuthError& error, + const std::string& access_token, + const base::Time& expiration); + + int num_invalidate_token() const { return num_invalidate_token_; } + + const std::string& last_token_invalidated() const { + return last_token_invalidated_; + } + + protected: + virtual void FetchOAuth2Token(RequestImpl* request, + const std::string& account_id, + net::URLRequestContextGetter* getter, + const std::string& client_id, + const std::string& client_secret, + const ScopeSet& scopes) OVERRIDE; + + virtual void InvalidateOAuth2Token(const std::string& account_id, + const std::string& client_id, + const ScopeSet& scopes, + const std::string& access_token) OVERRIDE; + + private: + GoogleServiceAuthError response_error_; + std::string response_access_token_; + base::Time response_expiration_; + int num_invalidate_token_; + std::string last_token_invalidated_; +}; + +MockOAuth2TokenService::MockOAuth2TokenService() + : response_error_(GoogleServiceAuthError::AuthErrorNone()), + response_access_token_(kAccessToken), + response_expiration_(base::Time::Max()), + num_invalidate_token_(0) { +} + +MockOAuth2TokenService::~MockOAuth2TokenService() { +} + +void MockOAuth2TokenService::SetResponse(const GoogleServiceAuthError& error, + const std::string& access_token, + const base::Time& expiration) { + response_error_ = error; + response_access_token_ = access_token; + response_expiration_ = expiration; +} + +void MockOAuth2TokenService::FetchOAuth2Token( + RequestImpl* request, + const std::string& account_id, + net::URLRequestContextGetter* getter, + const std::string& client_id, + const std::string& client_secret, + const ScopeSet& scopes) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&OAuth2TokenService::RequestImpl::InformConsumer, + request->AsWeakPtr(), + response_error_, + response_access_token_, + response_expiration_)); +} + +void MockOAuth2TokenService::InvalidateOAuth2Token( + const std::string& account_id, + const std::string& client_id, + const ScopeSet& scopes, + const std::string& access_token) { + ++num_invalidate_token_; + last_token_invalidated_ = access_token; +} + +class OAuth2TokenServiceRequestTest : public testing::Test { + public: + virtual void SetUp() OVERRIDE; + virtual void TearDown() OVERRIDE; + + protected: + class Provider : public OAuth2TokenServiceRequest::TokenServiceProvider { + public: + Provider(const scoped_refptr<base::SingleThreadTaskRunner>& task_runner, + OAuth2TokenService* token_service); + + virtual scoped_refptr<base::SingleThreadTaskRunner> + GetTokenServiceTaskRunner() OVERRIDE; + virtual OAuth2TokenService* GetTokenService() OVERRIDE; + + private: + scoped_refptr<base::SingleThreadTaskRunner> task_runner_; + OAuth2TokenService* token_service_; + }; + + base::MessageLoop ui_loop_; + OAuth2TokenService::ScopeSet scopes_; + scoped_ptr<MockOAuth2TokenService> oauth2_service_; + scoped_ptr<OAuth2TokenServiceRequest::TokenServiceProvider> provider_; + TestingOAuth2TokenServiceConsumer consumer_; +}; + +void OAuth2TokenServiceRequestTest::SetUp() { + scopes_.insert(kScope); + oauth2_service_.reset(new MockOAuth2TokenService); + oauth2_service_->AddAccount(kAccountId); + provider_.reset( + new Provider(base::MessageLoopProxy::current(), oauth2_service_.get())); +} + +void OAuth2TokenServiceRequestTest::TearDown() { + // Run the loop to execute any pending tasks that may free resources. + ui_loop_.RunUntilIdle(); +} + +OAuth2TokenServiceRequestTest::Provider::Provider( + const scoped_refptr<base::SingleThreadTaskRunner>& task_runner, + OAuth2TokenService* token_service) + : task_runner_(task_runner), token_service_(token_service) { +} + +scoped_refptr<base::SingleThreadTaskRunner> +OAuth2TokenServiceRequestTest::Provider::GetTokenServiceTaskRunner() { + return task_runner_; +} + +OAuth2TokenService* OAuth2TokenServiceRequestTest::Provider::GetTokenService() { + return token_service_; +} + +TEST_F(OAuth2TokenServiceRequestTest, CreateAndStart_Failure) { + oauth2_service_->SetResponse( + GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE), + std::string(), + base::Time()); + scoped_ptr<OAuth2TokenServiceRequest> request( + OAuth2TokenServiceRequest::CreateAndStart( + provider_.get(), kAccountId, scopes_, &consumer_)); + ui_loop_.RunUntilIdle(); + EXPECT_EQ(0, consumer_.num_get_token_success_); + EXPECT_EQ(1, consumer_.num_get_token_failure_); + EXPECT_EQ(GoogleServiceAuthError::SERVICE_UNAVAILABLE, + consumer_.last_error_.state()); + EXPECT_EQ(0, oauth2_service_->num_invalidate_token()); +} + +TEST_F(OAuth2TokenServiceRequestTest, CreateAndStart_Success) { + scoped_ptr<OAuth2TokenServiceRequest> request( + OAuth2TokenServiceRequest::CreateAndStart( + provider_.get(), kAccountId, scopes_, &consumer_)); + ui_loop_.RunUntilIdle(); + EXPECT_EQ(1, consumer_.num_get_token_success_); + EXPECT_EQ(0, consumer_.num_get_token_failure_); + EXPECT_EQ(kAccessToken, consumer_.last_token_); + EXPECT_EQ(0, oauth2_service_->num_invalidate_token()); +} + +TEST_F(OAuth2TokenServiceRequestTest, + CreateAndStart_DestroyRequestBeforeCompletes) { + scoped_ptr<OAuth2TokenServiceRequest> request( + OAuth2TokenServiceRequest::CreateAndStart( + provider_.get(), kAccountId, scopes_, &consumer_)); + request.reset(); + ui_loop_.RunUntilIdle(); + EXPECT_EQ(0, consumer_.num_get_token_success_); + EXPECT_EQ(0, consumer_.num_get_token_failure_); + EXPECT_EQ(0, oauth2_service_->num_invalidate_token()); +} + +TEST_F(OAuth2TokenServiceRequestTest, + CreateAndStart_DestroyRequestAfterCompletes) { + scoped_ptr<OAuth2TokenServiceRequest> request( + OAuth2TokenServiceRequest::CreateAndStart( + provider_.get(), kAccountId, scopes_, &consumer_)); + ui_loop_.RunUntilIdle(); + request.reset(); + EXPECT_EQ(1, consumer_.num_get_token_success_); + EXPECT_EQ(0, consumer_.num_get_token_failure_); + EXPECT_EQ(kAccessToken, consumer_.last_token_); + EXPECT_EQ(0, oauth2_service_->num_invalidate_token()); +} + +TEST_F(OAuth2TokenServiceRequestTest, InvalidateToken) { + OAuth2TokenServiceRequest::InvalidateToken( + provider_.get(), kAccountId, scopes_, kAccessToken); + ui_loop_.RunUntilIdle(); + EXPECT_EQ(0, consumer_.num_get_token_success_); + EXPECT_EQ(0, consumer_.num_get_token_failure_); + EXPECT_EQ(kAccessToken, oauth2_service_->last_token_invalidated()); + EXPECT_EQ(1, oauth2_service_->num_invalidate_token()); +} + +} // namespace diff --git a/chromium/google_apis/gaia/oauth2_token_service_test_util.cc b/chromium/google_apis/gaia/oauth2_token_service_test_util.cc index 2aae59bb41a..6dbed5f8974 100644 --- a/chromium/google_apis/gaia/oauth2_token_service_test_util.cc +++ b/chromium/google_apis/gaia/oauth2_token_service_test_util.cc @@ -20,7 +20,8 @@ std::string GetValidTokenResponse(std::string token, int expiration) { } TestingOAuth2TokenServiceConsumer::TestingOAuth2TokenServiceConsumer() - : number_of_successful_tokens_(0), + : OAuth2TokenService::Consumer("test"), + number_of_successful_tokens_(0), last_error_(GoogleServiceAuthError::AuthErrorNone()), number_of_errors_(0) { } diff --git a/chromium/google_apis/gaia/oauth2_token_service_unittest.cc b/chromium/google_apis/gaia/oauth2_token_service_unittest.cc index dcd21b12e93..b79e361c44c 100644 --- a/chromium/google_apis/gaia/oauth2_token_service_unittest.cc +++ b/chromium/google_apis/gaia/oauth2_token_service_unittest.cc @@ -9,7 +9,7 @@ #include "google_apis/gaia/gaia_constants.h" #include "google_apis/gaia/google_service_auth_error.h" #include "google_apis/gaia/oauth2_access_token_consumer.h" -#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "google_apis/gaia/oauth2_access_token_fetcher_impl.h" #include "google_apis/gaia/oauth2_token_service.h" #include "google_apis/gaia/oauth2_token_service_test_util.h" #include "net/http/http_status_code.h" @@ -56,14 +56,19 @@ class TestOAuth2TokenService : public OAuth2TokenService { // For testing: set the refresh token to be used. void set_refresh_token(const std::string& account_id, const std::string& refresh_token) { - refresh_tokens_[account_id] = refresh_token; + if (refresh_token.empty()) + refresh_tokens_.erase(account_id); + else + refresh_tokens_[account_id] = refresh_token; } - protected: - virtual std::string GetRefreshToken(const std::string& account_id) OVERRIDE { - // account_id explicitly ignored. - return refresh_tokens_[account_id]; - } + virtual bool RefreshTokenIsAvailable(const std::string& account_id) const + OVERRIDE { + std::map<std::string, std::string>::const_iterator it = + refresh_tokens_.find(account_id); + + return it != refresh_tokens_.end(); + }; private: // OAuth2TokenService implementation. @@ -71,6 +76,17 @@ class TestOAuth2TokenService : public OAuth2TokenService { return request_context_getter_.get(); } + virtual OAuth2AccessTokenFetcher* CreateAccessTokenFetcher( + const std::string& account_id, + net::URLRequestContextGetter* getter, + OAuth2AccessTokenConsumer* consumer) OVERRIDE { + std::map<std::string, std::string>::const_iterator it = + refresh_tokens_.find(account_id); + DCHECK(it != refresh_tokens_.end()); + std::string refresh_token(it->second); + return new OAuth2AccessTokenFetcherImpl(consumer, getter, refresh_token); + }; + std::map<std::string, std::string> refresh_tokens_; scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; }; diff --git a/chromium/google_apis/gaia/ubertoken_fetcher.cc b/chromium/google_apis/gaia/ubertoken_fetcher.cc new file mode 100644 index 00000000000..da0ff27c14c --- /dev/null +++ b/chromium/google_apis/gaia/ubertoken_fetcher.cc @@ -0,0 +1,63 @@ +// 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/gaia/ubertoken_fetcher.h" + +#include <vector> + +#include "base/logging.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_constants.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_token_service.h" + +UbertokenFetcher::UbertokenFetcher( + OAuth2TokenService* token_service, + UbertokenConsumer* consumer, + net::URLRequestContextGetter* request_context) + : OAuth2TokenService::Consumer("uber_token_fetcher"), + token_service_(token_service), + consumer_(consumer), + request_context_(request_context) { + DCHECK(token_service); + DCHECK(consumer); + DCHECK(request_context); +} + +UbertokenFetcher::~UbertokenFetcher() { +} + +void UbertokenFetcher::StartFetchingToken(const std::string& account_id) { + OAuth2TokenService::ScopeSet scopes; + scopes.insert(GaiaConstants::kOAuth1LoginScope); + access_token_request_ = + token_service_->StartRequest(account_id, scopes, this); +} + +void UbertokenFetcher::OnUberAuthTokenSuccess(const std::string& token) { + consumer_->OnUbertokenSuccess(token); +} + +void UbertokenFetcher::OnUberAuthTokenFailure( + const GoogleServiceAuthError& error) { + consumer_->OnUbertokenFailure(error); +} + +void UbertokenFetcher::OnGetTokenSuccess( + const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) { + access_token_request_.reset(); + gaia_auth_fetcher_.reset(new GaiaAuthFetcher(this, + GaiaConstants::kChromeSource, + request_context_)); + gaia_auth_fetcher_->StartTokenFetchForUberAuthExchange(access_token); +} + +void UbertokenFetcher::OnGetTokenFailure( + const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) { + access_token_request_.reset(); + consumer_->OnUbertokenFailure(error); +} diff --git a/chromium/google_apis/gaia/ubertoken_fetcher.h b/chromium/google_apis/gaia/ubertoken_fetcher.h new file mode 100644 index 00000000000..acc4bb4e1a7 --- /dev/null +++ b/chromium/google_apis/gaia/ubertoken_fetcher.h @@ -0,0 +1,71 @@ +// 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_GAIA_UBERTOKEN_FETCHER_H_ +#define GOOGLE_APIS_GAIA_UBERTOKEN_FETCHER_H_ + +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/gaia_auth_consumer.h" +#include "google_apis/gaia/oauth2_token_service.h" + +// Allow to retrieves an uber-auth token for the user. This class uses the +// |OAuth2TokenService| and considers that the user is already logged in. It +// will use the OAuth2 access token to generate the uber-auth token. +// +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// +// This class can handle one request at a time. + +class GaiaAuthFetcher; +class GoogleServiceAuthError; + +namespace net { +class URLRequestContextGetter; +} + +// Callback for the |UbertokenFetcher| class. +class UbertokenConsumer { + public: + UbertokenConsumer() {} + virtual ~UbertokenConsumer() {} + virtual void OnUbertokenSuccess(const std::string& token) {} + virtual void OnUbertokenFailure(const GoogleServiceAuthError& error) {} +}; + +// Allows to retrieve an uber-auth token. +class UbertokenFetcher : public GaiaAuthConsumer, + public OAuth2TokenService::Consumer { + public: + UbertokenFetcher(OAuth2TokenService* token_service, + UbertokenConsumer* consumer, + net::URLRequestContextGetter* request_context); + virtual ~UbertokenFetcher(); + + // Start fetching the token for |account_id|. + virtual void StartFetchingToken(const std::string& account_id); + + // Overriden from GaiaAuthConsumer + virtual void OnUberAuthTokenSuccess(const std::string& token) OVERRIDE; + virtual void OnUberAuthTokenFailure( + const GoogleServiceAuthError& error) OVERRIDE; + + // Overriden from OAuth2TokenService::Consumer: + virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) OVERRIDE; + + private: + OAuth2TokenService* token_service_; + UbertokenConsumer* consumer_; + net::URLRequestContextGetter* request_context_; + scoped_ptr<GaiaAuthFetcher> gaia_auth_fetcher_; + scoped_ptr<OAuth2TokenService::Request> access_token_request_; + + DISALLOW_COPY_AND_ASSIGN(UbertokenFetcher); +}; + +#endif // GOOGLE_APIS_GAIA_UBERTOKEN_FETCHER_H_ diff --git a/chromium/google_apis/gaia/ubertoken_fetcher_unittest.cc b/chromium/google_apis/gaia/ubertoken_fetcher_unittest.cc new file mode 100644 index 00000000000..8352ee8cab9 --- /dev/null +++ b/chromium/google_apis/gaia/ubertoken_fetcher_unittest.cc @@ -0,0 +1,112 @@ +// 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/gaia/ubertoken_fetcher.h" + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "google_apis/gaia/fake_oauth2_token_service.h" +#include "google_apis/gaia/gaia_constants.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 { + +const char kTestAccountId[] = "test@gmail.com"; + +class MockUbertokenConsumer : public UbertokenConsumer { + public: + MockUbertokenConsumer() + : nb_correct_token_(0), + last_error_(GoogleServiceAuthError::AuthErrorNone()), + nb_error_(0) { + } + virtual ~MockUbertokenConsumer() {} + + virtual void OnUbertokenSuccess(const std::string& token) OVERRIDE { + last_token_ = token; + ++ nb_correct_token_; + } + + virtual void OnUbertokenFailure(const GoogleServiceAuthError& error) + OVERRIDE { + last_error_ = error; + ++nb_error_; + } + + std::string last_token_; + int nb_correct_token_; + GoogleServiceAuthError last_error_; + int nb_error_; +}; + +} // namespace + +class UbertokenFetcherTest : public testing::Test { + public: + virtual void SetUp() OVERRIDE { + request_context_getter_ = new net::TestURLRequestContextGetter( + base::MessageLoopProxy::current()); + fetcher_.reset(new UbertokenFetcher(&token_service_, + &consumer_, + request_context_getter_.get())); + } + + virtual void TearDown() OVERRIDE { + fetcher_.reset(); + } + + protected: + base::MessageLoop message_loop_; + net::TestURLFetcherFactory factory_; + FakeOAuth2TokenService token_service_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + MockUbertokenConsumer consumer_; + scoped_ptr<UbertokenFetcher> fetcher_; +}; + +TEST_F(UbertokenFetcherTest, Basic) { +} + +TEST_F(UbertokenFetcherTest, Success) { + fetcher_->StartFetchingToken(kTestAccountId); + fetcher_->OnGetTokenSuccess(NULL, "accessToken", base::Time()); + fetcher_->OnUberAuthTokenSuccess("uberToken"); + + EXPECT_EQ(0, consumer_.nb_error_); + EXPECT_EQ(1, consumer_.nb_correct_token_); + EXPECT_EQ("uberToken", consumer_.last_token_); +} + +TEST_F(UbertokenFetcherTest, NoRefreshToken) { + fetcher_->StartFetchingToken(kTestAccountId); + GoogleServiceAuthError error(GoogleServiceAuthError::USER_NOT_SIGNED_UP); + fetcher_->OnGetTokenFailure(NULL, error); + + EXPECT_EQ(1, consumer_.nb_error_); + EXPECT_EQ(0, consumer_.nb_correct_token_); +} + +TEST_F(UbertokenFetcherTest, FailureToGetAccessToken) { + fetcher_->StartFetchingToken(kTestAccountId); + GoogleServiceAuthError error(GoogleServiceAuthError::USER_NOT_SIGNED_UP); + fetcher_->OnGetTokenFailure(NULL, error); + + EXPECT_EQ(1, consumer_.nb_error_); + EXPECT_EQ(0, consumer_.nb_correct_token_); + EXPECT_EQ("", consumer_.last_token_); +} + +TEST_F(UbertokenFetcherTest, FailureToGetUberToken) { + fetcher_->StartFetchingToken(kTestAccountId); + GoogleServiceAuthError error(GoogleServiceAuthError::USER_NOT_SIGNED_UP); + fetcher_->OnGetTokenSuccess(NULL, "accessToken", base::Time()); + fetcher_->OnUberAuthTokenFailure(error); + + EXPECT_EQ(1, consumer_.nb_error_); + EXPECT_EQ(0, consumer_.nb_correct_token_); + EXPECT_EQ("", consumer_.last_token_); +} diff --git a/chromium/google_apis/gcm/DEPS b/chromium/google_apis/gcm/DEPS index 08ac400e0d4..047dc1564b5 100644 --- a/chromium/google_apis/gcm/DEPS +++ b/chromium/google_apis/gcm/DEPS @@ -7,7 +7,6 @@ include_rules = [ "+base", "+testing", - "+components/webdata/encryptor", "+google", # For third_party/protobuf/src. "+net", "+third_party/leveldatabase", diff --git a/chromium/google_apis/gcm/base/encryptor.h b/chromium/google_apis/gcm/base/encryptor.h new file mode 100644 index 00000000000..898e7dba77e --- /dev/null +++ b/chromium/google_apis/gcm/base/encryptor.h @@ -0,0 +1,27 @@ +// Copyright (c) 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_ENCRYPTOR_H_ +#define GOOGLE_APIS_GCM_ENCRYPTOR_H_ + +#include <string> +#include "google_apis/gcm/base/gcm_export.h" + +namespace gcm { + +class GCM_EXPORT Encryptor { + public: + // All methods below should be thread-safe. + virtual bool EncryptString(const std::string& plaintext, + std::string* ciphertext) = 0; + + virtual bool DecryptString(const std::string& ciphertext, + std::string* plaintext) = 0; + + virtual ~Encryptor() {} +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_ENCRYPTOR_H_ diff --git a/chromium/google_apis/gcm/base/fake_encryptor.cc b/chromium/google_apis/gcm/base/fake_encryptor.cc new file mode 100644 index 00000000000..ac58081ee4c --- /dev/null +++ b/chromium/google_apis/gcm/base/fake_encryptor.cc @@ -0,0 +1,24 @@ +// Copyright (c) 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/base/fake_encryptor.h" + +#include "base/base64.h" + +namespace gcm { + +FakeEncryptor::~FakeEncryptor() {} + +bool FakeEncryptor::EncryptString(const std::string& plaintext, + std::string* ciphertext) { + base::Base64Encode(plaintext, ciphertext); + return true; +} + +bool FakeEncryptor::DecryptString(const std::string& ciphertext, + std::string* plaintext) { + return base::Base64Decode(ciphertext, plaintext); +} + +} // namespace gcm diff --git a/chromium/google_apis/gcm/base/fake_encryptor.h b/chromium/google_apis/gcm/base/fake_encryptor.h new file mode 100644 index 00000000000..01ae67b0e85 --- /dev/null +++ b/chromium/google_apis/gcm/base/fake_encryptor.h @@ -0,0 +1,28 @@ +// Copyright (c) 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_BASE_ENCRYPTOR_H_ +#define GOOGLE_APIS_GCM_BASE_ENCRYPTOR_H_ + +#include "base/compiler_specific.h" +#include "google_apis/gcm/base/encryptor.h" + +namespace gcm { + +// Encryptor which simply base64-encodes the plaintext to get the +// ciphertext. Obviously, this should be used only for testing. +class FakeEncryptor : public Encryptor { + public: + virtual ~FakeEncryptor(); + + virtual bool EncryptString(const std::string& plaintext, + std::string* ciphertext) OVERRIDE; + + virtual bool DecryptString(const std::string& ciphertext, + std::string* plaintext) OVERRIDE; +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_BASE_ENCRYPTOR_H_ diff --git a/chromium/google_apis/gcm/base/mcs_message_unittest.cc b/chromium/google_apis/gcm/base/mcs_message_unittest.cc index 4d4ef598ad2..55ab49d130f 100644 --- a/chromium/google_apis/gcm/base/mcs_message_unittest.cc +++ b/chromium/google_apis/gcm/base/mcs_message_unittest.cc @@ -36,7 +36,7 @@ TEST_F(MCSMessageTest, Invalid) { TEST_F(MCSMessageTest, InitInferTag) { scoped_ptr<mcs_proto::LoginRequest> login_request( - BuildLoginRequest(kAndroidId, kSecret)); + BuildLoginRequest(kAndroidId, kSecret, "")); scoped_ptr<google::protobuf::MessageLite> login_copy( new mcs_proto::LoginRequest(*login_request)); MCSMessage message(*login_copy); @@ -54,7 +54,7 @@ TEST_F(MCSMessageTest, InitInferTag) { TEST_F(MCSMessageTest, InitWithTag) { scoped_ptr<mcs_proto::LoginRequest> login_request( - BuildLoginRequest(kAndroidId, kSecret)); + BuildLoginRequest(kAndroidId, kSecret, "")); scoped_ptr<google::protobuf::MessageLite> login_copy( new mcs_proto::LoginRequest(*login_request)); MCSMessage message(kLoginRequestTag, *login_copy); @@ -72,7 +72,7 @@ TEST_F(MCSMessageTest, InitWithTag) { TEST_F(MCSMessageTest, InitPassOwnership) { scoped_ptr<mcs_proto::LoginRequest> login_request( - BuildLoginRequest(kAndroidId, kSecret)); + BuildLoginRequest(kAndroidId, kSecret, "")); scoped_ptr<google::protobuf::MessageLite> login_copy( new mcs_proto::LoginRequest(*login_request)); MCSMessage message(kLoginRequestTag, diff --git a/chromium/google_apis/gcm/base/mcs_util.cc b/chromium/google_apis/gcm/base/mcs_util.cc index 736556079f8..b578e573922 100644 --- a/chromium/google_apis/gcm/base/mcs_util.cc +++ b/chromium/google_apis/gcm/base/mcs_util.cc @@ -8,6 +8,8 @@ #include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" +#include "base/time/clock.h" +#include "base/time/time.h" namespace gcm { @@ -37,17 +39,21 @@ const char* kProtoNames[] = { COMPILE_ASSERT(arraysize(kProtoNames) == kNumProtoTypes, ProtoNamesMustIncludeAllTags); -// TODO(zea): replace these with proper values. -const char kLoginId[] = "login-1"; +const char kLoginId[] = "chrome-"; const char kLoginDomain[] = "mcs.android.com"; const char kLoginDeviceIdPrefix[] = "android-"; -const char kLoginSettingName[] = "new_vc"; -const char kLoginSettingValue[] = "1"; +const char kLoginSettingDefaultName[] = "new_vc"; +const char kLoginSettingDefaultValue[] = "1"; + +// Maximum amount of time to save an unsent outgoing message for. +const int kMaxTTLSeconds = 4 * 7 * 24 * 60 * 60; // 4 weeks. } // namespace -scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id, - uint64 auth_token) { +scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest( + uint64 auth_id, + uint64 auth_token, + const std::string& version_string) { // Create a hex encoded auth id for the device id field. std::string auth_id_hex; auth_id_hex = base::StringPrintf("%" PRIx64, auth_id); @@ -58,12 +64,10 @@ scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id, scoped_ptr<mcs_proto::LoginRequest> login_request( new mcs_proto::LoginRequest()); - // TODO(zea): set better values. - login_request->set_account_id(1000000); login_request->set_adaptive_heartbeat(false); login_request->set_auth_service(mcs_proto::LoginRequest::ANDROID_ID); login_request->set_auth_token(auth_token_str); - login_request->set_id(kLoginId); + login_request->set_id(kLoginId + version_string); login_request->set_domain(kLoginDomain); login_request->set_device_id(kLoginDeviceIdPrefix + auth_id_hex); login_request->set_network_type(1); @@ -72,8 +76,8 @@ scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id, login_request->set_use_rmq2(true); login_request->add_setting(); - login_request->mutable_setting(0)->set_name(kLoginSettingName); - login_request->mutable_setting(0)->set_value(kLoginSettingValue); + login_request->mutable_setting(0)->set_name(kLoginSettingDefaultName); + login_request->mutable_setting(0)->set_value(kLoginSettingDefaultValue); return login_request.Pass(); } @@ -209,25 +213,50 @@ void SetLastStreamIdReceived(uint32 val, if (protobuf->GetTypeName() == kProtoNames[kIqStanzaTag]) { reinterpret_cast<mcs_proto::IqStanza*>(protobuf)-> set_last_stream_id_received(val); - return; + return; } else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatPingTag]) { reinterpret_cast<mcs_proto::HeartbeatPing*>(protobuf)-> set_last_stream_id_received(val); - return; + return; } else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatAckTag]) { reinterpret_cast<mcs_proto::HeartbeatAck*>(protobuf)-> set_last_stream_id_received(val); - return; + return; } else if (protobuf->GetTypeName() == kProtoNames[kDataMessageStanzaTag]) { reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf)-> set_last_stream_id_received(val); - return; + return; } else if (protobuf->GetTypeName() == kProtoNames[kLoginResponseTag]) { reinterpret_cast<mcs_proto::LoginResponse*>(protobuf)-> set_last_stream_id_received(val); - return; + return; } NOTREACHED(); } +bool HasTTLExpired(const google::protobuf::MessageLite& protobuf, + base::Clock* clock) { + if (protobuf.GetTypeName() != kProtoNames[kDataMessageStanzaTag]) + return false; + uint64 ttl = GetTTL(protobuf); + uint64 sent = + reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf)->sent(); + DCHECK(sent); + return ttl > 0 && + clock->Now() > + base::Time::FromInternalValue( + (sent + ttl) * base::Time::kMicrosecondsPerSecond); +} + +int GetTTL(const google::protobuf::MessageLite& protobuf) { + if (protobuf.GetTypeName() != kProtoNames[kDataMessageStanzaTag]) + return 0; + const mcs_proto::DataMessageStanza* data_message = + reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf); + if (!data_message->has_ttl()) + return kMaxTTLSeconds; + DCHECK_LE(data_message->ttl(), kMaxTTLSeconds); + return data_message->ttl(); +} + } // namespace gcm diff --git a/chromium/google_apis/gcm/base/mcs_util.h b/chromium/google_apis/gcm/base/mcs_util.h index 7f92564ad27..95d97f29489 100644 --- a/chromium/google_apis/gcm/base/mcs_util.h +++ b/chromium/google_apis/gcm/base/mcs_util.h @@ -8,6 +8,7 @@ #define GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_ #include <string> +#include <vector> #include "base/basictypes.h" #include "base/memory/ref_counted.h" @@ -15,6 +16,10 @@ #include "google_apis/gcm/base/gcm_export.h" #include "google_apis/gcm/protocol/mcs.pb.h" +namespace base { +class Clock; +} + namespace net { class StreamSocket; } @@ -52,7 +57,8 @@ enum MCSIqStanzaExtension { // Builds a LoginRequest with the hardcoded local data. GCM_EXPORT scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest( uint64 auth_id, - uint64 auth_token); + uint64 auth_token, + const std::string& version_string); // Builds a StreamAck IqStanza message. GCM_EXPORT scoped_ptr<mcs_proto::IqStanza> BuildStreamAck(); @@ -76,6 +82,13 @@ GCM_EXPORT void SetLastStreamIdReceived( uint32 last_stream_id_received, google::protobuf::MessageLite* protobuf); +// Returns whether the TTL (time to live) for this message has expired, based +// on the |sent| timestamps and base::TimeTicks::Now(). If |protobuf| is not +// for a DataMessageStanza or the TTL is 0, will return false. +GCM_EXPORT bool HasTTLExpired(const google::protobuf::MessageLite& protobuf, + base::Clock* clock); +GCM_EXPORT int GetTTL(const google::protobuf::MessageLite& protobuf); + } // namespace gcm #endif // GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_ diff --git a/chromium/google_apis/gcm/base/mcs_util_unittest.cc b/chromium/google_apis/gcm/base/mcs_util_unittest.cc index d25914583a4..00bac1175f9 100644 --- a/chromium/google_apis/gcm/base/mcs_util_unittest.cc +++ b/chromium/google_apis/gcm/base/mcs_util_unittest.cc @@ -19,11 +19,13 @@ const uint64 kAuthToken = 12345; // Build a login request protobuf. TEST(MCSUtilTest, BuildLoginRequest) { scoped_ptr<mcs_proto::LoginRequest> login_request = - BuildLoginRequest(kAuthId, kAuthToken); - ASSERT_EQ("login-1", login_request->id()); + BuildLoginRequest(kAuthId, kAuthToken, "1.0"); + ASSERT_EQ("chrome-1.0", login_request->id()); ASSERT_EQ(base::Uint64ToString(kAuthToken), login_request->auth_token()); ASSERT_EQ(base::Uint64ToString(kAuthId), login_request->user()); ASSERT_EQ("android-3d5c23dac2a1fa7c", login_request->device_id()); + ASSERT_EQ("new_vc", login_request->setting(0).name()); + ASSERT_EQ("1", login_request->setting(0).value()); // TODO(zea): test the other fields once they have valid values. } diff --git a/chromium/google_apis/gcm/base/socket_stream.cc b/chromium/google_apis/gcm/base/socket_stream.cc index 1a0b29d8d07..8c152c6c5b3 100644 --- a/chromium/google_apis/gcm/base/socket_stream.cc +++ b/chromium/google_apis/gcm/base/socket_stream.cc @@ -89,8 +89,8 @@ net::Error SocketInputStream::Refresh(const base::Closure& callback, DCHECK_GT(byte_limit, 0); if (byte_limit > read_buffer_->BytesRemaining()) { - NOTREACHED() << "Out of buffer space, closing input stream."; - CloseStream(net::ERR_UNEXPECTED, base::Closure()); + LOG(ERROR) << "Out of buffer space, closing input stream."; + CloseStream(net::ERR_FILE_TOO_BIG, base::Closure()); return net::OK; } 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 diff --git a/chromium/google_apis/gcm/gcm.gyp b/chromium/google_apis/gcm/gcm.gyp index f81c4ef4493..81dcf21596f 100644 --- a/chromium/google_apis/gcm/gcm.gyp +++ b/chromium/google_apis/gcm/gcm.gyp @@ -31,35 +31,47 @@ 'dependencies': [ '../../base/base.gyp:base', '../../base/third_party/dynamic_annotations/dynamic_annotations.gyp:dynamic_annotations', - '../../components/components.gyp:encryptor', '../../net/net.gyp:net', '../../third_party/leveldatabase/leveldatabase.gyp:leveldatabase', '../../third_party/protobuf/protobuf.gyp:protobuf_lite', '../../url/url.gyp:url_lib', ], 'sources': [ - 'base/mcs_message.h', 'base/mcs_message.cc', - 'base/mcs_util.h', + 'base/mcs_message.h', 'base/mcs_util.cc', - 'base/socket_stream.h', + 'base/mcs_util.h', 'base/socket_stream.cc', - 'engine/connection_factory.h', + 'base/socket_stream.h', + 'engine/checkin_request.cc', + 'engine/checkin_request.h', 'engine/connection_factory.cc', - 'engine/connection_factory_impl.h', + 'engine/connection_factory.h', 'engine/connection_factory_impl.cc', - 'engine/connection_handler.h', + 'engine/connection_factory_impl.h', 'engine/connection_handler.cc', - 'engine/connection_handler_impl.h', + 'engine/connection_handler.h', 'engine/connection_handler_impl.cc', - 'engine/mcs_client.h', + 'engine/connection_handler_impl.h', + 'engine/gcm_store.cc', + 'engine/gcm_store.h', + 'engine/gcm_store_impl.cc', + 'engine/gcm_store_impl.h', + 'engine/gservices_settings.cc', + 'engine/gservices_settings.h', + 'engine/heartbeat_manager.cc', + 'engine/heartbeat_manager.h', 'engine/mcs_client.cc', - 'engine/rmq_store.h', - 'engine/rmq_store.cc', - 'gcm_client.cc', - 'gcm_client.h', - 'gcm_client_impl.cc', - 'gcm_client_impl.h', + 'engine/mcs_client.h', + 'engine/registration_info.cc', + 'engine/registration_info.h', + 'engine/registration_request.cc', + 'engine/registration_request.h', + 'engine/unregistration_request.cc', + 'engine/unregistration_request.h', + 'monitoring/gcm_stats_recorder.h', + 'protocol/android_checkin.proto', + 'protocol/checkin.proto', 'protocol/mcs.proto', ], 'includes': [ @@ -67,6 +79,34 @@ ], }, + # The test support library that is needed to test gcm. + { + 'target_name': 'gcm_test_support', + 'type': 'static_library', + 'include_dirs': [ + '..', + ], + 'export_dependent_settings': [ + '../../third_party/protobuf/protobuf.gyp:protobuf_lite' + ], + 'dependencies': [ + '../../base/base.gyp:base', + '../../testing/gtest.gyp:gtest', + '../../third_party/protobuf/protobuf.gyp:protobuf_lite', + 'gcm', + ], + 'sources': [ + 'base/fake_encryptor.cc', + 'base/fake_encryptor.h', + 'engine/fake_connection_factory.cc', + 'engine/fake_connection_factory.h', + 'engine/fake_connection_handler.cc', + 'engine/fake_connection_handler.h', + 'monitoring/fake_gcm_stats_recorder.cc', + 'monitoring/fake_gcm_stats_recorder.h', + ], + }, + # A standalone MCS (mobile connection server) client. { 'target_name': 'mcs_probe', @@ -80,7 +120,8 @@ '../../net/net.gyp:net', '../../net/net.gyp:net_test_support', '../../third_party/protobuf/protobuf.gyp:protobuf_lite', - 'gcm' + 'gcm', + 'gcm_test_support' ], 'sources': [ 'tools/mcs_probe.cc', @@ -101,25 +142,27 @@ 'dependencies': [ '../../base/base.gyp:run_all_unittests', '../../base/base.gyp:base', - '../../components/components.gyp:encryptor', '../../net/net.gyp:net', '../../net/net.gyp:net_test_support', '../../testing/gtest.gyp:gtest', '../../third_party/protobuf/protobuf.gyp:protobuf_lite', - 'gcm' + 'mcs_probe', + 'gcm', + 'gcm_test_support' ], 'sources': [ 'base/mcs_message_unittest.cc', 'base/mcs_util_unittest.cc', 'base/socket_stream_unittest.cc', + 'engine/checkin_request_unittest.cc', 'engine/connection_factory_impl_unittest.cc', 'engine/connection_handler_impl_unittest.cc', - 'engine/fake_connection_factory.h', - 'engine/fake_connection_factory.cc', - 'engine/fake_connection_handler.h', - 'engine/fake_connection_handler.cc', + 'engine/gcm_store_impl_unittest.cc', + 'engine/gservices_settings_unittest.cc', + 'engine/heartbeat_manager_unittest.cc', 'engine/mcs_client_unittest.cc', - 'engine/rmq_store_unittest.cc', + 'engine/registration_request_unittest.cc', + 'engine/unregistration_request_unittest.cc', ] }, ], diff --git a/chromium/google_apis/gcm/gcm_client.cc b/chromium/google_apis/gcm/gcm_client.cc deleted file mode 100644 index e437a305e47..00000000000 --- a/chromium/google_apis/gcm/gcm_client.cc +++ /dev/null @@ -1,45 +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/gcm_client.h" - -#include "base/lazy_instance.h" -#include "google_apis/gcm/gcm_client_impl.h" - -namespace gcm { - -namespace { - -static base::LazyInstance<GCMClientImpl>::Leaky g_gcm_client = - LAZY_INSTANCE_INITIALIZER; -static GCMClient* g_gcm_client_override = NULL; - -} // namespace - -GCMClient::OutgoingMessage::OutgoingMessage() - : time_to_live(0) { -} - -GCMClient::OutgoingMessage::~OutgoingMessage() { -} - -GCMClient::IncomingMessage::IncomingMessage() { -} - -GCMClient::IncomingMessage::~IncomingMessage() { -} - -// static -GCMClient* GCMClient::Get() { - if (g_gcm_client_override) - return g_gcm_client_override; - return g_gcm_client.Pointer(); -} - -// static -void GCMClient::SetForTesting(GCMClient* client) { - g_gcm_client_override = client; -} - -} // namespace gcm diff --git a/chromium/google_apis/gcm/gcm_client.h b/chromium/google_apis/gcm/gcm_client.h deleted file mode 100644 index 66e17a04489..00000000000 --- a/chromium/google_apis/gcm/gcm_client.h +++ /dev/null @@ -1,193 +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_GCM_CLIENT_H_ -#define GOOGLE_APIS_GCM_GCM_CLIENT_H_ - -#include <map> -#include <string> -#include <vector> - -#include "base/basictypes.h" -#include "google_apis/gcm/base/gcm_export.h" - -namespace base { -class TaskRunner; -} - -namespace gcm { - -// Interface that encapsulates the network communications with the Google Cloud -// Messaging server. This interface is not supposed to be thread-safe. -class GCM_EXPORT GCMClient { - public: - enum Result { - // Successful operation. - SUCCESS, - // Invalid parameter. - INVALID_PARAMETER, - // Previous asynchronous operation is still pending to finish. Certain - // operation, like register, is only allowed one at a time. - ASYNC_OPERATION_PENDING, - // Network socket error. - NETWORK_ERROR, - // Problem at the server. - SERVER_ERROR, - // Exceeded the specified TTL during message sending. - TTL_EXCEEDED, - // Other errors. - UNKNOWN_ERROR - }; - - // Message data consisting of key-value pairs. - typedef std::map<std::string, std::string> MessageData; - - // Message to be delivered to the other party. - struct GCM_EXPORT OutgoingMessage { - OutgoingMessage(); - ~OutgoingMessage(); - - // Message ID. - std::string id; - // In seconds. - int time_to_live; - MessageData data; - }; - - // Message being received from the other party. - struct GCM_EXPORT IncomingMessage { - IncomingMessage(); - ~IncomingMessage(); - - MessageData data; - }; - - // The check-in info for the user. Returned by the server. - struct GCM_EXPORT CheckInInfo { - CheckInInfo() : android_id(0), secret(0) {} - bool IsValid() const { return android_id != 0 && secret != 0; } - void Reset() { - android_id = 0; - secret = 0; - } - - uint64 android_id; - uint64 secret; - }; - - // A delegate interface that allows the GCMClient instance to interact with - // its caller, i.e. notifying asynchronous event. - class Delegate { - public: - // Called when the user has been checked in successfully or an error occurs. - // |checkin_info|: valid if the checkin completed successfully. - // |result|: the type of the error if an error occured, success otherwise. - virtual void OnCheckInFinished(const CheckInInfo& checkin_info, - Result result) = 0; - - // Called when the registration completed successfully or an error occurs. - // |app_id|: application ID. - // |registration_id|: non-empty if the registration completed successfully. - // |result|: the type of the error if an error occured, success otherwise. - virtual void OnRegisterFinished(const std::string& app_id, - const std::string& registration_id, - Result result) = 0; - - // Called when the message is scheduled to send successfully or an error - // occurs. - // |app_id|: application ID. - // |message_id|: ID of the message being sent. - // |result|: the type of the error if an error occured, success otherwise. - virtual void OnSendFinished(const std::string& app_id, - const std::string& message_id, - Result result) = 0; - - // Called when a message has been received. - // |app_id|: application ID. - // |message|: message received. - virtual void OnMessageReceived(const std::string& app_id, - const IncomingMessage& message) = 0; - - // Called when some messages have been deleted from the server. - // |app_id|: application ID. - virtual void OnMessagesDeleted(const std::string& app_id) = 0; - - // Called when a message failed to send to the server. - // |app_id|: application ID. - // |message_id|: ID of the message being sent. - // |result|: the type of the error if an error occured, success otherwise. - virtual void OnMessageSendError(const std::string& app_id, - const std::string& message_id, - Result result) = 0; - - // Returns the checkin info associated with this user. The delegate class - // is expected to persist the checkin info that is provided by - // OnCheckInFinished. - virtual CheckInInfo GetCheckInInfo() const = 0; - - // Called when the loading from the persistent store is done. The loading - // is triggered asynchronously when GCMClient is created. - virtual void OnLoadingCompleted() = 0; - - // Returns a task runner for file operations that may block. This is used - // in writing to or reading from the persistent store. - virtual base::TaskRunner* GetFileTaskRunner() = 0; - }; - - // Returns the single instance. Multiple profiles share the same client - // that makes use of the same MCS connection. - static GCMClient* Get(); - - // Passes a mocked instance for testing purpose. - static void SetForTesting(GCMClient* client); - - // Checks in the user to use GCM. If the device has not been checked in, it - // will be done first. - // |username|: the username (email address) used to check in with the server. - // |delegate|: the delegate whose methods will be called asynchronously in - // response to events and messages. - virtual void CheckIn(const std::string& username, Delegate* delegate) = 0; - - // Registers the application for GCM. Delegate::OnRegisterFinished will be - // called asynchronously upon completion. - // |username|: the username (email address) passed in CheckIn. - // |app_id|: application ID. - // |cert|: SHA-1 of public key of the application, in base16 format. - // |sender_ids|: list of IDs of the servers that are allowed to send the - // messages to the application. These IDs are assigned by the - // Google API Console. - virtual void Register(const std::string& username, - const std::string& app_id, - const std::string& cert, - const std::vector<std::string>& sender_ids) = 0; - - // Unregisters the application from GCM when it is uninstalled. - // Delegate::OnUnregisterFinished will be called asynchronously upon - // completion. - // |username|: the username (email address) passed in CheckIn. - // |app_id|: application ID. - virtual void Unregister(const std::string& username, - const std::string& app_id) = 0; - - // Sends a message to a given receiver. Delegate::OnSendFinished will be - // called asynchronously upon completion. - // |username|: the username (email address) passed in CheckIn. - // |app_id|: application ID. - // |receiver_id|: registration ID of the receiver party. - // |message|: message to be sent. - virtual void Send(const std::string& username, - const std::string& app_id, - const std::string& receiver_id, - const OutgoingMessage& message) = 0; - - // Returns true if the loading from the persistent store is still in progress. - virtual bool IsLoading() const = 0; - - protected: - virtual ~GCMClient() {} -}; - -} // namespace gcm - -#endif // GOOGLE_APIS_GCM_GCM_CLIENT_H_ diff --git a/chromium/google_apis/gcm/gcm_client_impl.cc b/chromium/google_apis/gcm/gcm_client_impl.cc deleted file mode 100644 index 76900cda747..00000000000 --- a/chromium/google_apis/gcm/gcm_client_impl.cc +++ /dev/null @@ -1,39 +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/gcm_client_impl.h" - -namespace gcm { - -GCMClientImpl::GCMClientImpl() { -} - -GCMClientImpl::~GCMClientImpl() { -} - -void GCMClientImpl::CheckIn(const std::string& username, - Delegate* delegate) { -} - -void GCMClientImpl::Register(const std::string& username, - const std::string& app_id, - const std::string& cert, - const std::vector<std::string>& sender_ids) { -} - -void GCMClientImpl::Unregister(const std::string& username, - const std::string& app_id) { -} - -void GCMClientImpl::Send(const std::string& username, - const std::string& app_id, - const std::string& receiver_id, - const OutgoingMessage& message) { -} - -bool GCMClientImpl::IsLoading() const { - return false; -} - -} // namespace gcm diff --git a/chromium/google_apis/gcm/gcm_client_impl.h b/chromium/google_apis/gcm/gcm_client_impl.h deleted file mode 100644 index 46f910e546a..00000000000 --- a/chromium/google_apis/gcm/gcm_client_impl.h +++ /dev/null @@ -1,39 +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_GCM_CLIENT_IMPL_H_ -#define GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_ - -#include "base/compiler_specific.h" -#include "google_apis/gcm/gcm_client.h" - -namespace gcm { - -class GCMClientImpl : public GCMClient { - public: - GCMClientImpl(); - virtual ~GCMClientImpl(); - - // Overridden from GCMClient: - virtual void CheckIn(const std::string& username, - Delegate* delegate) OVERRIDE; - virtual void Register(const std::string& username, - const std::string& app_id, - const std::string& cert, - const std::vector<std::string>& sender_ids) OVERRIDE; - virtual void Unregister(const std::string& username, - const std::string& app_id) OVERRIDE; - virtual void Send(const std::string& username, - const std::string& app_id, - const std::string& receiver_id, - const OutgoingMessage& message) OVERRIDE; - virtual bool IsLoading() const OVERRIDE; - - private: - DISALLOW_COPY_AND_ASSIGN(GCMClientImpl); -}; - -} // namespace gcm - -#endif // GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_ diff --git a/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.cc b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.cc new file mode 100644 index 00000000000..5ed8a1df49c --- /dev/null +++ b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.cc @@ -0,0 +1,107 @@ +// 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/monitoring/fake_gcm_stats_recorder.h"
+
+namespace gcm {
+
+FakeGCMStatsRecorder::FakeGCMStatsRecorder() {
+}
+
+FakeGCMStatsRecorder::~FakeGCMStatsRecorder() {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinInitiated(uint64 android_id) {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinDelayedDueToBackoff(int64 delay_msec) {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinSuccess() {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinFailure(std::string status,
+ bool will_retry) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionInitiated(const std::string& host) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionDelayedDueToBackoff(
+ int64 delay_msec) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionSuccess() {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionFailure(int network_error) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionResetSignaled(
+ ConnectionFactory::ConnectionResetReason reason) {
+}
+
+void FakeGCMStatsRecorder::RecordRegistrationSent(
+ const std::string& app_id,
+ const std::string& sender_ids) {
+}
+
+void FakeGCMStatsRecorder::RecordRegistrationResponse(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ RegistrationRequest::Status status) {
+}
+
+void FakeGCMStatsRecorder::RecordRegistrationRetryRequested(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ int retries_left) {
+}
+
+void FakeGCMStatsRecorder::RecordUnregistrationSent(
+ const std::string& app_id) {
+}
+
+void FakeGCMStatsRecorder::RecordUnregistrationResponse(
+ const std::string& app_id,
+ UnregistrationRequest::Status status) {
+}
+
+void FakeGCMStatsRecorder::RecordUnregistrationRetryDelayed(
+ const std::string& app_id,
+ int64 delay_msec) {
+}
+
+void FakeGCMStatsRecorder::RecordDataMessageReceived(
+ const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ bool to_registered_app,
+ ReceivedMessageType message_type) {
+}
+
+void FakeGCMStatsRecorder::RecordDataSentToWire(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ int queued) {
+}
+
+void FakeGCMStatsRecorder::RecordNotifySendStatus(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ gcm::MCSClient::MessageSendStatus status,
+ int byte_size,
+ int ttl) {
+}
+
+void FakeGCMStatsRecorder::RecordIncomingSendError(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id) {
+}
+
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.h b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.h new file mode 100644 index 00000000000..1ada61107c3 --- /dev/null +++ b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.h @@ -0,0 +1,71 @@ +// 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_MONITORING_FAKE_GCM_STATS_RECODER_H_
+#define GOOGLE_APIS_GCM_MONITORING_FAKE_GCM_STATS_RECODER_H_
+
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+
+namespace gcm {
+
+// The fake version of GCMStatsRecorder that does nothing.
+class FakeGCMStatsRecorder : public GCMStatsRecorder {
+ public:
+ FakeGCMStatsRecorder();
+ virtual ~FakeGCMStatsRecorder();
+
+ virtual void RecordCheckinInitiated(uint64 android_id) OVERRIDE;
+ virtual void RecordCheckinDelayedDueToBackoff(int64 delay_msec) OVERRIDE;
+ virtual void RecordCheckinSuccess() OVERRIDE;
+ virtual void RecordCheckinFailure(std::string status,
+ bool will_retry) OVERRIDE;
+ virtual void RecordConnectionInitiated(const std::string& host) OVERRIDE;
+ virtual void RecordConnectionDelayedDueToBackoff(int64 delay_msec) OVERRIDE;
+ virtual void RecordConnectionSuccess() OVERRIDE;
+ virtual void RecordConnectionFailure(int network_error) OVERRIDE;
+ virtual void RecordConnectionResetSignaled(
+ ConnectionFactory::ConnectionResetReason reason) OVERRIDE;
+ virtual void RecordRegistrationSent(const std::string& app_id,
+ const std::string& sender_ids) OVERRIDE;
+ virtual void RecordRegistrationResponse(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ RegistrationRequest::Status status) OVERRIDE;
+ virtual void RecordRegistrationRetryRequested(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ int retries_left) OVERRIDE;
+ virtual void RecordUnregistrationSent(const std::string& app_id) OVERRIDE;
+ virtual void RecordUnregistrationResponse(
+ const std::string& app_id,
+ UnregistrationRequest::Status status) OVERRIDE;
+ virtual void RecordUnregistrationRetryDelayed(const std::string& app_id,
+ int64 delay_msec) OVERRIDE;
+ virtual void RecordDataMessageReceived(
+ const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ bool to_registered_app,
+ ReceivedMessageType message_type) OVERRIDE;
+ virtual void RecordDataSentToWire(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ int queued) OVERRIDE;
+ virtual void RecordNotifySendStatus(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status,
+ int byte_size,
+ int ttl) OVERRIDE;
+ virtual void RecordIncomingSendError(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id) OVERRIDE;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FakeGCMStatsRecorder);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_MONITORING_FAKE_GCM_STATS_RECODER_H_
diff --git a/chromium/google_apis/gcm/monitoring/gcm_stats_recorder.h b/chromium/google_apis/gcm/monitoring/gcm_stats_recorder.h new file mode 100644 index 00000000000..71915693fda --- /dev/null +++ b/chromium/google_apis/gcm/monitoring/gcm_stats_recorder.h @@ -0,0 +1,135 @@ +// 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_MONITORING_GCM_STATS_RECORDER_H_ +#define GOOGLE_APIS_GCM_MONITORING_GCM_STATS_RECORDER_H_ + +#include <string> +#include <vector> + +#include "base/time/time.h" +#include "google_apis/gcm/base/gcm_export.h" +#include "google_apis/gcm/engine/connection_factory.h" +#include "google_apis/gcm/engine/mcs_client.h" +#include "google_apis/gcm/engine/registration_request.h" +#include "google_apis/gcm/engine/unregistration_request.h" + +namespace gcm { + +// Defines the interface to record GCM internal stats and activities for +// debugging purpose. +class GCM_EXPORT GCMStatsRecorder { + public: + // Type of a received message + enum ReceivedMessageType { + // Data message. + DATA_MESSAGE, + // Message that indicates some messages have been deleted on the server. + DELETED_MESSAGES, + }; + + // A delegate interface that allows the GCMStatsRecorderImpl instance to + // interact with its container. + class Delegate { + public: + // Called when the GCMStatsRecorderImpl is recording activities and a new + // activity has just been recorded. + virtual void OnActivityRecorded() = 0; + }; + + GCMStatsRecorder() {} + virtual ~GCMStatsRecorder() {} + + // Records that a check-in has been initiated. + virtual void RecordCheckinInitiated(uint64 android_id) = 0; + + // Records that a check-in has been delayed due to backoff. + virtual void RecordCheckinDelayedDueToBackoff(int64 delay_msec) = 0; + + // Records that a check-in request has succeeded. + virtual void RecordCheckinSuccess() = 0; + + // Records that a check-in request has failed. If a retry will be tempted then + // will_retry should be true. + virtual void RecordCheckinFailure(std::string status, bool will_retry) = 0; + + // Records that a connection to MCS has been initiated. + virtual void RecordConnectionInitiated(const std::string& host) = 0; + + // Records that a connection has been delayed due to backoff. + virtual void RecordConnectionDelayedDueToBackoff(int64 delay_msec) = 0; + + // Records that connection has been successfully established. + virtual void RecordConnectionSuccess() = 0; + + // Records that connection has failed with a network error code. + virtual void RecordConnectionFailure(int network_error) = 0; + + // Records that connection reset has been signaled. + virtual void RecordConnectionResetSignaled( + ConnectionFactory::ConnectionResetReason reason) = 0; + + // Records that a registration request has been sent. This could be initiated + // directly from API, or from retry logic. + virtual void RecordRegistrationSent(const std::string& app_id, + const std::string& sender_ids) = 0; + + // Records that a registration response has been received from server. + virtual void RecordRegistrationResponse( + const std::string& app_id, + const std::vector<std::string>& sender_ids, + RegistrationRequest::Status status) = 0; + + // Records that a registration retry has been requested. The actual retry + // action may not occur until some time later according to backoff logic. + virtual void RecordRegistrationRetryRequested( + const std::string& app_id, + const std::vector<std::string>& sender_ids, + int retries_left) = 0; + + // Records that an unregistration request has been sent. This could be + // initiated directly from API, or from retry logic. + virtual void RecordUnregistrationSent(const std::string& app_id) = 0; + + // Records that an unregistration response has been received from server. + virtual void RecordUnregistrationResponse( + const std::string& app_id, + UnregistrationRequest::Status status) = 0; + + // Records that an unregistration retry has been requested and delayed due to + // backoff logic. + virtual void RecordUnregistrationRetryDelayed(const std::string& app_id, + int64 delay_msec) = 0; + + // Records that a data message has been received. If this message is not + // sent to a registered app, to_registered_app shoudl be false. If it + // indicates that a message has been dropped on the server, is_message_dropped + // should be true. + virtual void RecordDataMessageReceived(const std::string& app_id, + const std::string& from, + int message_byte_size, + bool to_registered_app, + ReceivedMessageType message_type) = 0; + + // Records that an outgoing data message was sent over the wire. + virtual void RecordDataSentToWire(const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id, + int queued) = 0; + // Records that the MCS client sent a 'send status' notification to callback. + virtual void RecordNotifySendStatus(const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id, + MCSClient::MessageSendStatus status, + int byte_size, + int ttl) = 0; + // Records that a 'send error' message was received. + virtual void RecordIncomingSendError(const std::string& app_id, + const std::string& receiver_id, + const std::string& message_id) = 0; +}; + +} // namespace gcm + +#endif // GOOGLE_APIS_GCM_MONITORING_GCM_STATS_RECORDER_H_ diff --git a/chromium/google_apis/gcm/protocol/android_checkin.proto b/chromium/google_apis/gcm/protocol/android_checkin.proto new file mode 100644 index 00000000000..a4520513888 --- /dev/null +++ b/chromium/google_apis/gcm/protocol/android_checkin.proto @@ -0,0 +1,97 @@ +// 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. +// +// Logging information for Android "checkin" events (automatic, periodic +// requests made by Android devices to the server). + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; +option retain_unknown_fields = true; +package checkin_proto; + +// Build characteristics unique to the Chrome browser, and Chrome OS +message ChromeBuildProto { + enum Platform { + PLATFORM_WIN = 1; + PLATFORM_MAC = 2; + PLATFORM_LINUX = 3; + PLATFORM_CROS = 4; + PLATFORM_IOS = 5; + // Just a placeholder. Likely don't need it due to the presence of the + // Android GCM on phone/tablet devices. + PLATFORM_ANDROID = 6; + } + + enum Channel { + CHANNEL_STABLE = 1; + CHANNEL_BETA = 2; + CHANNEL_DEV = 3; + CHANNEL_CANARY = 4; + CHANNEL_UNKNOWN = 5; // for tip of tree or custom builds + } + + // The platform of the device. + optional Platform platform = 1; + + // The Chrome instance's version. + optional string chrome_version = 2; + + // The Channel (build type) of Chrome. + optional Channel channel = 3; +} + +// Information sent by the device in a "checkin" request. +message AndroidCheckinProto { + // Miliseconds since the Unix epoch of the device's last successful checkin. + optional int64 last_checkin_msec = 2; + + // The current MCC+MNC of the mobile device's current cell. + optional string cell_operator = 6; + + // The MCC+MNC of the SIM card (different from operator if the + // device is roaming, for instance). + optional string sim_operator = 7; + + // The device's current roaming state (reported starting in eclair builds). + // Currently one of "{,not}mobile-{,not}roaming", if it is present at all. + optional string roaming = 8; + + // For devices supporting multiple user profiles (which may be + // supported starting in jellybean), the ordinal number of the + // profile that is checking in. This is 0 for the primary profile + // (which can't be changed without wiping the device), and 1,2,3,... + // for additional profiles (which can be added and deleted freely). + optional int32 user_number = 9; + + // Class of device. Indicates the type of build proto + // (IosBuildProto/ChromeBuildProto/AndroidBuildProto) + // That is included in this proto + optional DeviceType type = 12 [default = DEVICE_ANDROID_OS]; + + // For devices running MCS on Chrome, build-specific characteristics + // of the browser. There are no hardware aspects (except for ChromeOS). + // This will only be populated for Chrome builds/ChromeOS devices + optional checkin_proto.ChromeBuildProto chrome_build = 13; + + // Note: Some of the Android specific optional fields were skipped to limit + // the protobuf definition. + // Next 14 +} + +// enum values correspond to the type of device. +// Used in the AndroidCheckinProto and Device proto. +enum DeviceType { + // Android Device + DEVICE_ANDROID_OS = 1; + + // Apple IOS device + DEVICE_IOS_OS = 2; + + // Chrome browser - Not Chrome OS. No hardware records. + DEVICE_CHROME_BROWSER = 3; + + // Chrome OS + DEVICE_CHROME_OS = 4; +} diff --git a/chromium/google_apis/gcm/protocol/checkin.proto b/chromium/google_apis/gcm/protocol/checkin.proto new file mode 100644 index 00000000000..10ad628f733 --- /dev/null +++ b/chromium/google_apis/gcm/protocol/checkin.proto @@ -0,0 +1,156 @@ +// 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. +// +// Request and reply to the "checkin server" devices poll every few hours. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; +option retain_unknown_fields = true; + +package checkin_proto; + +import "android_checkin.proto"; + +// A concrete name/value pair sent to the device's Gservices database. +message GservicesSetting { + required bytes name = 1; + required bytes value = 2; +} + +// Devices send this every few hours to tell us how they're doing. +message AndroidCheckinRequest { + // IMEI (used by GSM phones) is sent and stored as 15 decimal + // digits; the 15th is a check digit. + optional string imei = 1; // IMEI, reported but not logged. + + // MEID (used by CDMA phones) is sent and stored as 14 hexadecimal + // digits (no check digit). + optional string meid = 10; // MEID, reported but not logged. + + // MAC address (used by non-phone devices). 12 hexadecimal digits; + // no separators (eg "0016E6513AC2", not "00:16:E6:51:3A:C2"). + repeated string mac_addr = 9; // MAC address, reported but not logged. + + // An array parallel to mac_addr, describing the type of interface. + // Currently accepted values: "wifi", "ethernet", "bluetooth". If + // not present, "wifi" is assumed. + repeated string mac_addr_type = 19; + + // Serial number (a manufacturer-defined unique hardware + // identifier). Alphanumeric, case-insensitive. + optional string serial_number = 16; + + // Older CDMA networks use an ESN (8 hex digits) instead of an MEID. + optional string esn = 17; // ESN, reported but not logged + + optional int64 id = 2; // Android device ID, not logged + optional int64 logging_id = 7; // Pseudonymous logging ID for Sawmill + optional string digest = 3; // Digest of device provisioning, not logged. + optional string locale = 6; // Current locale in standard (xx_XX) format + required AndroidCheckinProto checkin = 4; + + // DEPRECATED, see AndroidCheckinProto.requested_group + optional string desired_build = 5; + + // Blob of data from the Market app to be passed to Market API server + optional string market_checkin = 8; + + // SID cookies of any google accounts stored on the phone. Not logged. + repeated string account_cookie = 11; + + // Time zone. Not currently logged. + optional string time_zone = 12; + + // Security token used to validate the checkin request. + // Required for android IDs issued to Froyo+ devices, not for legacy IDs. + optional fixed64 security_token = 13; + + // Version of checkin protocol. + // + // There are currently two versions: + // + // - version field missing: android IDs are assigned based on + // hardware identifiers. unsecured in the sense that you can + // "unregister" someone's phone by sending a registration request + // with their IMEI/MEID/MAC. + // + // - version=2: android IDs are assigned randomly. The device is + // sent a security token that must be included in all future + // checkins for that android id. + // + // - version=3: same as version 2, but the 'fragment' field is + // provided, and the device understands incremental updates to the + // gservices table (ie, only returning the keys whose values have + // changed.) + // + // (version=1 was skipped to avoid confusion with the "missing" + // version field that is effectively version 1.) + optional int32 version = 14; + + // OTA certs accepted by device (base-64 SHA-1 of cert files). Not + // logged. + repeated string ota_cert = 15; + + // Honeycomb and newer devices send configuration data with their checkin. + // optional DeviceConfigurationProto device_configuration = 18; + + // A single CheckinTask on the device may lead to multiple checkin + // requests if there is too much log data to upload in a single + // request. For version 3 and up, this field will be filled in with + // the number of the request, starting with 0. + optional int32 fragment = 20; + + // For devices supporting multiple users, the name of the current + // profile (they all check in independently, just as if they were + // multiple physical devices). This may not be set, even if the + // device is using multiuser. (checkin.user_number should be set to + // the ordinal of the user.) + optional string user_name = 21; + + // For devices supporting multiple user profiles, the serial number + // for the user checking in. Not logged. May not be set, even if + // the device supportes multiuser. checkin.user_number is the + // ordinal of the user (0, 1, 2, ...), which may be reused if users + // are deleted and re-created. user_serial_number is never reused + // (unless the device is wiped). + optional int32 user_serial_number = 22; + + // NEXT TAG: 23 +} + +// The response to the device. +message AndroidCheckinResponse { + required bool stats_ok = 1; // Whether statistics were recorded properly. + optional int64 time_msec = 3; // Time of day from server (Java epoch). + // repeated AndroidIntentProto intent = 2; + + // Provisioning is sent if the request included an obsolete digest. + // + // For version <= 2, 'digest' contains the digest that should be + // sent back to the server on the next checkin, and 'setting' + // contains the entire gservices table (which replaces the entire + // current table on the device). + // + // for version >= 3, 'digest' will be absent. If 'settings_diff' + // is false, then 'setting' contains the entire table, as in version + // 2. If 'settings_diff' is true, then 'delete_setting' contains + // the keys to delete, and 'setting' contains only keys to be added + // or for which the value has changed. All other keys in the + // current table should be left untouched. If 'settings_diff' is + // absent, don't touch the existing gservices table. + // + optional string digest = 4; + optional bool settings_diff = 9; + repeated string delete_setting = 10; + repeated GservicesSetting setting = 5; + + optional bool market_ok = 6; // If Market got the market_checkin data OK. + + optional fixed64 android_id = 7; // From the request, or newly assigned + optional fixed64 security_token = 8; // The associated security token + + optional string version_info = 11; + // NEXT TAG: 12 +} diff --git a/chromium/google_apis/gcm/tools/mcs_probe.cc b/chromium/google_apis/gcm/tools/mcs_probe.cc index bc4ad7cad3a..1bb0bc14dfc 100644 --- a/chromium/google_apis/gcm/tools/mcs_probe.cc +++ b/chromium/google_apis/gcm/tools/mcs_probe.cc @@ -8,6 +8,7 @@ #include <cstddef> #include <cstdio> #include <string> +#include <vector> #include "base/at_exit.h" #include "base/command_line.h" @@ -20,11 +21,17 @@ #include "base/strings/string_number_conversions.h" #include "base/threading/thread.h" #include "base/threading/worker_pool.h" +#include "base/time/default_clock.h" #include "base/values.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/engine/checkin_request.h" #include "google_apis/gcm/engine/connection_factory_impl.h" +#include "google_apis/gcm/engine/gcm_store_impl.h" +#include "google_apis/gcm/engine/gservices_settings.h" #include "google_apis/gcm/engine/mcs_client.h" +#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h" #include "net/base/host_mapping_rules.h" #include "net/base/net_log_logger.h" #include "net/cert/cert_verifier.h" @@ -48,6 +55,35 @@ namespace gcm { namespace { +const net::BackoffEntry::Policy kDefaultBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + 0, + + // 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, +}; + +// Default values used to communicate with the check-in server. +const char kChromeVersion[] = "Chrome MCS Probe"; + // The default server to communicate with. const char kMCSServerHost[] = "mtalk.google.com"; const uint16 kMCSServerPort = 5228; @@ -82,8 +118,14 @@ void MessageReceivedCallback(const MCSMessage& message) { } } -void MessageSentCallback(const std::string& local_id) { - LOG(INFO) << "Message sent. Status: " << local_id; +void MessageSentCallback(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status) { + LOG(INFO) << "Message sent. Serial number: " << user_serial_number + << " Application ID: " << app_id + << " Message ID: " << message_id + << " Message send status: " << status; } // Needed to use a real host resolver. @@ -164,16 +206,22 @@ class MCSProbe { uint64 secret() const { return secret_; } private: + void CheckIn(); void InitializeNetworkState(); void BuildNetworkSession(); - void InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token); + void LoadCallback(scoped_ptr<GCMStore::LoadResult> load_result); + void UpdateCallback(bool success); + void ErrorCallback(); + void OnCheckInCompleted( + const checkin_proto::AndroidCheckinResponse& checkin_response); + void StartMCSLogin(); + + base::DefaultClock clock_; CommandLine command_line_; - base::FilePath rmq_path_; + base::FilePath gcm_store_path_; uint64 android_id_; uint64 secret_; std::string server_host_; @@ -195,7 +243,10 @@ class MCSProbe { scoped_refptr<net::HttpNetworkSession> network_session_; scoped_ptr<net::ProxyService> proxy_service_; + FakeGCMStatsRecorder recorder_; + scoped_ptr<GCMStore> gcm_store_; scoped_ptr<MCSClient> mcs_client_; + scoped_ptr<CheckinRequest> checkin_request_; scoped_ptr<ConnectionFactoryImpl> connection_factory_; @@ -208,14 +259,14 @@ MCSProbe::MCSProbe( const CommandLine& command_line, scoped_refptr<net::URLRequestContextGetter> url_request_context_getter) : command_line_(command_line), - rmq_path_(base::FilePath(FILE_PATH_LITERAL("gcm_rmq_store"))), + gcm_store_path_(base::FilePath(FILE_PATH_LITERAL("gcm_store"))), android_id_(0), secret_(0), server_port_(0), url_request_context_getter_(url_request_context_getter), file_thread_("FileThread") { if (command_line.HasSwitch(kRMQFileName)) { - rmq_path_ = command_line.GetSwitchValuePath(kRMQFileName); + gcm_store_path_ = command_line.GetSwitchValuePath(kRMQFileName); } if (command_line.HasSwitch(kAndroidIdSwitch)) { base::StringToUint64(command_line.GetSwitchValueASCII(kAndroidIdSwitch), @@ -244,22 +295,64 @@ void MCSProbe::Start() { file_thread_.Start(); InitializeNetworkState(); BuildNetworkSession(); + std::vector<GURL> endpoints(1, + GURL("https://" + + net::HostPortPair(server_host_, + server_port_).ToString())); connection_factory_.reset( - new ConnectionFactoryImpl(GURL("https://" + net::HostPortPair( - server_host_, server_port_).ToString()), + new ConnectionFactoryImpl(endpoints, + kDefaultBackoffPolicy, network_session_, - &net_log_)); - mcs_client_.reset(new MCSClient(rmq_path_, + &net_log_, + &recorder_)); + gcm_store_.reset( + new GCMStoreImpl(gcm_store_path_, + file_thread_.message_loop_proxy(), + make_scoped_ptr<Encryptor>(new FakeEncryptor))); + mcs_client_.reset(new MCSClient("probe", + &clock_, connection_factory_.get(), - file_thread_.message_loop_proxy())); + gcm_store_.get(), + &recorder_)); run_loop_.reset(new base::RunLoop()); - mcs_client_->Initialize(base::Bind(&MCSProbe::InitializationCallback, - base::Unretained(this)), - base::Bind(&MessageReceivedCallback), - base::Bind(&MessageSentCallback)); + gcm_store_->Load(base::Bind(&MCSProbe::LoadCallback, + base::Unretained(this))); run_loop_->Run(); } +void MCSProbe::LoadCallback(scoped_ptr<GCMStore::LoadResult> load_result) { + DCHECK(load_result->success); + if (android_id_ != 0 && secret_ != 0) { + DVLOG(1) << "Presetting MCS id " << android_id_; + load_result->device_android_id = android_id_; + load_result->device_security_token = secret_; + gcm_store_->SetDeviceCredentials(android_id_, + secret_, + base::Bind(&MCSProbe::UpdateCallback, + base::Unretained(this))); + } else { + android_id_ = load_result->device_android_id; + secret_ = load_result->device_security_token; + DVLOG(1) << "Loaded MCS id " << android_id_; + } + mcs_client_->Initialize( + base::Bind(&MCSProbe::ErrorCallback, base::Unretained(this)), + base::Bind(&MessageReceivedCallback), + base::Bind(&MessageSentCallback), + load_result.Pass()); + + if (!android_id_ || !secret_) { + DVLOG(1) << "Checkin to generate new MCS credentials."; + CheckIn(); + return; + } + + StartMCSLogin(); +} + +void MCSProbe::UpdateCallback(bool success) { +} + void MCSProbe::InitializeNetworkState() { FILE* log_file = NULL; if (command_line_.HasSwitch(kLogFileSwitch)) { @@ -317,7 +410,6 @@ void MCSProbe::BuildNetworkSession() { session_params.network_delegate = NULL; // TODO(zea): implement? session_params.host_mapping_rules = host_mapping_rules_.get(); session_params.ignore_certificate_errors = true; - session_params.http_pipelining_enabled = false; session_params.testing_fixed_http_port = 0; session_params.testing_fixed_https_port = 0; session_params.net_log = &net_log_; @@ -326,16 +418,59 @@ void MCSProbe::BuildNetworkSession() { network_session_ = new net::HttpNetworkSession(session_params); } -void MCSProbe::InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token) { - LOG(INFO) << "Initialization " << (success ? "success!" : "failure!"); - if (restored_android_id && restored_security_token) { - android_id_ = restored_android_id; - secret_ = restored_security_token; - } - if (success) - mcs_client_->Login(android_id_, secret_); +void MCSProbe::ErrorCallback() { + LOG(INFO) << "MCS error happened"; +} + +void MCSProbe::CheckIn() { + LOG(INFO) << "Check-in request initiated."; + checkin_proto::ChromeBuildProto chrome_build_proto; + 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( + 0, 0, std::string(), chrome_build_proto); + + checkin_request_.reset(new CheckinRequest( + GServicesSettings::DefaultCheckinURL(), + request_info, + kDefaultBackoffPolicy, + base::Bind(&MCSProbe::OnCheckInCompleted, base::Unretained(this)), + url_request_context_getter_.get(), + &recorder_)); + checkin_request_->Start(); +} + +void MCSProbe::OnCheckInCompleted( + const checkin_proto::AndroidCheckinResponse& checkin_response) { + bool success = checkin_response.has_android_id() && + checkin_response.android_id() != 0UL && + checkin_response.has_security_token() && + checkin_response.security_token() != 0UL; + LOG(INFO) << "Check-in request completion " + << (success ? "success!" : "failure!"); + + if (!success) + return; + + android_id_ = checkin_response.android_id(); + secret_ = checkin_response.security_token(); + + gcm_store_->SetDeviceCredentials(android_id_, + secret_, + base::Bind(&MCSProbe::UpdateCallback, + base::Unretained(this))); + + StartMCSLogin(); +} + +void MCSProbe::StartMCSLogin() { + LOG(INFO) << "MCS login initiated."; + + mcs_client_->Login(android_id_, secret_); } int MCSProbeMain(int argc, char* argv[]) { diff --git a/chromium/google_apis/google_api_keys.cc b/chromium/google_apis/google_api_keys.cc index 94f6812ab74..f879979bc78 100644 --- a/chromium/google_apis/google_api_keys.cc +++ b/chromium/google_apis/google_api_keys.cc @@ -12,6 +12,7 @@ #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/strings/stringize_macros.h" +#include "google_apis/gaia/gaia_switches.h" #if defined(GOOGLE_CHROME_BUILD) || defined(USE_OFFICIAL_GOOGLE_API_KEYS) #include "google_apis/internal/google_chrome_api_keys.h" @@ -69,16 +70,6 @@ #define GOOGLE_DEFAULT_CLIENT_SECRET "" #endif -namespace switches { - -// Specifies custom OAuth2 client id for testing purposes. -const char kOAuth2ClientID[] = "oauth2-client-id"; - -// Specifies custom OAuth2 client secret for testing purposes. -const char kOAuth2ClientSecret[] = "oauth2-client-secret"; - -} // namespace switches - namespace google_apis { // This is used as a lazy instance to determine keys once and cache them. @@ -269,4 +260,12 @@ std::string GetOAuth2ClientSecret(OAuth2Client client) { return g_api_key_cache.Get().GetClientSecret(client); } +bool IsGoogleChromeAPIKeyUsed() { +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_OFFICIAL_GOOGLE_API_KEYS) + return true; +#else + return false; +#endif +} + } // namespace google_apis diff --git a/chromium/google_apis/google_api_keys.h b/chromium/google_apis/google_api_keys.h index 8f4bc7e9cac..5acdf263a6f 100644 --- a/chromium/google_apis/google_api_keys.h +++ b/chromium/google_apis/google_api_keys.h @@ -90,6 +90,10 @@ std::string GetOAuth2ClientID(OAuth2Client client); // in, e.g. URL-escaped if you use it in a URL. std::string GetOAuth2ClientSecret(OAuth2Client client); +// Returns if the API key using in the current build is the one for official +// Google Chrome. +bool IsGoogleChromeAPIKeyUsed(); + } // namespace google_apis #endif // GOOGLE_APIS_GOOGLE_API_KEYS_H_ diff --git a/chromium/google_apis/google_api_keys_unittest.cc b/chromium/google_apis/google_api_keys_unittest.cc index 7432f93e830..e18cff52f18 100644 --- a/chromium/google_apis/google_api_keys_unittest.cc +++ b/chromium/google_apis/google_api_keys_unittest.cc @@ -13,6 +13,7 @@ #include "google_apis/google_api_keys.h" #include "build/build_config.h" +#include "google_apis/gaia/gaia_switches.h" #include "testing/gtest/include/gtest/gtest.h" // The Win builders fail (with a linker crash) when trying to link diff --git a/chromium/google_apis/google_apis.gyp b/chromium/google_apis/google_apis.gyp index 2d281f2b2f3..77dfce9c5d6 100644 --- a/chromium/google_apis/google_apis.gyp +++ b/chromium/google_apis/google_apis.gyp @@ -36,23 +36,29 @@ 'GOOGLE_DEFAULT_CLIENT_SECRET="<(google_default_client_secret)"', ] }], - [ 'OS == "android"', { - 'dependencies': [ - '../third_party/openssl/openssl.gyp:openssl', - ], - 'sources/': [ - ['exclude', 'cup/client_update_protocol_nss\.cc$'], - ], + ['OS == "mac" or OS == "ios" or OS == "win"', { + 'dependencies': [ + '../third_party/nss/nss.gyp:nspr', + '../third_party/nss/nss.gyp:nss', + ], + }], + ['OS == "android"', { + 'dependencies': [ + '../third_party/openssl/openssl.gyp:openssl', + ], + 'sources/': [ + ['exclude', 'cup/client_update_protocol_nss\.cc$'], + ], + }], + ['use_openssl==1', { + 'sources!': [ + 'cup/client_update_protocol_nss.cc', + ], + }, { + 'sources!': [ + 'cup/client_update_protocol_openssl.cc', + ], }], - [ 'use_openssl==1', { - 'sources!': [ - 'cup/client_update_protocol_nss.cc', - ], - }, { - 'sources!': [ - 'cup/client_update_protocol_openssl.cc', - ], - },], ], 'sources': [ 'cup/client_update_protocol.cc', @@ -73,8 +79,6 @@ 'drive/drive_api_url_generator.h', 'drive/drive_common_callbacks.h', 'drive/drive_entry_kinds.h', - 'drive/gdata_contacts_requests.cc', - 'drive/gdata_contacts_requests.h', 'drive/gdata_errorcode.cc', 'drive/gdata_errorcode.h', 'drive/gdata_wapi_requests.cc', @@ -91,6 +95,8 @@ 'drive/task_util.h', 'drive/time_util.cc', 'drive/time_util.h', + 'gaia/account_tracker.cc', + 'gaia/account_tracker.h', 'gaia/gaia_auth_consumer.cc', 'gaia/gaia_auth_consumer.h', 'gaia/gaia_auth_fetcher.cc', @@ -107,17 +113,27 @@ 'gaia/gaia_urls.h', 'gaia/google_service_auth_error.cc', 'gaia/google_service_auth_error.h', + 'gaia/identity_provider.cc', + 'gaia/identity_provider.h', + 'gaia/merge_session_helper.cc', + 'gaia/merge_session_helper.h', 'gaia/oauth_request_signer.cc', 'gaia/oauth_request_signer.h', 'gaia/oauth2_access_token_consumer.h', - 'gaia/oauth2_access_token_fetcher.cc', 'gaia/oauth2_access_token_fetcher.h', + 'gaia/oauth2_access_token_fetcher.cc', + 'gaia/oauth2_access_token_fetcher_impl.cc', + 'gaia/oauth2_access_token_fetcher_impl.h', 'gaia/oauth2_api_call_flow.cc', 'gaia/oauth2_api_call_flow.h', 'gaia/oauth2_mint_token_flow.cc', 'gaia/oauth2_mint_token_flow.h', 'gaia/oauth2_token_service.cc', 'gaia/oauth2_token_service.h', + 'gaia/oauth2_token_service_request.cc', + 'gaia/oauth2_token_service_request.h', + 'gaia/ubertoken_fetcher.cc', + 'gaia/ubertoken_fetcher.h', 'google_api_keys.cc', 'google_api_keys.h', ], @@ -129,14 +145,55 @@ 'type': 'executable', 'dependencies': [ '../base/base.gyp:run_all_unittests', + '../testing/gmock.gyp:gmock', '../testing/gtest.gyp:gtest', 'google_apis', + 'google_apis_test_support', + ], + 'includes': [ + 'determine_use_official_keys.gypi', ], 'include_dirs': [ '..', ], 'sources': [ 'google_api_keys_unittest.cc', + 'cup/client_update_protocol_unittest.cc', + 'drive/base_requests_unittest.cc', + 'drive/base_requests_server_unittest.cc', + 'drive/drive_api_requests_unittest.cc', + 'drive/drive_api_parser_unittest.cc', + 'drive/drive_api_url_generator_unittest.cc', + 'drive/gdata_wapi_parser_unittest.cc', + 'drive/gdata_wapi_requests_unittest.cc', + 'drive/gdata_wapi_url_generator_unittest.cc', + 'drive/request_sender_unittest.cc', + 'drive/request_util_unittest.cc', + 'drive/time_util_unittest.cc', + 'gaia/account_tracker_unittest.cc', + 'gaia/gaia_auth_fetcher_unittest.cc', + 'gaia/gaia_auth_util_unittest.cc', + 'gaia/gaia_oauth_client_unittest.cc', + 'gaia/google_service_auth_error_unittest.cc', + 'gaia/merge_session_helper_unittest.cc', + 'gaia/oauth_request_signer_unittest.cc', + 'gaia/oauth2_access_token_fetcher_impl_unittest.cc', + 'gaia/oauth2_api_call_flow_unittest.cc', + 'gaia/oauth2_mint_token_flow_unittest.cc', + 'gaia/oauth2_token_service_request_unittest.cc', + 'gaia/oauth2_token_service_unittest.cc', + 'gaia/ubertoken_fetcher_unittest.cc', + ], + 'conditions': [ + ['OS=="android"', { + 'sources!': [ + 'drive/base_requests_server_unittest.cc', + 'drive/drive_api_parser_unittest.cc', + 'drive/drive_api_requests_unittest.cc', + 'drive/gdata_wapi_parser_unittest.cc', + 'drive/gdata_wapi_requests_unittest.cc', + ], + }], ], }, { @@ -155,8 +212,19 @@ '../net/net.gyp:net_test_support', ], 'sources': [ + 'drive/dummy_auth_service.cc', + 'drive/dummy_auth_service.h', + 'drive/test_util.cc', + 'drive/test_util.h', 'gaia/fake_gaia.cc', 'gaia/fake_gaia.h', + 'gaia/fake_identity_provider.cc', + 'gaia/fake_identity_provider.h', + 'gaia/fake_oauth2_token_service.cc', + 'gaia/fake_oauth2_token_service.h', + 'gaia/mock_url_fetcher_factory.h', + 'gaia/oauth2_token_service_test_util.cc', + 'gaia/oauth2_token_service_test_util.h', ], }, ], |